Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,28 @@ description: >-

# Release (rc prerelease or production)

One flow for both. The tag-driven `release.yml` (`on: push tags v*`) derives the dist-tag from the
version: `-rc.N` → `rc`, else `latest`. The `rc` is a **rehearsal** for the immutable production
publish — always do it first. Assumes the version bump is already merged to `main` (see the
`bump-aztec-version` skill).
One flow for both. The tag-driven `release.yml` (`on: push tags v*`) derives both the npm dist-tag and
GitHub environment from the version: a prerelease such as `-rc.N` uses dist-tag `rc` and the
`Development` environment; a stable version uses `latest` and the reviewer-gated `Production`
environment. The `rc` is a **rehearsal** for the immutable production publish — always do it first.
Assumes the version bump is already merged to `main` (see the `bump-aztec-version` skill).

## Preconditions (state them; don't assume)
- Fork workflows enabled; `NPM_TOKEN` in the `Production` environment with publish+**create** rights
for the package scope; workflows on an available runner (standard `ubuntu-latest`); `v*` tag-push rights.

- Fork workflows enabled; `NPM_TOKEN` with publish+**create** rights for the package scope in both
`Development` (prereleases/canaries) and `Production` (stable releases); workflows on an available
runner (standard `ubuntu-latest`); `v*` tag-push rights.

## Step 1 — pick mode + version (ALWAYS ASK — never infer)

**Always ask the user which release to cut** (use `AskUserQuestion`); never assume the mode from prior
context or conversation. This is a hard-to-reverse publish — the user chooses rc vs production every time.

- **rc rehearsal:** `X.Y.Z-rc.N` (do this first). Next `N` if a prior rc tag exists (`git ls-remote --tags origin`).
- **production:** `X.Y.Z` (only after a green rc).

## Step 2 — stamp the version (⚠️ from the repo ROOT, never `export/`)

`export/…` is a gitignored build artifact with its own trimmed `package.json`; `npm version` there is
a silent no-op on the real package. Always start with `cd "$(git rev-parse --show-toplevel)"`.

Expand All @@ -40,52 +46,66 @@ a silent no-op on the real package. Always start with `cd "$(git rev-parse --sho
- **production:** `main` is already `X.Y.Z` — no bump, no branch; you'll tag `main` directly.

## Step 3 — MANDATORY pre-tag guard (both modes)
Locally reproduce `release.yml`'s `tag == package.json` check *before* pushing the tag (this is the

Locally reproduce `release.yml`'s `tag == package.json` check _before_ pushing the tag (this is the
assertion that fails the run on a mismatch):

```bash
cd "$(git rev-parse --show-toplevel)"
TARGET="X.Y.Z-rc.N" # the version you're releasing: X.Y.Z (production) or X.Y.Z-rc.N (rc)
case "$TARGET" in *-*) DIST="${TARGET#*-}"; DIST="${DIST%%.*}";; *) DIST="latest";; esac # rc/beta/... | latest — mirrors release.yml
echo "releasing v$TARGET → dist-tag '$DIST'"
case "$TARGET" in
*-*) DIST="${TARGET#*-}"; DIST="${DIST%%.*}"; ENVIRONMENT="Development";;
*) DIST="latest"; ENVIRONMENT="Production";;
esac # mirrors release.yml
echo "releasing v$TARGET → 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?)"
```

## 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`,
and target commit, 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.)
`ENVIRONMENT`, and target commit, 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
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)
```
Approve the `Production` environment run if reviewers are set.

Before allowing the run to publish, verify its environment: prerelease tags must show `Development`;
stable tags must show `Production`. Stop immediately if an rc/beta/canary tag requests `Production`.
Approve the stable `Production` run if reviewers are set; prereleases should not need that approval.

## Step 5 — verify, THEN report

Watch (`gh run watch`). The publish step succeeding (`+ pkg@ver`, provenance signed) is the source of
truth — the publish is immutable. Then verify the artifact (use `--prefer-online` to dodge cache/lag):

```bash
npm view @aztec-foundation/aztec-standards@"$TARGET" version --prefer-online # exists
npm view @aztec-foundation/aztec-standards dist-tags --prefer-online # $DIST -> $TARGET (see latest gotcha)
cd "$(mktemp -d)" && npm init -y >/dev/null && npm i --prefer-online @aztec-foundation/aztec-standards@"$DIST"
```

For production `DIST=latest`, so `@latest` is what a plain `npm i @aztec-foundation/aztec-standards` resolves to.
Report success once the publish step is green (+ provenance) and the version resolves. A red
*smoke* step alone (propagation lag) is not a failed release — confirm the publish step + `npm view`.
_smoke_ step alone (propagation lag) is not a failed release — confirm the publish step + `npm view`.

**Order across releases:** rc first (`DIST=rc`, rehearsal), then production (`DIST=latest`) once it's green.

---

## Gotchas (release-time — check these first)

- **Run from the repo ROOT, never `export/`.** That dir is a gitignored build artifact with its own
trimmed `package.json`; `npm version` there is a no-op (`npm error Version not changed`) and leaves the
tag mismatched against the real version → `release.yml` validation fails. Step 3's guard catches it.
- **Registry propagation lag (esp. first publish).** A just-published version — particularly a package's
first-ever publish — can take minutes to resolve; an immediate `npm install` 404s. Don't fail the
release on a smoke miss (warn), and use `--prefer-online` to bypass the negative cache. Your *local*
release on a smoke miss (warn), and use `--prefer-online` to bypass the negative cache. Your _local_
npm may also cache the 404 — re-check with `npm view … --prefer-online`.
- **The first publish claims `latest`.** npm sets `latest` on a package's first-ever publish regardless of
`--tag`, so an `rc` rehearsal on a brand-new package leaves `latest` pointing at the rc. It self-corrects
Expand All @@ -94,6 +114,10 @@ Report success once the publish step is green (+ provenance) and the version res
- **Runners + fork state.** A GitHub fork has workflows disabled by default (enable in the Actions tab).
Target `ubuntu-latest`; don't assume custom/larger-runner labels (e.g. `ubuntu-latest-m`) exist — jobs
stuck `queued` are the symptom.
- **Environment separation.** Any version containing a prerelease suffix (`-rc.N`, `-beta.N`, etc.) is
a canary and must use `Development`; only a stable `X.Y.Z` tag may use `Production`. Check the selected
environment on the Actions deployment before publish. A prerelease waiting for Production approval is
a routing bug — do not approve it.
- **Pin CI to real tags.** Reusable-workflow refs (aztec-ci-actions / aztec-benchmark) should be pinned to a
released tag SHA (with a `# vX` comment), not a transient `main` HEAD — a pre-re-scope commit can still
`require('@defi-wonderland/…')`.
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Production Release
name: Release

# Tag-driven: pushing a `v*` tag (e.g. v5.0.0) publishes that version.
on:
Expand All @@ -12,11 +12,13 @@ concurrency:

jobs:
# ══════════════════════════════════════════════════════════════════════════════
# PRODUCTION RELEASE JOB
# TAGGED RELEASE JOB
# ══════════════════════════════════════════════════════════════════════════════
release:
name: Release
environment: Production
# Prerelease tags are canary releases and must not consume Production credentials or approvals.
# Stable tags use the reviewer-gated Production environment.
environment: ${{ contains(github.ref_name, '-') && 'Development' || 'Production' }}
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
Expand Down
Loading