#!/usr/bin/env bash set -euo pipefail # drill-recorded.sh [] [] — assert that the version # this tree claims to ship has a DRILL RECORD in drill/RUNS.md. # # CONTRIBUTING says a release carries the full real-hardware drill. Nothing # checked that, so no release in this family ever carried one: the step lived # only in a reviewer's memory, and a step that lives in a reviewer's memory is # performed exactly as often as the reviewer remembers it. A bot finally # blocked on it, which is the first time the omission was visible at all. So # the gate moves into CI, where it is asserted on every release PR rather than # recalled. # # WHAT IT ASSERTS, AND WHAT IT DELIBERATELY DOES NOT # # It asserts a RECORD EXISTS — not that the drill passed. That is the whole # design. A maintainer may ship on a failed or partial drill; what they may not # do is ship on silence. Requiring a record makes a waiver a deliberate, # reviewable commit (a section saying who waived it and what is untested) # instead of the default outcome of forgetting. A guard that demanded a PASS # would be argued with and eventually bypassed; one that demands EVIDENCE has # nothing to argue about. # # PER-REPO, ON PURPOSE # # This reads cast's OWN drill/RUNS.md. It does not reach into box or rig to ask # whether the family drilled. A cross-repo lookup has a failure mode this repo # keeps refusing: when the fetch fails — no network, moved file, renamed repo, # a token without read on the other repo — the honest answers are "unknown" and # "blocked", but the shape such code actually takes degrades to "pass". Same # class as the unreadable check rollup that read as "nothing is failing". # # There is also nothing to look up. The drill is ONE orchestrated run over the # whole stack (CONTRIBUTING.md, "Releasing"): rig bootstraps the host and # installs box, box mints a seed, the seed calls rig back to converge, and # cast's legs run on the result. rig therefore sits BELOW box and ABOVE it — # the three repos are mutually recursive, not linearly ordered, and their # releases are NOT published in a fixed sequence. The run pins CANDIDATE refs # (RIG_REPO/RIG_REF at mint time), so no repo must ship before another can be # drilled. Each repo records its own legs from that run, citing the shared run # ID and the other repos' SHAs — which is how three records reassemble into one # run without any repo reading another's file. # # A file of its own, not a clause inlined in ci.yml, for the same reason as # release-notes.sh and changelog-monotonic.sh: test/release.test.ts drives the # REAL script against fixtures, so what the tests prove is what CI runs. runs="${1:-drill/RUNS.md}" version_file="${2:-package.json}" [ "$#" -le 2 ] || { echo "usage: drill-recorded.sh [] []" >&2; exit 2; } [ -f "$version_file" ] || { echo "drill-recorded: no such file: $version_file" >&2; exit 1; } # cast's version lives in package.json (there is no VERSION file), so this # reads JSON — with sed, not node. release-notes.sh takes the version as an # ARGUMENT and so never had to; this one is invoked by CI with no arguments and # has to find it itself. sed keeps the script runnable by `bash -n`, shellcheck # and a bare shell alike, with no dependency on a toolchain being installed # before the guard can speak. The first "version" key in package.json is the # package's own by npm's schema; dependency entries are "": "" # pairs and carry no "version" key to be confused with it. ver="$(sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$version_file" | head -1)" [ -n "$ver" ] || { echo "drill-recorded: no \"version\" key in $version_file" >&2; exit 1; } # A -dev tree is main between releases. Nothing ships from it, so there is no # claim to evidence — and demanding a record here would make every ordinary # feature PR red until somebody drilled for a version that will never be cut. # The gate is about the SHIP CLAIM, and `-dev` is the absence of one. case "$ver" in *-dev) echo "drill-recorded: version $ver is a development tree — nothing ships from it, so there is nothing to assert." exit 0 ;; esac # A bare version is a release ceremony tree: this is the tree whose merge IS # the release, so this is where the evidence has to exist. [ -f "$runs" ] || { { echo "drill-recorded: version $ver is a release, but there is no $runs at all." echo echo " The release drill has to be RECORDED, not just performed. Create the file" echo " with the run's section (see the format below) and commit it." } >&2 exit 1 } # $5 of a drill heading ('## Release drill — 0.2.0 — 2026-07-21') is the bare # version, compared WHOLE — so 0.2.0 can never be satisfied by a 0.2.0-rc1 # section, or the reverse, and no regex-escaping of dots. Exactly the trap # release-notes.sh solves the same way, with the same awk field split, so the # two cannot drift apart about what a heading is. The trailing ' — DATE' is # optional and unread: the gate is about the version, and the date is for the # humans reading the log. # # grab is re-armed by every '## ' line, so the section ends at the next one — # a record cannot borrow the body of the record below it. sed drops the blank # padding under the heading and the command substitution eats the trailing # blanks, which is what makes "present but EMPTY" distinguishable from # "present with content": a heading with nothing but whitespace under it # extracts to the empty string. record="$(awk -v ver="$ver" ' /^## / { grab = ($2 == "Release" && $3 == "drill" && $4 == "—" && $5 == ver); next } grab { print } ' "$runs" | sed '/./,$!d')" if [ -z "$record" ]; then { echo "drill-recorded: $runs has no drill record for version '$ver'." echo echo " A release PR's version must have a NON-EMPTY section headed:" echo echo " ## Release drill — $ver — YYYY-MM-DD" echo echo " (An empty section under a correct heading counts as no record. The" echo " heading matches the version WHOLE — a '$ver-rc1' section does not" echo " satisfy '$ver', and vice versa.)" echo echo " To unblock, either:" echo " * run the drill and record it — the legs (team, apply, idempotent" echo " diff, smoke, inventory, emit-draft, fleet, destroy, read-only" echo " guard), the numbers, and what failed; or" echo " * record an explicit maintainer WAIVER for this version in $runs," echo " saying who waived it and what is untested." echo echo " The waiver is allowed on purpose: this gate requires a RECORD, not a" echo " passing result, so shipping without a drill stays possible — and" echo " stays a deliberate, reviewable commit instead of an oversight." } >&2 exit 1 fi echo "drill-recorded: $runs carries a drill record for $ver ($(printf '%s\n' "$record" | grep -c .) line(s))"