PR-based release flow with signed tags, branch protection compatibility, and CI polling
From gh-guardnpx claudepluginhub anthropics/claude-plugins-community --plugin gh-guardThis skill is limited to using the following tools:
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
This skill covers a PR-based release flow that works with branch protection rules, signed tags, and CI-triggered publishing.
Developer GitHub crates.io
│ │ │
├─ scripts/release.sh 0.2.0 │ │
│ │ │
├─ Local checks (deny, │ │
│ test, clippy, dry-run) │ │
│ │ │
├─ Create release/v0.2.0 ──→│ │
├─ Bump Cargo.toml ────────→│ │
├─ Push branch ────────────→│ │
├─ gh pr create ───────────→│── CI runs on PR │
│ │ │
├─ Poll for checks start │ │
├─ gh pr checks --watch ───→│── Waits for CI │
│ │ │
├─ gh pr merge ────────────→│── Squash merge │
├─ git pull origin main │ │
├─ git tag -s v0.2.0 ──────→│ │
├─ git push origin v0.2.0 ─→│── Tag push triggers: │
│ │ 1. publish (crates.io) ────→│
│ │ 2. provenance (SLSA L3) │
│ │ 3. release (GitHub) │
│ │ │
The release script verifies before doing anything:
X.Y.Zgit config user.signingkey must be setmainorigin/mainCargo.toml version differs from targetv${VERSION} tag not already presentBefore creating the PR, run local checks to catch issues early:
cargo deny check advisories bans licenses sources
cargo test --locked --all-features --quiet
cargo clippy --all-features -- -D warnings
cargo publish --dry-run --locked
This avoids wasting CI time on PRs that would fail basic checks.
GOTCHA: gh pr checks --watch has a race condition — it returns immediately with success if no checks have been registered yet (which happens in the few seconds between PR creation and GitHub processing the workflow triggers).
Solution: Poll for check existence before watching:
echo "Waiting for CI checks to start..."
for i in $(seq 1 30); do
if gh pr checks "$PR_NUMBER" --json name --jq '.[0].name' &>/dev/null 2>&1; then
break
fi
sleep 2
done
echo "Waiting for CI checks to complete..."
gh pr checks "$PR_NUMBER" --watch
Tags should be signed to verify the release was created by an authorized maintainer:
git tag -s "v$VERSION" -m "Release v$VERSION"
SSH signing setup (recommended):
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global tag.gpgSign true
GPG signing setup:
git config --global user.signingkey <GPG_KEY_ID>
git config --global tag.gpgSign true
The release script handles branch protection gracefully:
if ! gh pr merge "$PR_NUMBER" --squash --delete-branch; then
echo "Standard merge failed. --admin bypasses branch protection."
read -rp "Merge with --admin? [y/N] " ADMIN_CONFIRM
if [[ "$ADMIN_CONFIRM" =~ ^[Yy]$ ]]; then
gh pr merge "$PR_NUMBER" --squash --delete-branch --admin
else
echo "Aborting. Fix the issue and merge manually."
git checkout main
exit 1
fi
fi
--admin merge (bypasses protection — requires explicit consent)Note: Admin merges lower the Scorecard Code-Review score. For best scores, have another maintainer review the version bump PR.
If your org or repo has tag protection rules:
Cargo.tomlorigin/mainIf a publish workflow fails (e.g., crates.io outage), use workflow_dispatch instead of gh run rerun:
# Preferred: retrigger with current workflow file
gh workflow run publish.yml -f tag=v0.1.5
# Avoid: gh run rerun uses the ORIGINAL workflow file, not current
Why not gh run rerun: It re-runs with the original workflow file from the commit, not the current one. If you fixed a bug in the workflow file, the rerun will use the old buggy version.
The publish workflow's PUBLISH_TAG env var resolves the tag from either trigger type, so all verification steps work correctly with workflow_dispatch.
After the signed tag is pushed, CI handles everything:
.intoto.jsonl attachedMonitor the pipeline:
gh run watch $(gh run list --limit 1 --workflow publish.yml --json databaseId -q '.[0].databaseId')
See templates/release.sh for the complete release script.