CONTRIBUTING.md has said since #96 that the release PR is where the full real-hardware drill hangs, recorded in drill/RUNS.md. No release has ever done it: #95, #114 and #148 all shipped as a VERSION bump plus a CHANGELOG.md stamp, and RUNS.md carries no '## Release drill' section at all. A reviewer bot finally blocked on it — which is the point. The one time it was caught is the one time somebody happened to look, and that is not a gate. So the rule moves out of the document and into CI. .github/scripts/drill-recorded.sh, keyed on VERSION the same way changelog-armed.sh is: a -dev tree passes with nothing to assert (which is what keeps the guard installable — a version that fired on every PR would be switched off inside a day), and a bare VERSION must carry a section headed '## Release drill — <version>', optional ' — <date>' tail, with prose under it. The version is compared as a whole awk field, never as a substring, so 0.9.0 cannot be satisfied by a 0.9.0-rc1 drill or vice versa — release-notes.sh's trap, solved the same way so the two cannot disagree about what "the section for X" means. What it asserts is a RECORD, deliberately, not a passing drill: CI cannot run the drill (real hardware, the better part of an hour — ci.yml says as much about the rehearsal job it runs instead). That also keeps the maintainer waiver honest — a release that must ship undrilled writes that under the same heading, so the skip is a reviewable line in the diff rather than silence. Wired into ci.yml as its own step, NOT pull-request-only, for the reasoning #143 applied to the monotonic guard: the merge that publishes a release is a push to main carrying the same bare VERSION, so a PR-only check would leave the tree that actually ships unasserted. test/release.sh grows 27 cases (134 -> 161). Every fixture carries its own VERSION and its own RUNS.md — reaching for $ROOT/VERSION is the coupling #146 had to fix, and it goes red on the ceremony tree, the one tree where the release suite most needs to be trustworthy. CONTRIBUTING.md now states the flow (draft -> ready -> bot round -> drill -> state:needs-human -> merge), the heading format, that three releases shipped through the gap, and the recorded-waiver escape. It also describes the drill as ONE orchestrated run over the whole stack, because box and rig are mutually recursive and cannot be linearly ordered: rig sits below box as the host-builder ('rig bootstrap --host yes' installs box and runs setup-host) and above it as the guest-converger (a box new seed curls rig's installer and runs 'rig bootstrap <tenant>-box'), the inverted edge bin/box already documents as rig#28. The run is host bootstrap -> box new -> tenant converge -> cast. It drills CANDIDATE REFS, not released artifacts: RIG_REPO/RIG_REF are mint-time environment variables defaulting to heavy-duty/rig@main (bin/box:1116-1117), so a run pins the exact commits under test and no repo must be released before another can be drilled. Drilling the candidate is drilling the release — a release diff is VERSION + CHANGELOG.md, so nothing executable differs. One run, one shared run ID; each repo records its own legs citing that ID and the other two SHAs, and the guard reads only this repo's file. Recorded as a known gap, not fixed here: a released box still defaults RIG_REF to main, so a box minted a week after a drill is not the drilled combination. Pinning RIG_REF to a released rig tag in the templates is the outstanding step from #81 (rig#32 step 5). LABELS.md documents blocker:drill-pending — ceremony correct but unevidenced, maintainer-created because the bot account gets a 403 on label creation, with `blocked` standing in until it exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.9 KiB
6.9 KiB
Changelog
History before 0.5.0 lives in git and in drill/RUNS.md, which records not just what changed but what each drill run proved.
Unreleased
Added
box importstamps the trip, leaving the artifact's own mint stamp intact (#131)- A minted box records how it was minted, and
box inforeads it back (#103) - A clone re-stamps its own provenance instead of inheriting its source's (#103)
box infogrew a provenance block, blank on boxes that predate the stamp (#103)- Every fresh mint marks a
pristinesnapshot, before rig converges anything (#104, heavy-duty/rig#62) - A mint that converges a tenant role marks a
bootstrappedsnapshot (#130) - CI refuses a release PR with no drill record in
drill/RUNS.md
Changed
state:needs-humanis set at handoff, not by the cron (#141)- PR labels split into two axes:
state:*(whose ball) andblocker:*(what is in the way);state:needs-rebaseis retired - BREAKING: the tenant templates carry rig's family suffix —
claude→claude-box,codex→codex-box,grok→grok-box,staging→staging-box(#123, heavy-duty/rig#76) - Changelog entries are one line each, and the whole file now follows the rule (#147)
Fixed
test/release.shis green on the release ceremony's own treechangelog-monotonic.shno longer lets a duplicate heading through when it cannot see the base (#143)- An unreadable check rollup no longer reads as "nothing is failing"
state:needs-humanno longer appears on PRs a human cannot merge (#136)- CI's shellcheck sweep now lints
.github/scripts/*.sh(#116) - A PR can no longer delete or duplicate a shipped changelog section and stay green (#122)
- An upgrade over a pre-0.7.0 flat
/opt/boxno longer skips host setup (#115) - Host setup runs the version it just installed, not whatever
currentpoints at (#115) - The pre-0.7.0 migration says what it left behind, and how to keep or reap it (#117)
teardown-host.shrefuses a terminal-less run instead of aborting mute (#113)drill/wipe.shno longer carries #102's SIGPIPE shape, and the pin sweeps the class (#107)- The racing-reader sweep guards the class, not one spelling, and names
incus config trust listas a second writer (#124)
0.8.0 — 2026-07-19
Added
- Merging the release PR is the release, and the release re-arms main itself (#96)
Fixed
- The release ceremony re-arms
CHANGELOG.md, and CI refuses to let main sit disarmed (#108, heavy-duty/rig#67) - Ctrl-D at a confirmation prompt aborts out loud instead of exiting in silence (#111)
box restoreasks before it destroys, in the row's own words rather thanrm's (#105)box-firewallcould hand a UFW host the no-UFW firewall, ~2% of the time (#102)- A missing firewall log now diagnoses itself (#102)
box grantprovisions anincus-adminmember instead of refusing them (#99)
0.7.0 — 2026-07-19
Added
- The installer defaults to the latest release, and releases publish themselves (#83)
setup-hostauto-picks a free subnet — nested box-in-box with zero flags (#80)setup-hostrefuses a claimed subnet, andBOX_SUBNETpicks another (#80)box doctorknows the #80 signature: a gateway held as a local address, and duplicate connected routes for the uplink subnet- The
stagingtemplate — a server-class, creds-free seed (#81) - The
BOX_BOOTSTRAP_ROLEtemplate key, auto-run at mint (#81) - The rig pin point:
RIG_REPO/RIG_REF(#81) - Server-posture template keys
BOX_REQUIRE_VMandBOX_AUTOSTART(#81) - The template test suite discovers
templates/*/instead of hardcoding the list (#81) box export/box import— a box's state that survives the box and the host (#70)- Versioned installs at
<root>/versions/<v>, withbox versionsandbox use(#66) - A real uninstall:
box uninstall [<version>] [--all] [--purge-host], ending in an absence assert BOX_INSTALL_SOURCE=<dir-or-tarball>installs from a local tree, and CI's rehearsal drills the uninstall to zero residuetest/cli.shdrives real installs against throwaway roots and a fake incus (154 checks)
Changed
- Thin templates — box mints a creds-free seed, rig's bootstrap roles converge the tenant content (#81, heavy-duty/rig#31)
Fixed
- A wedged
incus launchfails loudly, not forever: the launch phase is narrated and time-boxed (#93) - UFW's gateway carve-out converges with the bridge, and the doctor can see it (#86)
- The boot-time gateway fallback is gone — an unaddressed bridge leaves the persisted UFW rules alone (#86)
revoke --purgere-checks the incus-user state, and stats it through$SUDO- A wedged
$BINDIR/boxno longer blocks installing
0.6.0 — 2026-07-18
Added
- The restricted tier:
box grant/box revokegive a user their own boxes on the shared hardenedboxnet(#74) - CI runs the multi-user rehearsal on a real Incus
- Global / root install — one world-readable tree at
/opt/box(#71) - CI and a test suite:
.github/workflows/ci.ymlandtest/cli.sh
Fixed
box restorenever worked against Incus 6 — it dispatchedincus restore, which does not existbox tmuxworks on every template — tmux is in each template's package list (#65)box setup-hostfinishes in one run, re-execing itself undersg incus-admin(#63)setup-hostworks as root, with or withoutsudosetup-hostgrantsincus-adminto the human, not to rootbox-firewall.servicereports its state honestly, viaRemainAfterExit=yessetup-host's apt calls can no longer hang on the dpkg lock
Changed
drill.shasserts the post-install stack instead of building it itselfinstall.shasks, sets up the host, and no-ops on re-run (#64)install.shnever overwrites an existing install
0.5.0 — 2026-07-15
The release the project was renamed in: the repo is heavy-duty/box, matching
the CLI it ships. Everything legacy-facing is honored forever — the
user.claudebox=1 tag, the .claudebox/ runbook folder, the old symlink the
installer retires — but nothing current carries the old name.
Added
codexandgroktemplatesbox expose <box> <port> [<host-port>]— a loopback-only door to a port inside a box- Inline resource overrides on
new:--cpu,--memory,--disk(#57) - Host lifecycle as verbs:
box setup-host,box teardown-host,box migrate-host - The
.box/recipe convention, renamed from.claudebox/(both spellings read)
Fixed
- VM mints no longer hang at GRUB — boxes launch with
security.secureboot=false box exposeactually delivers packets- Firewall rules converge on upgrade instead of pinning a host to the release that first ran there
- Failed mints tell you why
grokinstalls the binary it actually ships
Changed
- Debrand complete — env vars, install dir, docs, template descriptions and
the README all say
box; the install URL isheavy-duty/box - The drill grew from 47 to 84 checks