refactor: one drill record per version, in drills/

Drill records move from sections inside drill/RUNS.md to one file per
version at drills/<version>.md.

The old guard had to parse headings: an em-dash prefix match, an optional
" — DATE" tail, a whole-version comparison so 0.3.0-rc1 could not satisfy
0.3.0, and a separate non-blank-body rule. All of it existed only because
records shared one file, and both sibling repos shipped a defect out of
that complexity in review — a `sed '/./,$!d'` extractor where `.` matches
a space (box#149, cast#138), and heading-grammar drift. One file per
version makes nearly all of it unrepresentable: 0.3.0.md and 0.3.0-rc1.md
are different files, and the filesystem does the whole-version comparison.

The awk drill_section() machinery is gone. What survives is the one rule
splitting the files does not make unrepresentable: a file of only
whitespace is not a record.

Plain drills/, NOT .drills/ — a dot-directory is invisible to globs
without dotglob, the cause of #70 here and box#116/box#118.

drill/RUNS.md is deleted; it was created in this same unmerged PR, held no
real records, and its useful reasoning moves to drills/README.md. (box
keeps ITS drill/RUNS.md — that one is a genuine harness log.)

Also corrects the ordering framing in CONTRIBUTING and the new README: the
three repos' drills are INDEPENDENT, run in any order. What makes that safe
is that each pins the same fixed set of candidate refs, so box and rig
measure the same pair — that, not sequencing, is what dissolves the mutual
recursion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-21 16:27:08 +00:00
parent 750d93deda
commit 11dfeb2ecd
8 changed files with 344 additions and 276 deletions

View file

@ -1,10 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
# drill-recorded.sh [<runs-file>] [<version-file>] — assert that the version
# this tree is about to ship has a DRILL RECORD in drill/RUNS.md.
# drill-recorded.sh [<drills-dir>] [<version-file>] — assert that the version
# this tree is about to ship has a DRILL RECORD at <drills-dir>/<version>.md.
#
# defaults: drill/RUNS.md VERSION
# defaults: drills VERSION
#
# CONTRIBUTING ("Releasing") says a release carries a real-hardware drill.
# Nothing enforced it, so no release in this family has ever had one: the
@ -15,6 +15,21 @@ set -euo pipefail
# interesting, which is never at 0.4.3. A bot finally blocked on it; this is
# that block, moved into CI where it does not depend on anyone's attention.
#
# ONE FILE PER VERSION, which is what this script is now mostly about. The
# first cut of this guard kept every record as a section inside one
# drill/RUNS.md, and paid for it: it needed an awk extractor that matched a
# literal '## Release drill — ' prefix, tolerated an optional ' — DATE' tail,
# compared the version WHOLE so that '0.3.0-rc1' could not answer for '0.3.0',
# and then separately insisted the extracted body hold a non-blank line. Every
# one of those rules existed only because records shared a file. Both sibling
# repos shipped a DEFECT out of that complexity during review — a
# `sed '/./,$!d'` extractor where `.` matches a space, so a heading plus one
# tab satisfied the gate (box#149, cast#138), and heading-grammar drift on the
# other side. Splitting the records makes nearly all of it unrepresentable:
# `0.3.0.md` and `0.3.0-rc1.md` are simply different files, there is no
# heading to parse and no grammar to drift, and the whole-version comparison
# is done by the filesystem.
#
# PER-REPO, and that is the load-bearing design decision. The obvious
# alternative — have rig ask box's repo whether the drill ran — cannot fail
# safely: the lookup needs a network call, a token, and a checkout that may be
@ -26,13 +41,18 @@ set -euo pipefail
# records rig's own legs in rig's own repo, and this script reads a file that
# is either in the checkout or is not.
#
# The directory is `drills/`, NOT `.drills/`. A dot-directory is invisible to
# every glob that has not set `dotglob`, which is how #70 here and box#116 /
# box#118 all happened: a file that exists but that no sweep can see is worse
# than no file, because it reads as covered.
#
# What it asserts is a RECORD, not a RESULT — and that is deliberate, not a
# weakness. A gate that demanded "the drill passed" would have to parse
# somebody's prose for a verdict, and would leave a maintainer who consciously
# ships without a full drill (a doc-only release, a hardware outage) with no
# move except deleting the check. Requiring a record means the waiver is
# WRITTEN DOWN, under the version it applies to, in a commit a reviewer sees.
# Skipping stays possible; skipping silently does not.
# WRITTEN DOWN, in a file named for the version it applies to, in a commit a
# reviewer sees. Skipping stays possible; skipping silently does not.
#
# Vacuous on a `-dev` tree, which is why it needs no trigger scoping in
# ci.yml (unlike changelog-monotonic.sh, whose input is a diff): every ordinary
@ -40,9 +60,13 @@ set -euo pipefail
# all. The check has something to say on exactly one tree — the release
# ceremony PR — and that is the tree it must be impossible to merge without.
runs="${1:-drill/RUNS.md}"
drills="${1:-drills}"
version_file="${2:-VERSION}"
# An unreadable version file is an ERROR, never a silent pass. There is no
# version to be lenient about, so leniency here could only mean "ship
# unevidenced" — the exact degradation the per-repo decision above exists to
# avoid.
[ -f "$version_file" ] || {
echo "drill-recorded: no such file: $version_file" >&2
exit 1
@ -65,88 +89,60 @@ case "$version" in
;;
esac
# drill_section <file> <version> — print the BODY under that version's
# heading: everything between it and the next '## ' (or EOF), leading blank
# lines dropped. Empty output means "no record", which is the failure.
#
# Modelled on release-lib.sh's changelog_section(), with one difference that
# is the whole reason it is not that function: changelog_section() matches on
# awk's field $2, which works because a changelog heading is '## <version> …'.
# Here the version is the FOURTH field of '## Release drill — X.Y.Z — DATE',
# and matching by field number would break the moment the em dashes moved. So
# this matches the literal prefix and then compares the version WHOLE.
#
# Whole is the point. A prefix match makes '0.3.0' satisfied by a record for
# '0.3.0-rc1' — a drill run against a release candidate, silently accepted as
# evidence for the final — and, in the other direction, makes an '0.3.0'
# record satisfy '0.3.0-rc1'. Both are the same defect: the string that
# matched is not the artefact that ships. changelog_section()'s exact `$2 ==
# ver` is the precedent; this preserves it through a longer heading.
drill_section() {
awk -v ver="$2" '
/^## / {
if (found) exit
line = $0; sub(/[[:space:]]+$/, "", line)
found = 0
pfx = "## Release drill — "
if (index(line, pfx) == 1) {
rest = substr(line, length(pfx) + 1)
# Split at the version LENGTH, then demand the remainder be either
# nothing or the optional " — <date>". A trailing "-rc1" lands in
# tail, fails both, and is correctly not a match. (Double quotes on
# purpose: an apostrophe here would close the awk program.)
if (substr(rest, 1, length(ver)) == ver) {
tail = substr(rest, length(ver) + 1)
if (tail == "" || index(tail, " —") == 1) found = 1
}
}
next
}
found && !body && /^[[:space:]]*$/ { next }
found { body = 1; print }
' "$1"
}
record="$drills/$version.md"
# A missing runs file is not a different failure from a missing section: both
# mean "this release has no recorded drill", and both want the same unblock
# text. The first release under this gate in a repo with no drill/ directory
# yet is the missing-file case, and it must read as a to-do, not as a broken
# invocation.
record=""
if [ -f "$runs" ]; then
record="$(drill_section "$runs" "$version")"
# WHITESPACE IS NOT A RECORD. This is the one surviving piece of the rule set
# the old section-parsing guard needed, and it survives because it is the one
# part that splitting the files does not make unrepresentable: an empty file,
# or a file holding only spaces, tabs and newlines, exists at the right path
# and is still no evidence. It is the same property box#149 and cast#138 both
# got wrong with `sed '/./,$!d'` (`.` matches a space), where a record of one
# tab shipped an evidence-free release. `grep -q '[^[:space:]]'` is the whole
# check now, with no extractor in front of it to get wrong.
#
# Written as if/then, NOT as `[ -f "$record" ] && recorded=yes`. Under `set -e`
# a bare `a && b` statement whose last command fails takes the whole script
# down with its exit status — so a miss would exit 1 here, before the failure
# message below ever printed, and the author would see nothing.
recorded=no
if [ -f "$record" ] && grep -q '[^[:space:]]' "$record"; then
recorded=yes
fi
if [ -z "$record" ]; then
if [ "$recorded" != yes ]; then
{
echo "drill-recorded: VERSION is $version, and $runs has NO drill record for it."
echo "drill-recorded: VERSION is $version, and there is no drill record at $record."
echo
cat <<EOF
This tree is a release ceremony tree — VERSION is bare, so merging it ships
$version. CONTRIBUTING ("Releasing") requires that release to carry a real
hardware drill, recorded in $runs under a heading of exactly this shape:
hardware drill, recorded in a file named for the version, exactly:
## Release drill — $version — YYYY-MM-DD
$drills/$version.md
The trailing date is optional; the version is matched WHOLE, so a record for
$version-rc1 (or for a different version entirely) does not count. The
section must have at least one non-blank line under it — a bare heading is
not a record.
One file per version, so the name IS the match: a record for
$version-rc1 lives at a different path and does not count. The file must
hold at least one non-whitespace character — an empty file, or one of only
spaces and tabs, is not a record.
Two ways to unblock, and both are a commit on this PR:
1. RUN THE DRILL and record it. What ran, on what hardware, the numbers,
and what failed. It is ONE orchestrated run over the whole stack, on
CANDIDATE refs (RIG_REPO/RIG_REF are mint-time variables, so the run
pins the commits under test) — drilling the candidate IS drilling the
release, since a release PR's diff is VERSION + CHANGELOG.md and nothing
executable differs. Cite the run ID and the other repos' SHAs.
and what failed. rig's drill asserts CONVERGENCE — a machine reaches
its role, idempotently — against a PINNED set of candidate refs
(RIG_REPO/RIG_REF and BOX_REF are mint-time variables, so the run pins
the commits under test). Drilling the candidate IS drilling the
release, since a release PR's diff is VERSION + CHANGELOG.md and
nothing executable differs. Cite the run ID and the other repos' SHAs.
The three repos' drills are independent — rig's does not wait on box's.
2. RECORD AN EXPLICIT MAINTAINER WAIVER under the same heading, saying who
2. RECORD AN EXPLICIT MAINTAINER WAIVER in that same file, saying who
waived it and why. This guard asks for a RECORD, not a passing result,
so a deliberate skip is allowed — it just has to be visible and
reviewable rather than silent.
See $drills/README.md for what a record should contain.
Do not delete this step to get green. A release that cannot say what was
drilled is the state this check exists to end.
EOF
@ -154,5 +150,5 @@ EOF
exit 1
fi
lines="$(printf '%s\n' "$record" | grep -c . || true)"
echo "drill-recorded: $runs records a drill for $version ($lines line(s) under the heading)."
lines="$(grep -c . "$record" || true)"
echo "drill-recorded: $record records a drill for $version ($lines non-blank line(s))."

View file

@ -78,7 +78,7 @@ jobs:
CHANGELOG_MONOTONIC_STRICT: '1'
run: bash .github/scripts/changelog-monotonic.sh "origin/${{ github.base_ref || github.ref_name }}"
# The release this tree would ship has a recorded real-hardware drill
# (drill/RUNS.md). CONTRIBUTING ("Releasing") has always required one and
# (drills/<version>.md). CONTRIBUTING ("Releasing") has always required one and
# nothing enforced it, so no release in this family has ever carried one
# — the drill was the single ceremony step checked by a reviewer
# remembering rather than by a script.

View file

@ -23,7 +23,7 @@ on the way to cutting its first release, and this file starts there.
### Added
- CI refuses a release PR with no drill record in `drill/RUNS.md`
- CI refuses a release PR with no drill record at `drills/<version>.md`
- `rig platform` — what this machine is, computed at run time, stored nowhere
(#64)
- `/etc/rig/manifest` records which rig converged a machine, and when (#61)

View file

@ -100,33 +100,52 @@ extra gate before the handoff:
The **drill** is a real-hardware run — tenant guests minted and converged via
box, `test/db-integration.sh`, the GitHub runner lifecycle against a fork, a
coolify install — recorded in [`drill/RUNS.md`](drill/RUNS.md) under a heading
of exactly:
coolify install — recorded in **one file per version**:
```
## Release drill — X.Y.Z — YYYY-MM-DD
drills/<version>.md
```
named for the version exactly as `VERSION` carries it. See
[`drills/README.md`](drills/README.md) for what a record should contain.
`.github/scripts/drill-recorded.sh` enforces it on every release: a bare
`VERSION` with no non-empty section for it turns CI red, naming the version.
It is **not a thing a reviewer has to remember** — that is how every release
in this family shipped undrilled until a bot finally blocked on one. On a
`-dev` tree it asserts nothing, so it is invisible to ordinary PRs. rig reads
`VERSION` with no non-empty `drills/<version>.md` turns CI red, naming the
version. It is **not a thing a reviewer has to remember** — that is how every
release in this family shipped undrilled until a bot finally blocked on one. On
a `-dev` tree it asserts nothing, so it is invisible to ordinary PRs. rig reads
rig's own record and never box's repo: a cross-repo lookup fails on a token,
a fork checkout or a network blip, and all of those degrade to "pass" —
the UNREADABLE-vs-NONE shape #90 fixed.
**The drill is ONE orchestrated run over the whole stack**, not three
independent ones. box and rig are mutually recursive, so there is no linear
order to drill them in: rig sits *below* box as the host-builder and *above*
it as the guest-converger. The run therefore goes:
One file per version is what keeps the guard small. Records used to share a
single log, which forced a heading grammar, an optional-date tail, a
whole-version comparison and a non-blank-body rule just to read them back — and
both sibling repos shipped a defect out of that complexity in review. Now
`0.3.0.md` and `0.3.0-rc1.md` are simply different files.
1. `rig bootstrap … --host yes` on a bare Debian host — which installs box and
runs box's `setup-host` (`RIG_SKIP_BOX_INSTALL=1` skips it; see README)
2. `box new` mints a creds-free seed
3. the seed converges via `rig bootstrap <tenant>-box` — the seed's cloud-init
curls rig's installer at `@RIG_REPO@/@RIG_REF@`
4. cast on top
**The three repos' drills are INDEPENDENT.** Run them in any order, on any
schedule, in separate sittings. What makes that safe is that every drill **pins
the same fixed set of candidate refs**: rig's drill runs `--host yes` with
`BOX_REF=release/<box-version>`, so it exercises the box that will actually
ship; box's drill mints with `RIG_REF=release/<rig-version>`, so it exercises
the rig that will actually ship. Both measure the same pair.
That — not sequencing — is what dissolves the box↔rig recursion. box and rig
are mutually recursive (`rig bootstrap … --host yes` installs box and runs
box's `setup-host`; box's `box new` seeds converge back through rig's installer
at `@RIG_REPO@/@RIG_REF@`), but the refs are static identifiers that exist as
soon as the release branches do, long before any drill runs, so a cycle at
runtime becomes independent tests against one fixed pair. Within a single drill
you naturally bring the substrate up before probing it — a host before a guest
— but that is how you run a drill, not an ordering rule between repos.
Each repo drills in a **different way** and asserts a different thing: rig
asserts **convergence** (a machine reaches its role, idempotently), box asserts
the **isolation contract** (the VM trust boundary), cast asserts **promotion**
(A→B reproduces, the diff is idempotent). Three different exercises sharing a
substrate, not three phases of one script — which is exactly why the records
are per-repo.
It drills **candidate refs, not released artifacts.** `RIG_REPO`/`RIG_REF` are
mint-time environment variables (default `heavy-duty/rig@main`), so a run pins
@ -137,16 +156,18 @@ repo has to be released before another can be drilled.
`VERSION` + `CHANGELOG.md` and nothing else — no executable difference exists
between the tree that was drilled and the tree that ships.
One run emits **one shared run ID**. Each repo records *its own* legs under its
own `## Release drill — X.Y.Z — DATE`, citing that run ID and the other two
repos' commit SHAs, so the three records can be joined after the fact by
anyone reading them. The guard still reads only this repo's file — there is no
Drills that share a substrate share **one run ID**. Each repo records *its own*
legs in its own `drills/<version>.md`, citing that run ID and the other two
repos' commit SHAs, so the records can be joined after the fact by anyone
reading them. The guard still reads only this repo's file — there is no
cross-repo lookup anywhere in the gate. Releases do **not** have to be
published in a fixed order.
published in a fixed order. If a defect shows up only in the combination:
patch, re-drill, re-record. The three releases converge on a set that holds
together; they are not required to be right in one pass.
A **maintainer waiver** is possible — a doc-only release, a hardware outage —
but it must be **recorded in `drill/RUNS.md` for that version**, saying who
waived it and why. The guard asks for a *record*, not a passing result,
but it must be **recorded in `drills/<version>.md` for that version**, saying
who waived it and why. The guard asks for a *record*, not a passing result,
precisely so that skipping is a deliberate, reviewable commit instead of a
silence. Deleting the check is not the move.

View file

@ -36,7 +36,7 @@ PR carries as many as apply.
| `blocker:conflict` | `#B60205` | GitHub says `CONFLICTING` — the agent owes a **rebase** | it merges cleanly |
| `blocker:ci-red` | `#B60205` | a check failed — the agent owes a **fix**, which a rebase will not provide | checks are green |
| `blocker:unrequested` | `#E99695` | this head has no verdict from somebody — never reviewed, or staled by a push — and **nobody was asked** for one | reviews are requested |
| `blocker:drill-pending` | `#E99695` | a `release` PR whose version has **no drill record** in [`drill/RUNS.md`](drill/RUNS.md) — the ceremony is correct but *unevidenced* | the drill is run and recorded, or a maintainer waiver is recorded for that version |
| `blocker:drill-pending` | `#E99695` | a `release` PR whose version has **no drill record** at [`drills/<version>.md`](drills/README.md) — the ceremony is correct but *unevidenced* | the drill is run and recorded, or a maintainer waiver is recorded for that version |
`blocker:drill-pending` is the one blocker that is not about the code: the
branch merges, the checks that read the tree are green, and the release is
@ -165,7 +165,7 @@ gh label create "blocker:conflict" --color B60205 --description "Does not me
gh label create "blocker:ci-red" --color B60205 --description "A check is failing — the agent owes a fix (not a rebase)" --force
gh label create "blocker:unrequested" --color E99695 --description "Somebody still owes a verdict and nobody was asked for one" --force
# Needs a MAINTAINER account — the bot 403s on label creation. Until it exists, `blocked` stands in.
gh label create "blocker:drill-pending" --color E99695 --description "Release PR with no drill record in drill/RUNS.md — correct but unevidenced" --force
gh label create "blocker:drill-pending" --color E99695 --description "Release PR with no drill record at drills/<version>.md — correct but unevidenced" --force
# retired — the reconciler strips it; delete it once no PR carries it
# gh label delete "state:needs-rebase"
gh label create "state:needs-human" --color 8250DF --description "No blockers, all bots approve — waiting on the human reviewer" --force

View file

@ -1,80 +0,0 @@
# Drill run log
rig's own record of its real-hardware drill legs. One section per run,
appended. **This file is the record, not the instrument.**
rig has **no drill harness script of its own yet**. Its legs are run by hand,
following the documented procedure:
- tenant guests minted and converged **via box**
- `bash test/db-integration.sh` against a real Postgres on the machine
- the GitHub runner lifecycle — register, take a job, deregister — against a
fork
- a coolify install
The harness lives in heavy-duty/box's `drill/`. rig does **not** reach into it
to decide whether rig may ship. A cross-repo lookup that fails silently
degrades to "pass", which is the UNREADABLE-vs-NONE shape #90 fixed — so the
gate reads this file, in this repo, and nothing else.
The drill itself is **one orchestrated run over the whole stack**
`rig bootstrap --host yes` on a bare host (which installs box and runs box's
`setup-host`), then `box new` for a creds-free seed, then that seed converging
via `rig bootstrap <tenant>-box`, then cast on top. box and rig are mutually
recursive, so there is no order to drill them in: rig is both the host-builder
below box and the guest-converger above it.
It drills **candidate refs, not released artifacts**: `RIG_REPO`/`RIG_REF` are
mint-time environment variables, so a run pins the exact commits under test.
Drilling the candidate *is* drilling the release, because a release PR's diff
is `VERSION` + `CHANGELOG.md` and nothing executable differs. One run emits one
shared **run ID**; each repo records its own legs under its own heading, citing
that run ID and the other two repos' commit SHAs (CONTRIBUTING, "Releasing").
## What the gate requires
`.github/scripts/drill-recorded.sh` runs on every PR. On a `-dev` tree it
asserts nothing — a development tree has no release to evidence. On a bare
`VERSION` — a release ceremony tree — it requires a section here headed
exactly:
## Release drill — X.Y.Z — YYYY-MM-DD
The trailing ` — DATE` is optional; the version is matched **whole**, so a
`0.3.0-rc1` record does not satisfy `0.3.0` and the reverse is equally false.
The section must extract non-empty: at least one non-blank line before the
next `## `.
The guard requires a **record**, not a passing result. A maintainer waiver is
a legitimate outcome of a release — but it is written here, under that
version's heading, so that skipping the drill is a deliberate, reviewable
commit rather than a silence.
### What a record should contain
What ran, on what, the numbers, and what failed. Below is the *shape*, not a
run that happened — no drill has been recorded here yet:
## Release drill — 9.9.9 — 2026-01-01
Run ID: drill-2026-01-01-a. Host: bare Debian 13 cloud image, 4 vCPU / 8 GB.
Candidate refs: box@1a2b3c4, rig@5d6e7f8, cast@9a0b1c2.
| Leg | Result |
| --- | --- |
| tenant guests minted + converged via box | 3/3 |
| `test/db-integration.sh` | 14/14 |
| runner lifecycle against a fork | PASS — registered, took a job, deregistered clean |
| coolify install | PASS, ~6 min |
Failed: `rig users apply` left one revoked key in `authorized_keys`
(filed #NNN). Everything else clean.
State what failed. A record with no failures listed reads as "nothing broke",
so if a leg was not run, say that instead of omitting it.
## Runs
*None recorded yet.* This log starts empty rather than reconstructing runs
from memory — an invented number is worse than no number. The first release
cut under the gate writes the first section here.

113
drills/README.md Normal file
View file

@ -0,0 +1,113 @@
# Drill records
Per-release evidence for rig's real-hardware drill. **One file per version**,
named for the version exactly as `VERSION` carries it:
drills/<version>.md
So `0.3.0` is recorded in `drills/0.3.0.md`, and `0.3.0-rc1` in
`drills/0.3.0-rc1.md`. They are different files, which is the whole point: the
filesystem does the whole-version comparison that an earlier single-file
version of this had to do with an awk extractor and a heading grammar. A
record for the candidate cannot be mistaken for evidence for the final.
The directory is `drills/`, not `.drills/` — a dot-directory is invisible to
any glob without `dotglob`, which is how #70 here and box#116 / box#118 all
happened.
**This directory is the record, not the instrument.** rig has **no drill
harness script of its own**; its legs are run by following the documented
procedure, and the harness lives in heavy-duty/box's `drill/`. rig does not
reach into it to decide whether rig may ship: a cross-repo lookup that fails
silently degrades to "pass", which is the UNREADABLE-vs-NONE shape #90 fixed.
The gate reads a file in this repo, and nothing else.
## What the gate requires
`.github/scripts/drill-recorded.sh` runs on every PR. On a `-dev` tree it
asserts nothing — a development tree has no release to evidence. On a bare
`VERSION` — a release ceremony tree — it requires `drills/<version>.md` to
exist and to hold at least one non-whitespace character. An empty file, or one
of only spaces and tabs, is not a record (box#149 and cast#138 both shipped an
extractor where a single tab satisfied the gate).
The guard requires a **record**, not a passing result. A maintainer waiver is
a legitimate outcome of a release — but it is written in that version's file,
so that skipping the drill is a deliberate, reviewable commit rather than a
silence. **A failed drill is still a valid record**: the gate wants evidence,
not success.
## The drill
rig's legs:
- tenant guests minted and converged **via box**
- `bash test/db-integration.sh` against a real Postgres on the machine
- the GitHub runner lifecycle — register, take a job, deregister — against a
fork
- a coolify install
box and rig are **mutually recursive**: `rig bootstrap --host yes` installs box
and runs box's `setup-host`, while box's guests converge back through rig's
installer. Within a single drill you naturally bring the substrate up before
probing it — a host before a guest — but that is how you run a drill, not an
ordering rule between repos.
**The three repos' drills are independent.** Run them in any order, on any
schedule, in separate sittings. What makes that safe is that every drill
**pins the same fixed set of candidate refs**: rig's drill runs `--host yes`
with `BOX_REF=release/<box-version>`, so it exercises the box that will
actually ship; box's drill mints with `RIG_REF=release/<rig-version>`, so it
exercises the rig that will actually ship. Both measure the same pair.
That — not sequencing — is what dissolves the box↔rig recursion. The refs are
static identifiers that exist as soon as the release branches do, long before
any drill runs, so a cycle at runtime becomes two independent tests against
one fixed pair. It also means **candidate refs, not released artifacts**:
`RIG_REPO`/`RIG_REF` are mint-time environment variables, so no repo has to be
released before another can be drilled. Drilling the candidate *is* drilling
the release, because a release PR's diff is `VERSION` + `CHANGELOG.md` and
nothing executable differs.
Each repo drills in a **different way** and asserts a different thing: rig
asserts **convergence** (a machine reaches its role, idempotently), box asserts
the **isolation contract** (the VM trust boundary), cast asserts **promotion**
(A→B reproduces, the diff is idempotent). Three different exercises sharing a
substrate, not three phases of one script — which is exactly why the records
are per-repo.
Drills that share a substrate share **one run ID**; each repo records its own
legs in its own file, citing that run ID and the other repos' commit SHAs, so
the records can be joined after the fact. If a defect shows up only in the
combination: patch, re-drill, re-record. The three releases converge on a set
that holds together; they are not required to be right in one pass. Releases
do **not** have to be published in a fixed order.
## What a record should contain
What ran, on what host, the pinned candidate refs, the numbers, and what
failed. Below is the *shape*, in a file named `drills/9.9.9.md` — a version
that can never collide with a real release. **No drill has been recorded here
yet**; this log starts empty rather than reconstructing runs from memory, since
an invented number is worse than no number.
```markdown
# Release drill — 9.9.9 — 2026-01-01
Run ID: drill-2026-01-01-a. Host: bare Debian 13 cloud image, 4 vCPU / 8 GB.
Candidate refs: box@1a2b3c4 (BOX_REF=release/0.4.0), rig@5d6e7f8, cast@9a0b1c2.
| Leg | Result |
| --- | --- |
| tenant guests minted + converged via box | 3/3 |
| re-converge (idempotence) | clean, no changes |
| `test/db-integration.sh` | 14/14 |
| runner lifecycle against a fork | PASS — registered, took a job, deregistered clean |
| coolify install | PASS, ~6 min |
Failed: `rig users apply` left one revoked key in `authorized_keys`
(filed #NNN). Everything else clean.
```
State what failed. A record with no failures listed reads as "nothing broke",
so if a leg was not run, say that instead of omitting it.

View file

@ -471,49 +471,71 @@ check "ci.yml: the checkout has full history (the base ref must resolve)" 0 "" \
# other ceremony step is checked by a script, and the one that costs an
# afternoon was checked by a reviewer remembering. A bot finally blocked on it.
#
# Fixtures carry their OWN VERSION and RUNS.md, inside the fixture dir. This is
# not tidiness — it is heavy-duty/box#146, verbatim: fixtures that read the
# REPO's VERSION exercised the `-dev` branch on every ordinary tree, so the
# whole bare-version half of the guard was untested and went red for the first
# time while somebody was cutting a release. A fixture must state the tree it
# is about.
# Records are ONE FILE PER VERSION, at drills/<version>.md. The first cut of
# this guard kept them as sections in a single drill/RUNS.md and needed a
# heading grammar, an optional-date tail, a whole-version comparison and a
# non-blank-body rule to read them back — all of it there only because the
# records shared a file, and both sibling repos shipped a defect out of it in
# review. Splitting the files deletes most of these tests along with the code
# they covered: `0.3.0.md` and `0.3.0-rc1.md` cannot be confused by any
# grammar, because there is no grammar.
#
# Fixtures carry their OWN version file and their OWN drills dir, inside the
# fixture dir. This is not tidiness — it is heavy-duty/box#146, verbatim:
# fixtures that read the REPO's VERSION exercised the `-dev` branch on every
# ordinary tree, so the whole bare-version half of the guard was untested and
# went red for the first time while somebody was cutting a release. A fixture
# must state the tree it is about.
DRILL="$ROOT/.github/scripts/drill-recorded.sh"
check "drill-recorded.sh: exists and is the guard under test" 0 "" test -f "$DRILL"
check "drill-recorded.sh: is executable" 0 "" test -x "$DRILL"
drilltree() { # drilltree <name> <version> <runs-line...> -> prints the dir
drilltree() { # drilltree <name> <version> -> prints the dir (no drills/ yet)
local d="$WORK/drill-$1"; mkdir -p "$d"; printf '%s\n' "$2" > "$d/VERSION"
shift 2; printf '%s\n' "$@" > "$d/RUNS.md"; printf '%s' "$d"
printf '%s' "$d"
}
drill() { bash "$DRILL" "$1/RUNS.md" "$1/VERSION" 2>&1; }
drillrec() { # drillrec <dir> <version> <line...> — write drills/<version>.md
mkdir -p "$1/drills"; local f="$1/drills/$2.md"; shift 2
printf '%s\n' "$@" > "$f"
}
drill() { bash "$DRILL" "$1/drills" "$1/VERSION" 2>&1; }
# A development tree. Vacuous by construction — every ordinary PR looks like
# this, and none of them can be asked to have drilled a release that does not
# exist. It must pass with the log empty of records, which is the state
# drill/RUNS.md ships in today.
T="$(drilltree dev 0.2.1-dev '# Drill run log' '' 'No runs recorded yet.')"
check "drill: a -dev tree passes with NO drill record" 0 "" drill "$T"
# exist. It passes with no drills/ directory present AT ALL, which is the
# state this repo ships in today.
T="$(drilltree dev 0.2.1-dev)"
check "drill: a -dev tree passes with NO drills dir at all" 0 "" drill "$T"
check "drill: ...saying so out loud, not exiting 0 in silence" 0 \
"nothing to assert" drill "$T"
# The release ceremony tree, drilled and recorded. GREEN.
T="$(drilltree recorded 0.3.0 '# Drill run log' '' '## Release drill — 0.3.0 — 2026-07-21' '' \
'Host: bare Debian 13. Guests via box 0.4.0 (released, pinned).' '' \
'- db-integration: 14/14' '- runner lifecycle: PASS' '' \
'## Release drill — 0.2.0 — 2026-07-01' '' 'an older run')"
check "drill: a bare VERSION with a matching non-empty record passes" 0 "records a drill for 0.3.0" drill "$T"
T="$(drilltree recorded 0.3.0)"
drillrec "$T" 0.3.0 '# Release drill — 0.3.0 — 2026-07-21' '' \
'Host: bare Debian 13. Candidate refs pinned: box@1a2b3c4, cast@9a0b1c2.' '' \
'- convergence, then re-converge: clean' \
'- db-integration: 14/14' '- runner lifecycle: PASS'
drillrec "$T" 0.2.0 '# Release drill — 0.2.0 — 2026-07-01' '' 'an older run'
check "drill: a bare VERSION with a non-empty record for it passes" 0 \
"records a drill for 0.3.0" drill "$T"
# The heading with no date — the trailing ' — DATE' is optional, so a record
# written without one is still a record.
T="$(drilltree nodate 0.3.0 '# Drill run log' '' '## Release drill — 0.3.0' '' 'ran it, 12/12')"
check "drill: the date suffix is optional" 0 "records a drill" drill "$T"
# The gate itself: a release tree with no record at all. RED, naming the
# version, because "which release is unevidenced" is the only fact the author
# needs.
T="$(drilltree norecord 0.3.0 '# Drill run log' '' 'No runs recorded yet.')"
check "drill: a bare VERSION with NO record FAILS" 1 "NO drill record" drill "$T"
# The gate itself: a release tree with no drills/ directory at all. RED,
# naming the version, because "which release is unevidenced" is the only fact
# the author needs. This is the state a repo is in the first time it cuts a
# release under the gate — it must read as a to-do, not a broken invocation.
T="$(drilltree norecord 0.3.0)"
check "drill: a bare VERSION with NO drills dir FAILS" 1 \
"no drill record" drill "$T"
check "drill: ...and the failure names the version" 1 "VERSION is 0.3.0" drill "$T"
check "drill: ...and names the file it wanted" 1 "drills/0.3.0.md" drill "$T"
# A drills/ that exists but holds nothing for THIS version. Same failure —
# other releases having been drilled says nothing about this one.
T="$(drilltree otherversion 0.4.0)"
drillrec "$T" 0.3.0 '# Release drill — 0.3.0' '' 'the previous release'
check "drill: a drills dir with no file for THIS version FAILS" 1 \
"no drill record" drill "$T"
check "drill: ...naming the version that is unevidenced" 1 "VERSION is 0.4.0" drill "$T"
# ...and the failure has to say how to get out of it. Both moves are a commit
# on the PR, and the second one is the point of asking for a RECORD rather
@ -522,75 +544,67 @@ check "drill: ...and the failure names the unblock — run the drill" 1 \
"RUN THE DRILL" drill "$T"
check "drill: ...and the waiver, recorded, as the other way out" 1 \
"MAINTAINER WAIVER" drill "$T"
check "drill: ...and shows the exact heading the guard wants" 1 \
"## Release drill — 0.3.0" drill "$T"
check "drill: ...and points at the README for what a record contains" 1 \
"README.md" drill "$T"
# A heading with nothing under it. This is the failure a laxer guard invites —
# the ceremony PR adds the heading to get green and fills it in never. A
# section is a record only if something is in it.
T="$(drilltree empty 0.3.0 '# Drill run log' '' '## Release drill — 0.3.0 — 2026-07-21' '' '' \
'## Release drill — 0.2.0 — 2026-07-01' '' 'an older run')"
check "drill: a PRESENT but EMPTY record FAILS" 1 "NO drill record" drill "$T"
# ...and it fails for being empty, not for being unfindable: the older section
# below it must not be scavenged to satisfy the newer heading.
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "drill: ...an empty section never borrows the next section's body" 1 "" \
bash -c 'bash "$1" "$2/RUNS.md" "$2/VERSION" 2>&1 | grep -q "an older run"' _ "$DRILL" "$T"
# An EMPTY file at the right path. This is the failure a laxer guard invites —
# the ceremony PR touches the file to get green and fills it in never.
T="$(drilltree emptyfile 0.3.0)"
mkdir -p "$T/drills"; : > "$T/drills/0.3.0.md"
check "drill: a PRESENT but EMPTY record FAILS" 1 "no drill record" drill "$T"
# ...and WHITESPACE is not a record either. This guard already gets it right —
# `/^[[:space:]]*$/` skips a spaces-or-tabs line the same as a bare one — so
# this pins behaviour rather than fixing it. It is here because the siblings
# did NOT get it right: box#149 and cast#138 both extracted with
# `sed '/./,$!d'`, where `.` matches a space, so a heading followed by one tab
# satisfied the gate and shipped an evidence-free release. All three reviewers
# caught it there. Nothing caught it here, because there was nothing to catch —
# which is exactly the state in which a later "simplify this to match its
# siblings" quietly reintroduces it. The cheapest moment to pin a property is
# while you still remember why it matters.
T="$(drilltree blank 0.3.0 '# Drill run log' '' '## Release drill — 0.3.0 — 2026-07-21' ' ' ' ' '' \
'## Release drill — 0.2.0 — 2026-07-01' '' 'an older run')"
check "drill: a record body of only spaces and tabs FAILS (box#149, cast#138)" \
1 "NO drill record" drill "$T"
# ...and WHITESPACE is not a record either. This is the ONE piece of the old
# section-parsing rule set that splitting the files did not make
# unrepresentable, so it is the one that still needs a test. It is here because
# the siblings got it wrong: box#149 and cast#138 both extracted with
# `sed '/./,$!d'`, where `.` matches a space, so one tab satisfied the gate and
# shipped an evidence-free release. All three reviewers caught it there.
# Nothing caught it here, because there was nothing to catch — which is exactly
# the state in which a later simplification quietly reintroduces it.
T="$(drilltree blank 0.3.0)"
drillrec "$T" 0.3.0 ' ' ' ' ''
check "drill: a record of only spaces, tabs and newlines FAILS (box#149, cast#138)" \
1 "no drill record" drill "$T"
# The version is matched WHOLE, both directions. A drill run against a release
# The version is matched WHOLE, both directions — and now the filesystem does
# it, since the version IS the filename. A drill run against a release
# candidate is not evidence for the final release, and the reverse is equally
# false — in both cases the string that matched is not the artefact that
# ships. changelog_section()'s exact `$2 == ver` is the precedent; this heading
# is longer, so the comparison had to be rebuilt rather than inherited.
T="$(drilltree whole-rc 0.3.0 '# Drill run log' '' '## Release drill — 0.3.0-rc1 — 2026-07-20' '' 'the rc drill')"
check "drill: an -rc1 record does NOT satisfy the bare version" 1 "NO drill record" drill "$T"
T="$(drilltree whole-final 0.3.0-rc1 '# Drill run log' '' '## Release drill — 0.3.0 — 2026-07-21' '' 'the final drill')"
check "drill: ...and a bare-version record does NOT satisfy the -rc1" 1 "NO drill record" drill "$T"
# A shorter version is not a prefix win either: 0.3.0 must not be answered by
# a 0.3.0.1 heading, nor 0.3 by 0.3.0.
T="$(drilltree whole-longer 0.3.0 '# Drill run log' '' '## Release drill — 0.3.0.1 — 2026-07-21' '' 'a different thing')"
check "drill: ...nor does a LONGER version number match by prefix" 1 "NO drill record" drill "$T"
# And an unrelated version is simply absent, which is the same failure.
T="$(drilltree other 0.4.0 '# Drill run log' '' '## Release drill — 0.3.0 — 2026-07-21' '' 'the previous release')"
check "drill: a record for a DIFFERENT version is not this version's record" 1 "VERSION is 0.4.0" drill "$T"
# The repo with no drill/ directory yet — the state rig was in before this
# guard. It must read as "this release has no record", the same to-do as an
# empty log, not as a broken invocation.
T="$(drilltree norunsfile 0.3.0 'placeholder')"
rm -f "$T/RUNS.md"
check "drill: a MISSING runs file is the same failure, not a crash" 1 "NO drill record" drill "$T"
check "drill: ...and still names the unblock" 1 "RUN THE DRILL" drill "$T"
# false: in both cases the string that matched is not the artefact that ships.
T="$(drilltree whole-rc 0.3.0)"
drillrec "$T" 0.3.0-rc1 '# Release drill — 0.3.0-rc1' '' 'the rc drill'
check "drill: an -rc1 record does NOT satisfy the bare version" 1 \
"no drill record" drill "$T"
T="$(drilltree whole-final 0.3.0-rc1)"
drillrec "$T" 0.3.0 '# Release drill — 0.3.0' '' 'the final drill'
check "drill: ...and a bare-version record does NOT satisfy the -rc1" 1 \
"no drill record" drill "$T"
# A missing VERSION file is a wrong invocation, not a degradation — there is
# no version to be lenient about.
T="$(drilltree noversion 0.3.0 '# Drill run log')"
# no version to be lenient about, so it must never read as a pass.
T="$(drilltree noversion 0.3.0)"
rm -f "$T/VERSION"
check "drill: a missing VERSION file is an error, never a pass" 1 "no such file" drill "$T"
# The real files, last. The shipped log must be readable by the guard the
# repo actually runs, on the VERSION the repo actually carries.
check "drill/RUNS.md: exists" 0 "" test -f "$ROOT/drill/RUNS.md"
# The real files, last. The shipped README must be readable, and the guard the
# repo actually runs must pass on the VERSION the repo actually carries.
check "drills/README.md: exists" 0 "" test -f "$ROOT/drills/README.md"
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "drill/RUNS.md: documents the heading format the guard requires" 0 "" \
bash -c 'grep -qF "## Release drill — X.Y.Z — YYYY-MM-DD" "$1"' _ "$ROOT/drill/RUNS.md"
check "drills/README.md: documents the one-file-per-version naming rule" 0 "" \
bash -c 'grep -qF "drills/<version>.md" "$1"' _ "$ROOT/drills/README.md"
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "drills/README.md: says a FAILED drill is still a valid record" 0 "" \
bash -c 'grep -qi "failed drill is still a valid record" "$1"' _ "$ROOT/drills/README.md"
# The old single-file record must be gone, not merely unreferenced: a stale
# drill/RUNS.md would be a second place to write a record that nothing reads.
check "drill/RUNS.md: is gone — records live one per version now" 1 "" \
test -e "$ROOT/drill/RUNS.md"
check "drill-recorded.sh: passes against the real tree" 0 "" \
bash "$DRILL" "$ROOT/drill/RUNS.md" "$ROOT/VERSION"
bash "$DRILL" "$ROOT/drills" "$ROOT/VERSION"
# ...and with no arguments at all, since that is how ci.yml invokes it. The
# defaults must be the paths this repo actually uses.
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "drill-recorded.sh: ...and with its DEFAULT arguments, as CI runs it" 0 "" \
bash -c 'cd "$1" && bash .github/scripts/drill-recorded.sh' _ "$ROOT"
# ci.yml: the guard runs from there, so pin the wiring — a script nothing
# invokes is not a check (same reasoning as the monotonic pins above).
@ -607,22 +621,26 @@ check "ci.yml: the drill step itself is NOT trigger-gated" 1 "" drill_step_gated
check "ci.yml: ...and the block was actually found (guards the awk above)" 0 \
"drill-recorded" drill_step_block
# CONTRIBUTING must state the gate, and must state what the drill actually is:
# ONE orchestrated run over the whole stack, on CANDIDATE refs. box and rig are
# mutually recursive — rig builds the host box runs on, and box mints the seeds
# rig converges — so there is no linear "drill A, release A, then drill B"
# order to write down, and pinning RIG_REPO/RIG_REF at mint time is what makes
# the recursion drillable at all. An earlier draft of this doc claimed a fixed
# box → rig → cast release order; it is wrong, and this pins the correction.
# CONTRIBUTING must state the gate, and must state what the drill actually is.
# The three repos' drills are INDEPENDENT — run in any order, on any schedule —
# and what makes that safe is that each one pins the same fixed set of
# CANDIDATE refs, so box and rig measure the same pair. That, not sequencing,
# is what dissolves the mutual recursion (rig builds the host box runs on, and
# box mints the seeds rig converges). An earlier draft of this doc claimed a
# fixed box → rig → cast release order; it is wrong, and this pins the
# correction.
CONTRIB="$ROOT/CONTRIBUTING.md"
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "CONTRIBUTING: the release flow names the drill gate" 0 "" \
bash -c 'grep -qF "drill/RUNS.md" "$1"' _ "$CONTRIB"
bash -c 'grep -qF "drills/<version>.md" "$1"' _ "$CONTRIB"
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "CONTRIBUTING: ...and says the drill is ONE orchestrated stack run" 0 "" \
bash -c 'grep -qi "one orchestrated run" "$1"' _ "$CONTRIB"
check "CONTRIBUTING: ...and says the three repos' drills are INDEPENDENT" 0 "" \
bash -c 'grep -qi "drills are independent" "$1"' _ "$CONTRIB"
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "CONTRIBUTING: ...on candidate refs, which is what dissolves the recursion" 0 "" \
check "CONTRIBUTING: ...pinned to one fixed set of candidate refs" 0 "" \
bash -c 'grep -qi "same fixed set of candidate refs" "$1"' _ "$CONTRIB"
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
check "CONTRIBUTING: ...which is what dissolves the recursion, not sequencing" 0 "" \
bash -c 'grep -qF "RIG_REF" "$1"' _ "$CONTRIB"
# The negative that keeps the correction from being re-lost: no fixed release
# order may be claimed. Nothing requires box to ship before rig.