From 50d93eb299432605d69a3386a2cfa0f47803832c Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Mon, 21 Sep 2026 22:28:15 -0700 Subject: [PATCH] docs: a release procedure, and the overreach the tag history corrects THE ROOT CAUSE WAS A MISSING DOCUMENT, NOT A MISSING CHECK. Nothing in this repo said "bump the version before you tag": release.yml is cargo-dist's autogenerated mechanism, README's Installation section describes the result, and plan.md Phase 3 carries one-time SETUP instructions from the launch plan. So the preflight added in the previous commits is the second half of the fix. RELEASING.md is the first, and it is a checklist rather than an explanation -- the reasoning stays in context/code/tooling.md section 4, because a checklist followed at 2am is not read for its argument and one carrying it does not get followed. Its own file rather than a README section because README Installation addresses someone INSTALLING and this addresses the one person PUBLISHING. THE DIAGNOSIS WAS UNDERSTATED AND IS NOW CORRECTED AT THE SOURCE. The earlier passage rested on a local `dist plan` run, which is the weaker half. v0.1.0-rc1 and v0.1.0-rc2 both carry COMPLETE published GitHub Releases from 2026-04-11 -- all four targets, .sha256 files, sha256.sum, dist-manifest.json, source.tar.gz and improvise-installer.sh. The workflow is not merely expected to work; it demonstrably ran end to end the last time a tag and the version agreed. Nearly the opposite was concluded from an empty `gh run list`: Actions logs age out at 90 days and those tags are five months old. An empty log is a statement about RETENTION, not about history -- the artefact a run published outlives the run, so ask the releases before asking the runs. AND THE MISTAKE IS RECURRENT, which is the real argument for writing a procedure down. `gh run list --workflow=release.yml --event=push` keeps the whole tag history: rc1 FAILED TWICE (14s each) before succeeding on the third push (3m25s), rc2 succeeded first time (3m9s), rc3 failed (21s). Three occurrences across two releases, all the same version/tag disagreement. A one-off justifies a note; a third justifies a document and a check before the irreversible step. The failure signature is recorded because it is cheap to read and easy to misread: `plan` fails in ~20s with the four jobs behind it skipped -- verified on the rc3 run, where plan is `failure` and build-local-artifacts, build-global-artifacts, host and announce are all `skipped` -- against ~3 minutes for a real build. The workflow ALSO triggers on pull_request, where it plans without a tag and passes in ~20s, so a green 20s run on a PR and a failed 20s run on a tag look alike at a glance and mean opposite things. Every claim above was read off `gh`, not inferred. Step 5 of RELEASING.md said "if plan succeeds and the four jobs skip" in a first draft; the run says plan FAILS, and it was corrected rather than kept as a plausible-sounding shape. No version bumped, no tag cut, no tag deleted, the rc1/rc2 releases untouched. Whether the stale rc tags are ever removed is left as the user's call and deliberately not made a step: it is not part of cutting a release, and the principle that covers it -- a published tag is not moved -- is stated. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH --- RELEASING.md | 70 +++++++++++++++++++++++++++++++++++++++++ context/code/tooling.md | 45 ++++++++++++++++++++++++-- 2 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 RELEASING.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..b577e047 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,70 @@ +# Releasing + +A checklist, not an explanation. The reasoning is `context/code/tooling.md` §4; +read it if a step surprises you, not before. + +Its own file rather than a `README.md` section because `README.md` §Installation +addresses someone *installing* improvise and this addresses the one person +*publishing* it — folding them together lengthens the user-facing document for +no user benefit. + +## The one rule + +**Bump the version and commit it BEFORE you tag.** `dist` matches the tag +against the root package's version and refuses when they disagree — and by the +time CI tells you, the tag is public on three remotes and a published tag is not +moved. Every step below exists to put that check before the irreversible one. + +## Cutting a release + +1. **Bump the version.** `Cargo.toml`'s `[package]` version is the one `dist` + announces (`dist-workspace.toml` says `members = ["cargo:."]`). Keep the + crates under `crates/` in step with it; `gp-tree` sits at its own version + deliberately, and the preflight lists such crates as a note rather than an + error. + +2. **Commit the bump**, and let `Cargo.lock` be part of that commit. + +3. **Run the preflight**, with the tag you intend to push: + + ```sh + scripts/release-preflight.sh vX.Y.Z + ``` + + It refuses a dirty tree, refuses a tag that does not match the version, runs + `dist plan` as a second opinion when `dist` is on `PATH`, and prints the tag + commands only once it has passed. It cuts nothing itself. + +4. **Tag and push**, using the commands it printed: + + ```sh + git tag -a vX.Y.Z -m "vX.Y.Z" + git push gh vX.Y.Z + ``` + + Push the tag to **`gh`** — `.github/workflows/release.yml` triggers on a tag + push there, matching `**[0-9]+.[0-9]+.[0-9]+*`. `origin` and `gitea` are + mirrors and build nothing. + +5. **Watch the run.** A green release takes about three minutes and publishes + tarballs for all four targets with their `.sha256` files, `sha256.sum`, + `dist-manifest.json`, `source.tar.gz` and `improvise-installer.sh`. + + The failure signature is unmistakable once you know it: **`plan` fails in + about twenty seconds and the four jobs behind it skip.** That is the tag not + matching the version — see below. A run that gets past `plan` is building. + +## When a tag has already been pushed against the wrong version + +**Leave it dead and cut the next one.** Do not move or re-push the tag: all +three remotes carry it, and a published tag that changes meaning is worse than +one that never built. Bump to the next version, commit, run the preflight, and +tag that. + +`v0.1.0-rc3` is the standing example — pushed while every crate still said +`0.1.0-rc2`, so `dist` refused and the four jobs behind `plan` skipped. It has +no GitHub release and is left as it is. + +A tag with no release is not a tag to clean up on the way to cutting one; +whether the stale `rc` tags are ever removed is a separate decision, and not a +step here. diff --git a/context/code/tooling.md b/context/code/tooling.md index 4c33e355..2fa68829 100644 --- a/context/code/tooling.md +++ b/context/code/tooling.md @@ -329,7 +329,17 @@ artifact is gone rather than transiently unlistable, so a retry cannot find it. ### 4. The release preflight, and why a loud failure was not enough `scripts/release-preflight.sh` answers, before a tag exists, the question the -Release workflow answers after. +Release workflow answers after. **The procedure is `RELEASING.md`**; this +section is the argument behind it, and the split is deliberate — a checklist +followed at 2am is not read for its reasoning, and one that carries it does not +get followed. + +**The root cause was a missing document, not a missing check.** Nothing in this +repo said *bump the version before you tag* — `release.yml` is cargo-dist's +autogenerated mechanism, `README.md` §Installation describes the result, and +`context/plan.md` Phase 3 carries one-time *setup* instructions from the launch +plan. So the preflight is the second half of the fix; `RELEASING.md` is the +first. **What happened.** `v0.1.0-rc3` was pushed to all three remotes while every crate still said `0.1.0-rc2`. dist matches a tag against the root package's @@ -338,8 +348,37 @@ Release!"*, and the four jobs behind `plan` skipped. **Nothing was wrong with the workflow**, which is worth stating plainly because the instinct on a failed release run is to edit the YAML. The trigger is correct, the pattern matches, the workflow is enabled; `dist plan --tag=v0.1.0-rc2` succeeds today and -`--tag=v0.1.0-rc3` reproduces the CI error exactly, which is the whole -diagnosis and both halves of it. +`--tag=v0.1.0-rc3` reproduces the CI error exactly. + +**The decisive evidence is a shipped release, not that local run**, and an +earlier draft of this passage rested on the weaker half. `v0.1.0-rc1` and +`v0.1.0-rc2` both carry **complete published GitHub Releases** from +2026-04-11 — tarballs for all four targets, their `.sha256` files, +`sha256.sum`, `dist-manifest.json`, `source.tar.gz` and +`improvise-installer.sh` (`gh release view v0.1.0-rc2 --json assets`). So the +workflow is not merely *expected* to work: it **demonstrably ran end to end** +the last time a tag and the version agreed. Note how nearly the opposite was +concluded — `gh run list` came back empty and was read as "only one tag push +ever ran this", when Actions logs simply age out at 90 days and those tags are +five months old. **An empty log is a statement about retention, not about +history**; the artefact a run *published* outlives the run, so ask the releases +before asking the runs. "The four jobs behind `plan` skipped" is true of the +rc3 run alone and must not be read as "no release has ever been built". + +**And the mistake is recurrent, which is the real argument for a written +procedure.** `gh run list --workflow=release.yml --event=push` keeps the whole +tag history, and it reads: rc1 **failed twice** (14s each) before succeeding on +the third push (3m25s), rc2 succeeded first time (3m9s), rc3 failed (21s). So +this same version/tag disagreement has been hit **three times across two +releases** by the person who set the workflow up. A one-off justifies a note; a +third occurrence justifies `RELEASING.md` and a check that runs before the +irreversible step. The signature is worth memorising because it is so cheap to +read: **`plan` fails in ~20s with four jobs skipped** (verified on the rc3 run: +`plan: failure`, `build-local-artifacts`, `build-global-artifacts`, `host` and +`announce` all `skipped`), where a real build takes about three minutes. Note +the workflow also triggers on `pull_request`, where it plans without a tag and +passes in ~20s — so a *green* 20s Release run on a PR and a *failed* 20s one on +a tag look alike at a glance and mean opposite things. **Why a loud, precise failure still cost something.** dist named the right answer in its own error text — *"`--tag=v0.1.0-rc2` will Announce: improvise"*.