box/CHANGELOG.md
dan-claude-bot 0678b22e01 feat: CI refuses a release PR with no drill record
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>
2026-07-21 15:24:15 +00:00

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 import stamps the trip, leaving the artifact's own mint stamp intact (#131)
  • A minted box records how it was minted, and box info reads it back (#103)
  • A clone re-stamps its own provenance instead of inheriting its source's (#103)
  • box info grew a provenance block, blank on boxes that predate the stamp (#103)
  • Every fresh mint marks a pristine snapshot, before rig converges anything (#104, heavy-duty/rig#62)
  • A mint that converges a tenant role marks a bootstrapped snapshot (#130)
  • CI refuses a release PR with no drill record in drill/RUNS.md

Changed

  • state:needs-human is set at handoff, not by the cron (#141)
  • PR labels split into two axes: state:* (whose ball) and blocker:* (what is in the way); state:needs-rebase is retired
  • BREAKING: the tenant templates carry rig's family suffix — claudeclaude-box, codexcodex-box, grokgrok-box, stagingstaging-box (#123, heavy-duty/rig#76)
  • Changelog entries are one line each, and the whole file now follows the rule (#147)

Fixed

  • test/release.sh is green on the release ceremony's own tree
  • changelog-monotonic.sh no 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-human no 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/box no longer skips host setup (#115)
  • Host setup runs the version it just installed, not whatever current points at (#115)
  • The pre-0.7.0 migration says what it left behind, and how to keep or reap it (#117)
  • teardown-host.sh refuses a terminal-less run instead of aborting mute (#113)
  • drill/wipe.sh no 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 list as 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 restore asks before it destroys, in the row's own words rather than rm's (#105)
  • box-firewall could hand a UFW host the no-UFW firewall, ~2% of the time (#102)
  • A missing firewall log now diagnoses itself (#102)
  • box grant provisions an incus-admin member 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-host auto-picks a free subnet — nested box-in-box with zero flags (#80)
  • setup-host refuses a claimed subnet, and BOX_SUBNET picks another (#80)
  • box doctor knows the #80 signature: a gateway held as a local address, and duplicate connected routes for the uplink subnet
  • The staging template — a server-class, creds-free seed (#81)
  • The BOX_BOOTSTRAP_ROLE template key, auto-run at mint (#81)
  • The rig pin point: RIG_REPO / RIG_REF (#81)
  • Server-posture template keys BOX_REQUIRE_VM and BOX_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>, with box versions and box 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 residue
  • test/cli.sh drives 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 launch fails 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 --purge re-checks the incus-user state, and stats it through $SUDO
  • A wedged $BINDIR/box no longer blocks installing

0.6.0 — 2026-07-18

Added

  • The restricted tier: box grant / box revoke give a user their own boxes on the shared hardened boxnet (#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.yml and test/cli.sh

Fixed

  • box restore never worked against Incus 6 — it dispatched incus restore, which does not exist
  • box tmux works on every template — tmux is in each template's package list (#65)
  • box setup-host finishes in one run, re-execing itself under sg incus-admin (#63)
  • setup-host works as root, with or without sudo
  • setup-host grants incus-admin to the human, not to root
  • box-firewall.service reports its state honestly, via RemainAfterExit=yes
  • setup-host's apt calls can no longer hang on the dpkg lock

Changed

  • drill.sh asserts the post-install stack instead of building it itself
  • install.sh asks, sets up the host, and no-ops on re-run (#64)
  • install.sh never 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

  • codex and grok templates
  • box 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 expose actually delivers packets
  • Firewall rules converge on upgrade instead of pinning a host to the release that first ran there
  • Failed mints tell you why
  • grok installs the binary it actually ships

Changed

  • Debrand complete — env vars, install dir, docs, template descriptions and the README all say box; the install URL is heavy-duty/box
  • The drill grew from 47 to 84 checks