CONTRIBUTING has always asked for the full real-hardware drill on a release. Nothing asserted it, so it was performed exactly as often as a reviewer remembered to ask — which is never, across every release in the family, until a reviewer bot finally blocked on it. The gate moves out of memory and into the tree. drill/RUNS.md is cast's own run log, starting empty: no fabricated history, and an honest note that cast has no drill harness script yet — its legs are run by the documented procedure. The file is the record, not the instrument. .github/scripts/drill-recorded.sh reads package.json and asserts that a bare version has a non-empty '## Release drill — X.Y.Z' section. A -dev tree has no ship claim and passes trivially. The version is matched WHOLE via awk field equality, release-notes.sh's fix for the same trap: 0.2.0 is not satisfied by 0.2.0-rc1, or the reverse. It requires a RECORD, not a PASS. A maintainer waiver is legal and is itself a section in drill/RUNS.md, so skipping the drill stays possible and stays a deliberate, reviewable commit rather than an oversight. The drill itself is ONE orchestrated run over the whole stack: rig bootstraps a bare host and installs box, box new mints a seed, the seed calls rig back to converge, and cast's legs run on the result. rig sits below box and above it, so the repos are mutually recursive rather than 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, and drilling the candidate is drilling the release — a release diff is the version file and CHANGELOG.md, nothing executable. Each repo records its own legs from that run, citing the shared run ID and the other repos' SHAs. cast never reads box's or rig's drill log to decide whether cast may ship: a cross-repo lookup degrades to "pass" the moment it fails to resolve — the unreadable-rollup class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
134 lines
6.8 KiB
Bash
Executable file
134 lines
6.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# drill-recorded.sh [<runs-file>] [<version-file>] — 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 [<runs-file>] [<version-file>]" >&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 "<name>": "<range>"
|
|
# 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))"
|