Terminal mockups for scripts/release.sh — Tier B of
docs/design-release-machine.md, drawn so the option stays open at full
information. Tier A ships first and changes none of these screens except adding
verify.
Conformance is partial and deliberate. Glyph vocabulary, phases,
· · · leaders and 92-col width follow desktop/scripts/REPORT-STYLE.md.
Three departures the existing renderer cannot produce, which is why §12 item 5 carries a
renderer cost the first draft did not budget: detail sits inline in the right cluster
rather than on the indent line (rule 5), so a 14-step release reads as 14 lines rather than
28 — which in turn makes rule 8's single right edge impossible; no header/footer panels;
and no separate 1…N display number, the [tag] carrying both. Amend
REPORT-STYLE.md for list-shaped callers, or conform — but not silently.
Always first, always free. Note row 4: gate freshness (F5) — the check that would have caught 0.27.0's build failure 1 before eleven minutes of build, by noticing a gate hadn't run since the commits it guards.
$ ./scripts/release.sh plan 0.28.0 Release 0.28.0 · Tier 1 · minor from v0.27.0 · 23 commits · bristlenose/ frontend/ desktop/ touched PRE-FLIGHT 4 steps ✓ preflight · · · · · · · · · · · · check-release-ready.sh 0.28.0 [1] ✓ prose present · · · · · · · · · · · · CHANGELOG + README carry 0.28.0 [2] ⚠ gate freshness · · · · · · · · · · · · 1 stale [3] ⚠ check-window-surfaces last green at 4d462e6e · 6 commits behind HEAD a gate is only as fresh as the last time something ran it ✓ skip-worktree · · · · · · · · · · · · 2 tracked, both match HEAD [4] PUBLISH · reversible 3 steps — bump + commit + tag · · · · · · · · · · bump-version.py minor [5] — push main + tag · · · · · · · · · · two pushes, never --tags [6] — mac artefacts · · · · · · · · · · build-all · build-dmg ~65 min [7] GATE — ci green · · · · · · · · · · both runs · gate:ci [8] PUBLISH · irreversible 5 steps — upload testflight · · · · · · · · · · soft — spends build 2857 [9] — upload dmg · · · · · · · · · · permalink swaps [10] — approve publish · · · · · · · · · · HARD — gate:approval [11] — website deploy · · · · · · · · · · manual · after PyPI 200 [12] — snap edge · · · · · · · · · · gh workflow run snap.yml [13] estimate ~1h55 pipeline · excludes human decision time events .release/0.28.0/events.jsonl ⚠ 1 warning. Run anyway with run 0.28.0, or clear it first: desktop/scripts/check-window-surfaces.sh
Each step prints the command before running it — the conductor rule, applied to the driver. The run stops at the hard gate and exits 0: a release waiting on a person is behaving correctly, not failing (F6).
$ ./scripts/release.sh run 0.28.0 PRE-FLIGHT ✓ preflight · · · · · · · · · · · · 31 checks, 0 fail [1] 52s ✓ gate freshness · · · · · · · · · · · · all green at HEAD [3] 2s PUBLISH · reversible $ ./scripts/bump-version.py minor ✓ bump + commit + tag · · · · · · · · · · v0.28.0 == HEAD [5] 4s $ git push origin main && git push origin v0.28.0 ✓ push main + tag · · · · · · · · · · 2 runs in flight [6] 11s $ SIGN_IDENTITY="$SIGN_IDENTITY" desktop/scripts/build-all.sh builds the sidecar, archives, exports — cold caches after a bump ✓ build-all · · · · · · · · · · 4 phases, 15 gates [7a] 11m02s ✓ build-dmg · · · · · · · · · · notarised, stapled [7b] 28m40s art dmg = Bristlenose-0.28.0.dmg · 651 MB · expires 22 Sep GATE ✓ ci green · · · · · · · · · · CI 37m · Release 39m [8] 2m18s PUBLISH · irreversible $ desktop/scripts/upload-testflight.sh spends build 2857 · not reusable ✓ upload testflight · · · · · · · · · · build 2857 accepted [9] 5m41s art delivery-uuid = 8f3c1a04-… · recorded, no longer hand-captured ✓ upload dmg · · · · · · · · · · permalink swapped [10] 12m22s ℹ HELD · approve publish [11] gate:approval · nothing publishes until a human approves → https://github.com/cassiocassio/bristlenose/actions/runs/4471002 Review deployments ▸ pypi ▸ Approve and deploy ← ticking the box is not pressing it expires 22 Sep 2026 resume with ./scripts/release.sh run 0.28.0 8 of 13 steps done · nothing unrecoverable has happened $ echo $? 0
The 0.27.0 failure made structurally impossible. The driver redirects — never
pipes — so $? is the build's own status, and the tail is printed through
tr '\r' '\n' so a rsync --progress-style carriage-return log doesn't
render as one frozen line (tricky things #1 and #2, fixed in one place).
$ ./scripts/release.sh run 0.28.0 … 4 steps ok, skipped … $ SIGN_IDENTITY="$SIGN_IDENTITY" desktop/scripts/build-all.sh ✗ build-all · · · · · · · · · · exit 1 at step 1/11 [7a] 26s PRE-FLIGHT · window surfaces ✗ assertion unsatisfiable: no line matches 'handshakeProjectPath' ServeFleet.swift renamed it plural in 037b371e (20 Aug) ✗ Build failed .release/0.28.0/logs/build-all.1.log · last 20 lines above ✗ Stopped at build-all. Nothing irreversible has happened. the failing check, on its own (<1s): desktop/scripts/check-window-surfaces.sh the whole step, once it passes: SIGN_IDENTITY="$SIGN_IDENTITY" desktop/scripts/build-all.sh or fix and resume: ./scripts/release.sh run 0.28.0 picks up at build-all 4 of 14 steps done · tag v0.28.0 exists on origin and publishes nothing to abandon: git push --delete origin v0.28.0 && git tag -d v0.28.0 $ echo $? 1
Driver killed mid-upload — laptop slept, Ctrl-C, a self-imposed
timeout. The fold sees a running with no terminus and reports
unknown. It is never auto-retried: a half-finished upload is exactly where
guessing is worst. Note the driver reads the script's own resume design rather than
assuming the worst.
$ ./scripts/release.sh run 0.28.0 ⚠ Previous attempt did not finish cleanly. ⚠ upload-dmg · · · · · · · · · · unknown — started 21:04, no terminus last event @bn bar parent=upload-dmg:3 done=118 total=651 (18%) probe HEAD https://bristlenose.app/dmg/… → 302 → 0.27.0 the permalink still serves the OLD version — the swap is atomic and had not happened, so this transfer was genuinely incomplete on disk .upload-Bristlenose-0.28.0.dmg.part · 118 MB · rsync --partial resumes This step is not auto-retried. Choose: ./scripts/release.sh retry upload-dmg resumes the partial transfer ./scripts/release.sh run 0.28.0 --skip upload-dmg ship the rest, dmg later everything before it is recorded ok · 10 of 14 steps done $ echo $? 1
The case D1 exists for. The log says we uploaded; the channel says otherwise.
The probe wins, the disagreement is appended as an event rather than silently
resolved, and the run refuses to report complete. This is also what caught 0.27.0's
tricky thing #6 — a gate believed cleared that pending_deployments said was
still pending.
$ ./scripts/release.sh verify 0.28.0 Channels · 0.28.0 ✓ PyPI · · · · · · · · 200 · /pypi/bristlenose/0.28.0/json ✓ GitHub Release · · · · · · · · v0.28.0 · 2 assets ✓ Homebrew · · · · · · · · bristlenose-0.28.0.tar.gz ✓ TestFlight · · · · · · · · build 2857 · Ready to Test ✓ .dmg · · · · · · · · 302 → Bristlenose-0.28.0.dmg ✗ Snap · · · · · · · · edge = 0.27.0 rev 9 log step snap-edge status=ok at 22:41 (workflow conclusion: success) probe api.snapcraft.io channel-map → edge 0.27.0 the workflow uploaded; the store has not released it → gh workflow run snap.yml --ref v0.28.0 ✗ Website · · · · · · · · changelog names 0.27.0 → cd ../bristlenose-site && ./build.py && ./deploy.sh expiry .dmg 22 Sep (30d from build) · TestFlight 21 Nov (90d from upload) ✗ 5 of 7 channels on 0.28.0 · 2 outstanding $ echo $? 1
What you type mid-release to answer "where am I". Folded from the log, costs
nothing. Pipeline time sums the measured steps and shows the remainder rather than
deriving itself as wall − held, which would reconcile by construction — the same
tautology as attempted == succeeded + failed. The unaccounted row is the only one
of the four that can ever catch anything. The 21:00 weekday nudge is absent here by design:
it prints only when it would change the answer (weekday, before 21:00), and on the 0.27.0
evidence base a release reaches this gate after 21:00 by its own duration.
$ ./scripts/release.sh 0.28.0 · Tier 1 · held · started 19:41 · 3h07 ago · 1 of 2 attempts in .release/ ✓ preflight ✓ prose ✓ gate-freshness ✓ skip-worktree ✓ bump-version ✓ push ✓ build-all ✓ build-dmg ✓ ci-green ✓ upload-testflight ✓ upload-dmg ℹ approve publish held 1h48 · expires 22 Sep — website-deploy — snap-edge 11 done · 1 held · 2 pending = 14 pipeline time 1h01 measured · sum of 11 steps waiting on you 1h48 1 held interval unaccounted 18m between steps — driver idle, or a gap in the log wall clock 3h07 → approve at github.com/…/runs/4471002 then release.sh run 0.28.0
Six event kinds, all of them report.sh's existing vocabulary. The
sink adds run, seq, attempt and exit;
everything else passes through. State is folded from this on every read, never
stored — so derived state cannot drift from the record, because it is the record.
.release/0.28.0/events.jsonl
{"ts":"2026-08-23T19:41:02Z","run":"0.28.0","seq":1,"kind":"meta","tier":1,"bump":"minor"}
{"ts":"…19:41:04Z","run":"0.28.0","seq":2,"kind":"step","step":"preflight","attempt":1,"status":"start"}
{"ts":"…19:41:56Z","run":"0.28.0","seq":3,"kind":"step","step":"preflight","attempt":1,
"status":"ok","exit":0,"elapsed":52,"detail":"31 checks, 0 fail"}
{"ts":"…19:43:11Z","run":"0.28.0","seq":8,"kind":"step","step":"build-all","attempt":1,
"status":"fail","exit":1,"log":".release/0.28.0/logs/build-all.1.log",
"detail":"window surfaces: assertion unsatisfiable"}
{"ts":"…20:58:40Z","run":"0.28.0","seq":22,"kind":"art","key":"delivery-uuid",
"value":"8f3c1a04-2b77-4e19-9c31-6ad0f1e2c845"}
{"ts":"…21:02:14Z","run":"0.28.0","seq":24,"kind":"gate","step":"approve-publish",
"result":"held","evidence":"pending_deployments: pypi"}
{"ts":"…22:50:31Z","run":"0.28.0","seq":31,"kind":"check","step":"snap-edge",
"result":"disagree","evidence":"log=ok probe=edge:0.27.0"}
{"ts":"…22:51:02Z","run":"0.28.0","seq":32,"kind":"done","status":"partial"}
gitignored — machinery, not history. docs/release-log.md stays the committed human record, and now gets its numbers from here rather than from recall.