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"*.