From 2da8836c02041b42fa3dcbe6758e48b7915f4b4b Mon Sep 17 00:00:00 2001 From: Alejo Amiras Date: Tue, 29 Sep 2026 21:41:59 +0000 Subject: [PATCH 1/2] docs: stop the release skill from deleting and re-creating release tags Step 4 deleted the remote tag before every push, so re-running it after a publish would move a published version's tag off the commit its provenance names. It now stops if the version is already on npm, if npm can't be reached, or if the tag already exists on origin or origin can't be reached. The tag is created with --no-sign so a machine with tag.gpgSign=true still makes a lightweight tag instead of opening an editor for a signed annotated one. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/release/SKILL.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 8a8401fb..23e8bd2e 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -80,8 +80,15 @@ and npm versions are **immutable**. Before running the push, show the user the e even for an rc. (The local tag/guard steps are safe to run first; only the `git push origin` line is gated.) ```bash -git tag -d "v$TARGET" 2>/dev/null; git push origin ":refs/tags/v$TARGET" 2>/dev/null # clear any stale/orphaned tag -git tag "v$TARGET" && git push origin "v$TARGET" # fires release.yml (tag-triggered; uses the file at this commit) +# A release tag is never moved or re-created: npm versions are immutable and the provenance names the tagged commit. +if out=$(npm view "@aztec-foundation/aztec-standards@$TARGET" version --prefer-online 2>&1); then + echo "STOP: $TARGET is already on npm ($out)"; exit 1 +elif ! grep -q E404 <<< "$out"; then + echo "STOP: npm lookup failed, so an earlier publish can't be ruled out"; exit 1 +fi +rc=0; git ls-remote --exit-code --tags origin "refs/tags/v$TARGET" > /dev/null || rc=$? # 2 = no such tag +[ "$rc" -eq 2 ] || { echo "STOP: v$TARGET already exists on origin, or origin is unreachable (exit $rc); see Gotchas"; exit 1; } +git tag --no-sign "v$TARGET" && git push origin "refs/tags/v$TARGET" # lightweight, like every release tag; fires release.yml ``` Every tag — rc included — parks on the `Production` environment waiting for a reviewer. Verify the run @@ -152,3 +159,8 @@ _smoke_ step alone (propagation lag) is not a failed release — confirm the pub - **Reruns cannot repair dist-tags.** npm OIDC authenticates `npm publish`, not `npm dist-tag add`. A rerun skips an already-published version only when its expected dist-tag is already correct; otherwise the workflow fails with instructions to repair the tag using an authorized npm account. +- **Never move a release tag.** A failed run is re-run, not re-tagged. The one exception is a tag pushed + to the wrong commit whose run never reached `Publish to NPM with OIDC` (e.g. the tag/`package.json` + check failed). Before deleting such a tag, confirm with `--prefer-online` that npm answers `E404` for the + version, that the run's publish step never started, and that no run for the tag is queued, waiting or in + progress; then, with the user's explicit OK, `git push origin :refs/tags/v` and tag again. From 31aa2b8b2195405f67a94b58bd805e4b98a0d35c Mon Sep 17 00:00:00 2001 From: Alejo Amiras Date: Wed, 30 Sep 2026 12:39:39 +0000 Subject: [PATCH 2/2] docs: require a GitHub-verified commit before tagging a release v6.0.0-rc.1 shows as Unverified on GitHub. Its tag is lightweight, so GitHub shows the tagged commit's signature, and that commit is unsigned: #41 was rebase-merged, and GitHub re-creates rebase-merged commits without a signature. Step 3 now records the commit to release as SHA and aborts unless GitHub reports a verified signature on it; Step 4 tags that exact SHA, so a later checkout can't swap in an unchecked commit. The rc flow signs its rehearse commit and pushes the branch so GitHub can verify it before the tag exists. A new gotcha says to squash-merge into main. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/release/SKILL.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 23e8bd2e..b753a50e 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -48,7 +48,8 @@ a silent no-op on the real package. Always start with `cd "$(git rev-parse --sho git checkout main && git pull git checkout -b rehearse/vX.Y.Z-rc.N npm version X.Y.Z-rc.N --no-git-tag-version # edits the tracked ROOT package.json - git commit -am "chore: rehearse X.Y.Z-rc.N" + git commit -S -am "chore: rehearse X.Y.Z-rc.N" # signed: Step 3 requires a Verified commit + git push -u origin rehearse/vX.Y.Z-rc.N # so GitHub can verify it before the tag exists ``` - **production:** `main` is already `X.Y.Z` — no bump, no branch; you'll tag `main` directly. @@ -65,18 +66,20 @@ case "$TARGET" in *) DIST="latest";; esac # mirrors release.yml ENVIRONMENT="Production" # every tag, rc included -echo "releasing v$TARGET → dist-tag '$DIST' via '$ENVIRONMENT'" +SHA=$(git rev-parse HEAD) # Step 4 tags exactly this commit +echo "releasing v$TARGET ($SHA) → dist-tag '$DIST' via '$ENVIRONMENT'" [ "$(node -p "require('./package.json').version")" = "$TARGET" ] || { echo "ABORT: root package.json != $TARGET (bump didn't land — wrong dir / edited export/?)"; exit 1; } git diff --quiet HEAD -- package.json || { echo "ABORT: bump uncommitted — the tag must point at the committed bump"; exit 1; } grep -q "runs-on: ubuntu-latest$" .github/workflows/release.yml || echo "WARN: release.yml runner may be wrong (rebase onto main?)" grep -q "^ environment: Production$" .github/workflows/release.yml || { echo "ABORT: release.yml does not pin environment: Production — the npm trusted publisher will reject the OIDC token"; exit 1; } +[ "$(gh api "repos/AztecProtocol/aztec-standards/commits/$SHA" --jq .commit.verification.verified)" = true ] || { echo "ABORT: GitHub shows no verified signature on $SHA (unsigned, rebase-merged, or not pushed); see Gotchas"; exit 1; } ``` ## Step 4 — tag & push ⚠️ **STOP — the tag push is the point of no return.** It fires `release.yml`, which publishes to npm, and npm versions are **immutable**. Before running the push, show the user the exact `TARGET`, `DIST`, -`ENVIRONMENT`, and target commit, and get explicit confirmation. Do **not** push on your own initiative — +`ENVIRONMENT`, and `SHA`, and get explicit confirmation. Do **not** push on your own initiative — even for an rc. (The local tag/guard steps are safe to run first; only the `git push origin` line is gated.) ```bash @@ -88,7 +91,7 @@ elif ! grep -q E404 <<< "$out"; then fi rc=0; git ls-remote --exit-code --tags origin "refs/tags/v$TARGET" > /dev/null || rc=$? # 2 = no such tag [ "$rc" -eq 2 ] || { echo "STOP: v$TARGET already exists on origin, or origin is unreachable (exit $rc); see Gotchas"; exit 1; } -git tag --no-sign "v$TARGET" && git push origin "refs/tags/v$TARGET" # lightweight, like every release tag; fires release.yml +git tag --no-sign "v$TARGET" "$SHA" && git push origin "refs/tags/v$TARGET" # lightweight: GitHub shows the commit's signature (Step 3); fires release.yml ``` Every tag — rc included — parks on the `Production` environment waiting for a reviewer. Verify the run @@ -164,3 +167,7 @@ _smoke_ step alone (propagation lag) is not a failed release — confirm the pub check failed). Before deleting such a tag, confirm with `--prefer-online` that npm answers `E404` for the version, that the run's publish step never started, and that no run for the tag is queued, waiting or in progress; then, with the user's explicit OK, `git push origin :refs/tags/v` and tag again. +- **Tag only a verified commit.** A lightweight tag has no signature of its own; GitHub shows the tagged + commit's, which Step 3 requires GitHub to have verified. Merge PRs into `main` with squash, which GitHub signs. + "Rebase and merge" re-creates each commit without a signature, which is how `v6.0.0-rc.1` ended up on an + unverified commit. A published release can't be fixed: re-signing changes the SHA the provenance names.