docs(release): make the two-PR flow, smoke, ledger and UI tag explicit (#2298)

The release skill described a v2 release as three steps and left four
things a release actually depends on implicit or absent: that it is two
pull requests against two different bases, the production smoke of the
release candidate, the ledger artifact the maintainers review, and where
a smoke finding gets fixed. It also gave only the `git tag` path, which
is the fallback rather than the norm.

Restructure it around PR 1 -> PR 2 -> Release:

- a "The shape" table contrasting the two PRs by branch, base, contents,
  verification and merge condition, plus why they must not be folded
  together and why PR 1 merges before PR 2 is opened;
- step 2 broken into the tree-hash check and production smoke, the
  ledger's structure, and the fix-on-`v2/main` rule that keeps the merge
  tree byte-identical to `origin/v2/main`;
- step 3 leading with the GitHub UI path, keeping the CLI commands as the
  by-hand equivalent.

Docs only. The `npm audit fix`, `--no-git-tag-version`, tag-`origin/main`,
no-`v`-prefix and #2010 warnings are all preserved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsmR1iQstcrEzJFmgZXGLi
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-08 22:45:52 -04:00
commit 440a8e24b2

View file

@ -1,6 +1,6 @@
---
name: release
description: Cut an Inspector v2 release — run npm audit and bump the version on v2/main first, merge the milestone into main, tag origin/main with a bare x.y.z, and publish via the GitHub Release. Also covers the v1 line and what the publish jobs gate on.
description: "Cut an Inspector v2 release — two PRs and then a GitHub Release. PR 1 puts the npm audit, any fixes it forces, and the version bump on v2/main; PR 2 merges v2/main into main and is smoke-tested from the production build with a ledger artifact for the maintainers; the maintainer then tags and publishes through the GitHub UI. Also covers the v1 line and what the publish jobs gate on."
disable-model-invocation: true
---
@ -24,14 +24,35 @@ job or the coverage gate red:
--access public --provenance`.
- **`publish-github-container-registry`** — the GHCR image.
## The shape: two PRs, then the Release
There is **one version number** (only the root `package.json` has one — the
clients carry none), so the flow is three steps.
clients carry none), and the release moves through **two pull requests** in
order. They are not interchangeable and neither one's content belongs on the
other.
## 1. `npm audit`, then bump, on `v2/main` — before the milestone merge
| | PR 1 — prep | PR 2 — the milestone merge |
| --- | --- | --- |
| Branch | `v2/chore/<ISSUE>-bump-<X-Y-Z>`, cut from `origin/v2/main` | the milestone-merge branch, cut from `origin/main` |
| Base | **`v2/main`** | **`main`** |
| Carries | the `npm audit` report, **any fixes the audit forces**, and the **version bump** — all three, one PR | the milestone's work, arriving whole from `v2/main`. **No commits of its own.** |
| Verified by | `npm run local:gate` | `npm run local:gate` **plus** a hand-driven smoke of every contribution in the milestone, from the **production build**, written up as a **ledger artifact** |
| Merged when | reviewed and green | the ledger is reviewed by the maintainers and clean |
Both are part of the milestone's work, so both belong on the develop branch and
flow into `main` together, in the same PR — audit first, so the bump sits on top
of a tree you have just checked.
Then, and only then, a maintainer tags and publishes the **GitHub Release**
(step 3), which is what triggers the publish jobs.
⚠️ **Do not fold the two together.** The bump must exist on `v2/main` before the
merge (see [Why the bump goes on `v2/main` first](#why-the-bump-goes-on-v2main-first-2010)),
and PR 2 must stay a pure merge — a commit authored on the merge branch is a
change that exists downstream of `v2/main` and nothing carries it back.
## 1. PR 1 — audit, audit fixes and the bump, on `v2/main`
All three are part of the milestone's work, so all three belong on the develop
branch and flow into `main` together, **in the same PR** — audit first, so the
bump sits on top of a tree you have just checked, and so a reviewer sees the
report and the fixes it forced as one change.
```sh
# Branch from the REMOTE ref, and read the version only once you are on it.
@ -92,7 +113,11 @@ and the tag would land on a `v2/main` commit — but the release must be cut fro
`main`, so the tag has to point at the merge commit there (step 3). Tagging here
creates a tag on a commit that is never released.
## 2. Merge `v2/main` → `main`
**PR 1 merges before PR 2 is opened.** The merge branch is cut from `main` and
takes `v2/main` whole, so opening it early means merging a `v2/main` that does
not yet carry the bump.
## 2. PR 2 — merge `v2/main` → `main`, smoke-test it, and write the ledger
Through the usual milestone-merge branch. It now carries the bump, so the
release lands on `main` with the version already correct.
@ -104,10 +129,78 @@ milestone merge lands they agree again, and `v2/main` is never left **behind**
`main`. If you see `v2/main` ahead of `main`, a release is in flight; if you see
it behind, something went wrong.
## 3. Tag `origin/main` and draft the Release
### 2a. Smoke-test the release candidate from the production build
Derive the tag from the version that just landed, rather than typing one — a
hard-coded tag is either already taken (so `git tag` aborts) or, worse, wrong:
The merge branch's tree **is** the release candidate. Check that rather than
assume it — the merge commit's tree and `origin/v2/main`'s must be identical:
```sh
git rev-parse origin/v2/main^{tree}
git rev-parse <merge-commit>^{tree} # must print the same hash
```
Then drive it. Work from a **dedicated worktree** with its own full
`npm install` (a symlinked `node_modules` passes lint and tests and then fails
every story file), run `npm run local:gate` there, and exercise the app from the
**production build** — the packaged bin and the built bundles, not `vite dev`.
The `local-dev`, `test-servers` and `pre-push-gate` skills cover the mechanics;
`pack:verify` inside the gate is what proves the tarball a consumer installs
actually resolves.
**Every contribution closed in the milestone gets driven, not read.** The bar is
observed behavior from the running app — a rendered panel, a status attribute, a
server's own stderr — against a real test server, through whichever clients the
change touches (web, CLI, TUI). "Its tests pass" is not evidence for this step;
the gate already said that. For a change with no observable surface, the
evidence is the thing that holds it — a probe that makes the guard fire, a
counted before/after, a resolved binary path.
### 2b. The ledger artifact
Write the results up as a **published artifact** for the maintainers to review,
and link it from PR 2. Shape it like the
[v2.5.0 ledger](https://claude.ai/code/artifact/6f25d292-3623-419f-af7f-26aba57247ef):
- **Masthead** — repo, PR number and merge commit, version, date; and a
standfirst saying what tree was tested and that its hash matches
`origin/v2/main`, plus whether the milestone payload is complete (the only
issue left open should be the merge itself).
- **Verdict band** — `local:gate` result, milestone issues verified as `N / N`,
distinct test count, regressions found.
- **The automated gate** — one cell per stage with its number (file counts,
test counts, smoke count, `pack:verify` size), and a note on what is new this
milestone.
- **One section per theme**, each a table of *Issue · What was driven ·
Observed · Status*. One row per closed issue, issue-linked, with the actual
output in the Observed cell.
- **Notes / findings** — anything that is a caveat rather than a pass, called
out rather than folded into a row.
A row that says "verified" without saying what was run is not a ledger entry.
### 2c. When the smoke finds something
**The fix goes on `v2/main`, never on the merge branch.** File the issue, fix it
through an ordinary PR against `v2/main`, then merge `v2/main` into the merge
branch again so the fix arrives the same way everything else did. That keeps the
merge tree byte-identical to `origin/v2/main` — which is both the invariant
checked in 2a and the reason a finding here does not create a commit that only
exists downstream (#2000 → #2092; #2215 → #2216–2224).
Re-run the affected part of the smoke afterwards and update the ledger; it is
the artifact the maintainers approve the merge on.
## 3. Tag and publish the Release
**Normally this is done by a maintainer through the GitHub UI**, after PR 2 has
merged: *Releases → Draft a new release → Choose a tag → type the bare `x.y.z`
→ Create new tag on publish*, with **Target: `main`**, then generate the notes
and publish. Publishing the Release is what fires the `publish` and
`publish-github-container-registry` jobs.
The equivalent by hand, for when the UI is not an option — derive the tag from
the version that just landed rather than typing one, since a hard-coded tag is
either already taken (so `git tag` aborts) or, worse, wrong:
```sh
git fetch origin main
@ -122,13 +215,15 @@ resolves through whatever merge-or-rebase strategy you have configured, so a
divergent local `main` can quietly produce or replay local commits. Tagging
`HEAD` there tags a commit that is not on `origin/main`, and `git push origin
<tag>` pushes only the tag — leaving a release whose commit was never published.
The UI path avoids this by construction: the target is `main` itself.
⚠️ **No `v` prefix.** This repo's release tags are bare `x.y.z` — which is why
the command above tags `$VERSION` and not `v$VERSION`. npm's own `tag-version-prefix` defaults to `v` and the repo
sets no `.npmrc`, so a bare `npm version` would have produced a mismatched tag;
tagging by hand is what keeps it right. (The workflow's assert step strips a
leading `v` before comparing, so a `v`-prefixed tag would still publish — it
would just be inconsistent with every previous release.)
the command above tags `$VERSION` and not `v$VERSION`, and why the tag typed
into the UI carries no prefix either. npm's own `tag-version-prefix` defaults to
`v` and the repo sets no `.npmrc`, so a bare `npm version` would have produced a
mismatched tag; tagging by hand is what keeps it right. (The workflow's assert
step strips a leading `v` before comparing, so a `v`-prefixed tag would still
publish — it would just be inconsistent with every previous release.)
The release's target commit selects which workflow runs, so this only publishes
when a release is cut from a commit carrying the v2 workflow.