|
| 1 | +--- |
| 2 | +name: release |
| 3 | +description: >- |
| 4 | + Publish this repo to npm via the tag-driven release.yml — both rc prereleases |
| 5 | + (X.Y.Z-rc.N → `rc` dist-tag) and production releases (X.Y.Z → `latest`). Stamps and |
| 6 | + validates the version, pushes a `v*` tag, and verifies the publish (dist-tag, provenance, |
| 7 | + smoke install). Always rehearse with an rc before a production tag. Use to cut an rc or a |
| 8 | + final release. |
| 9 | +--- |
| 10 | + |
| 11 | +# Release (rc prerelease or production) |
| 12 | + |
| 13 | +One flow for both. The tag-driven `release.yml` (`on: push tags v*`) derives the dist-tag from the |
| 14 | +version: `-rc.N` → `rc`, else `latest`. The `rc` is a **rehearsal** for the immutable production |
| 15 | +publish — always do it first. Assumes the version bump is already merged to `main` (see the |
| 16 | +`bump-aztec-version` skill). |
| 17 | + |
| 18 | +## Preconditions (state them; don't assume) |
| 19 | +- Fork workflows enabled; `NPM_TOKEN` in the `Production` environment with publish+**create** rights |
| 20 | + for the package scope; workflows on an available runner (standard `ubuntu-latest`); `v*` tag-push rights. |
| 21 | + |
| 22 | +## Step 1 — pick mode + version (ALWAYS ASK — never infer) |
| 23 | +**Always ask the user which release to cut** (use `AskUserQuestion`); never assume the mode from prior |
| 24 | +context or conversation. This is a hard-to-reverse publish — the user chooses rc vs production every time. |
| 25 | +- **rc rehearsal:** `X.Y.Z-rc.N` (do this first). Next `N` if a prior rc tag exists (`git ls-remote --tags origin`). |
| 26 | +- **production:** `X.Y.Z` (only after a green rc). |
| 27 | + |
| 28 | +## Step 2 — stamp the version (⚠️ from the repo ROOT, never `export/`) |
| 29 | +`export/…` is a gitignored build artifact with its own trimmed `package.json`; `npm version` there is |
| 30 | +a silent no-op on the real package. Always start with `cd "$(git rev-parse --show-toplevel)"`. |
| 31 | + |
| 32 | +- **rc:** throwaway branch off `main` carrying the bump (keeps `main` at `X.Y.Z`): |
| 33 | + ```bash |
| 34 | + cd "$(git rev-parse --show-toplevel)" |
| 35 | + git checkout main && git pull |
| 36 | + git checkout -b rehearse/vX.Y.Z-rc.N |
| 37 | + npm version X.Y.Z-rc.N --no-git-tag-version # edits the tracked ROOT package.json |
| 38 | + git commit -am "chore: rehearse X.Y.Z-rc.N" |
| 39 | + ``` |
| 40 | +- **production:** `main` is already `X.Y.Z` — no bump, no branch; you'll tag `main` directly. |
| 41 | + |
| 42 | +## Step 3 — MANDATORY pre-tag guard (both modes) |
| 43 | +Locally reproduce `release.yml`'s `tag == package.json` check *before* pushing the tag (this is the |
| 44 | +assertion that fails the run on a mismatch): |
| 45 | +```bash |
| 46 | +cd "$(git rev-parse --show-toplevel)" |
| 47 | +TARGET="X.Y.Z-rc.N" # the version you're releasing: X.Y.Z (production) or X.Y.Z-rc.N (rc) |
| 48 | +case "$TARGET" in *-*) DIST="${TARGET#*-}"; DIST="${DIST%%.*}";; *) DIST="latest";; esac # rc/beta/... | latest — mirrors release.yml |
| 49 | +echo "releasing v$TARGET → dist-tag '$DIST'" |
| 50 | +[ "$(node -p "require('./package.json').version")" = "$TARGET" ] || { echo "ABORT: root package.json != $TARGET (bump didn't land — wrong dir / edited export/?)"; exit 1; } |
| 51 | +git diff --quiet HEAD -- package.json || { echo "ABORT: bump uncommitted — the tag must point at the committed bump"; exit 1; } |
| 52 | +grep -q "runs-on: ubuntu-latest$" .github/workflows/release.yml || echo "WARN: release.yml runner may be wrong (rebase onto main?)" |
| 53 | +``` |
| 54 | + |
| 55 | +## Step 4 — tag & push |
| 56 | +⚠️ **STOP — the tag push is the point of no return.** It fires `release.yml`, which publishes to npm, |
| 57 | +and npm versions are **immutable**. Before running the push, show the user the exact `TARGET`, `DIST`, |
| 58 | +and target commit, and get explicit confirmation. Do **not** push on your own initiative — even for an |
| 59 | +rc. (The local tag/guard steps are safe to run first; only the `git push origin` line is gated.) |
| 60 | +```bash |
| 61 | +git tag -d "v$TARGET" 2>/dev/null; git push origin ":refs/tags/v$TARGET" 2>/dev/null # clear any stale/orphaned tag |
| 62 | +git tag "v$TARGET" && git push origin "v$TARGET" # fires release.yml (tag-triggered; uses the file at this commit) |
| 63 | +``` |
| 64 | +Approve the `Production` environment run if reviewers are set. |
| 65 | + |
| 66 | +## Step 5 — verify, THEN report |
| 67 | +Watch (`gh run watch`). The publish step succeeding (`+ pkg@ver`, provenance signed) is the source of |
| 68 | +truth — the publish is immutable. Then verify the artifact (use `--prefer-online` to dodge cache/lag): |
| 69 | +```bash |
| 70 | +npm view @aztec-foundation/aztec-standards@"$TARGET" version --prefer-online # exists |
| 71 | +npm view @aztec-foundation/aztec-standards dist-tags --prefer-online # $DIST -> $TARGET (see latest gotcha) |
| 72 | +cd "$(mktemp -d)" && npm init -y >/dev/null && npm i --prefer-online @aztec-foundation/aztec-standards@"$DIST" |
| 73 | +``` |
| 74 | +For production `DIST=latest`, so `@latest` is what a plain `npm i @aztec-foundation/aztec-standards` resolves to. |
| 75 | +Report success once the publish step is green (+ provenance) and the version resolves. A red |
| 76 | +*smoke* step alone (propagation lag) is not a failed release — confirm the publish step + `npm view`. |
| 77 | + |
| 78 | +**Order across releases:** rc first (`DIST=rc`, rehearsal), then production (`DIST=latest`) once it's green. |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## Gotchas (release-time — check these first) |
| 83 | +- **Run from the repo ROOT, never `export/`.** That dir is a gitignored build artifact with its own |
| 84 | + trimmed `package.json`; `npm version` there is a no-op (`npm error Version not changed`) and leaves the |
| 85 | + tag mismatched against the real version → `release.yml` validation fails. Step 3's guard catches it. |
| 86 | +- **Registry propagation lag (esp. first publish).** A just-published version — particularly a package's |
| 87 | + first-ever publish — can take minutes to resolve; an immediate `npm install` 404s. Don't fail the |
| 88 | + release on a smoke miss (warn), and use `--prefer-online` to bypass the negative cache. Your *local* |
| 89 | + npm may also cache the 404 — re-check with `npm view … --prefer-online`. |
| 90 | +- **The first publish claims `latest`.** npm sets `latest` on a package's first-ever publish regardless of |
| 91 | + `--tag`, so an `rc` rehearsal on a brand-new package leaves `latest` pointing at the rc. It self-corrects |
| 92 | + when the stable `X.Y.Z` publishes; you can't `dist-tag rm latest` (a package must have one). Don't |
| 93 | + announce the package until the stable is out. |
| 94 | +- **Runners + fork state.** A GitHub fork has workflows disabled by default (enable in the Actions tab). |
| 95 | + Target `ubuntu-latest`; don't assume custom/larger-runner labels (e.g. `ubuntu-latest-m`) exist — jobs |
| 96 | + stuck `queued` are the symptom. |
| 97 | +- **Pin CI to real tags.** Reusable-workflow refs (aztec-ci-actions / aztec-benchmark) should be pinned to a |
| 98 | + released tag SHA (with a `# vX` comment), not a transient `main` HEAD — a pre-re-scope commit can still |
| 99 | + `require('@defi-wonderland/…')`. |
| 100 | +- **npm is immutable.** Always rehearse with an `rc` before the final tag. `release.yml` publishes |
| 101 | + idempotently (re-running a tag re-points the dist-tag instead of erroring) and smoke-tests, so a failed |
| 102 | + run is safe to re-trigger. |
| 103 | +- **Provenance.** `release.yml` publishes with `--provenance` (needs `id-token: write` + `repository.url` |
| 104 | + matching the running repo). The `rc` run is the first to exercise it; a Sigstore transparency-log line |
| 105 | + in the publish output confirms it worked. |
0 commit comments