ceremony-ci-probe/actions/drill-recorded/drill-recorded.sh
claude-bot-andresmgsl 71c45b7fe5 feat: actions/drill-recorded — a release carries its evidence
Port box's drill-recorded.sh (the origin of the three copies) as a
composite action on the #5 pattern: version read through lib/version.sh,
one file per version under drills/, whitespace-only records refused.
Keyed on the tree's version — -dev trees pass with a notice that says
why nothing was asserted; bare trees must show drills/<version>.md with
at least one non-whitespace character.

Closes #7

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:28:06 +00:00

141 lines
7.2 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
# drill-recorded.sh [<drills-dir>] [<version-source>] — assert that a
# RELEASE tree carries a drill record: that the ritual this repo says a
# release rests on was actually run for this version, and written down as
# <drills-dir>/<version>.md.
#
# Ported from box .github/scripts/drill-recorded.sh (the origin); rig and
# cast carry their own rephrasings of the same rule, and this action is the
# one copy they all converge on (#13#16).
#
# Why it exists: box's CONTRIBUTING said since box#96 that "this PR is where
# the release ritual hangs: the full drill on real hardware, recorded". No
# release ever did it. box#95, box#114 and box#148 all shipped as a VERSION
# bump plus a CHANGELOG.md stamp and nothing else. That is three releases
# through the same gap, because the gate was a sentence in a document and
# the only thing standing on it was a reviewer remembering to ask. A
# reviewer finally did — which is the point: the ONE time it was caught is
# the time somebody happened to look, and that is not a gate, it is luck
# with good manners. So the rule moves into CI, where it fires on every
# release PR whether or not anyone is paying attention.
#
# It is keyed on the tree's version for the same reason changelog-armed is —
# the two states are genuinely different:
#
# version ends in -dev -> PASS. A development tree ships nothing, so
# there is nothing for it to have proven. Almost
# every PR in a consumer repo is this case, and
# a guard that nagged all of them would be
# turned off.
# version is bare -> the ceremony tree, the one about to ship.
# <drills-dir>/<version>.md MUST exist and carry
# at least one non-whitespace character.
#
# ONE FILE PER VERSION, and that is the whole design. Records used to be
# sections sharing one file, and every hard edge the old guard had existed
# only because of that sharing: em-dash field matching, an optional
# ' — DATE' tail, whole-version comparison so '0.9.0-rc1' could not satisfy
# '0.9.0', avoiding '\x' escapes because CI runs mawk not gawk, and a
# non-blank body rule to tell an empty section from a filled one. Two
# separate defects were found in review because of that complexity — a
# `sed '/./,$!d'` whitespace bypass, and heading-grammar drift from the
# sibling repos. Splitting the file makes almost all of it UNREPRESENTABLE:
# '0.9.0.md' and '0.9.0-rc1.md' are simply different files, so whole-version
# matching is free rather than a trap, and there is no grammar left to
# drift.
#
# The directory is plain 'drills', NOT '.drills'. A dot-directory is
# invisible to a glob without dotglob, which is exactly what caused box#116
# and box#118; evidence a sweep cannot see is evidence that goes missing
# quietly.
#
# What this guard asserts is a RECORD, deliberately — not a passing drill.
# CI cannot run a consumer's drill: box's wants real hardware, a real Incus,
# and the better part of an hour. What CI can do is refuse to let a release
# claim a ritual it left no evidence of. That also leaves the maintainer
# waiver intact and honest: a release that must ship without a full drill
# records WHY in its own file, which is a deliberate, reviewable commit in
# the diff — rather than the silent skip that let box#95, box#114 and
# box#148 all ship unproven.
#
# What a drill MEANS is the consumer's business (box: the isolation
# contract; rig: convergence; cast: promotion; each names it in its own
# drills/README.md). This guard only ever reads the record file in the tree
# it runs in.
#
# A file of its own (not inlined in action.yml) so
# test/drill-recorded.test.sh can drive it against constructed trees for
# both states — the same discipline as the libs it sources.
drills="${1:-${DRILLS_DIR:-drills}}"
version_source="${2:-${VERSION_SOURCE:-file}}"
# The shared libs travel with this action: a consumer's
# `uses: heavy-duty/ceremony/actions/drill-recorded@<tag>` downloads this
# whole repository at that ref, so ../../lib is always present and always at
# the same ref — no checkout step, no version skew possible.
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/version.sh
. "$here/../../lib/version.sh"
# A missing or empty version source is an ERROR, never a silent pass. A
# guard that cannot read the version cannot know whether this tree is its
# business, and "could not tell" must not resolve to "allowed". version_read
# refuses loudly on its own; the wrapper line names the guard so a workflow
# log shows which check refused.
ver="$(version_read "$version_source")" || {
echo "drill-recorded: cannot read the version (version-source: $version_source)" >&2
exit 1
}
# version_is_dev is the single definition of the -dev special case (#3): an
# rc is a pre-release, not a dev tree — it ships, so it needs its own record
# ('2.0.0-rc1.md'), which one-file-per-version makes just another path.
if version_is_dev "$ver"; then
# Nothing to assert, and saying so is the point: the operator reading a
# green log should be able to tell "the guard passed" from "the guard
# decided this tree was not its business".
echo "drill-recorded: version '$ver' is a development tree — nothing to assert; only ceremony trees ship"
exit 0
fi
record="$drills/$ver.md"
# The one rule that survives the rewrite, and it survives because it was
# never really about heading parsing: a file of only spaces, tabs and
# newlines is NOT a record. The first cut of box's guard extracted with
# `sed '/./,$!d'`, where `.` matches a space — so a record whose body was
# one tab satisfied a guard that promised "at least one non-blank line". An
# evidence-free release for the price of an invisible character, on the one
# check whose entire job is to demand evidence. Existence alone is a weaker
# claim than `touch` can defeat, so existence alone is not the test.
if [ ! -f "$record" ] || ! grep -q '[^[:space:]]' "$record"; then
cat >&2 <<EOF
drill-recorded: the version is '$ver' — a release — and there is no drill
record for it. The file this looks for is:
$record
...with something written in it. Either the file is absent entirely, or it
is present and blank; both mean the same thing, which is that this release
is asserting a ritual it has left no evidence of — this release is
unproven.
The unblock is to RUN THE DRILL and record it at that path — what it
measured, what it found, what it cost. See $drills/README.md for what a
drill means in this repo and what a record should contain. CI cannot run
the drill for you; it can only refuse a release that never ran one.
If this release must ship without a full drill, that is a maintainer's
call to make and it is still recorded: create the same file and say
plainly that the drill was WAIVED and why. The guard requires a record,
not a passing result — a failed drill honestly written down satisfies it
too — so a skip is a visible, reviewable file in the diff rather than the
silent gap that let box#95, box#114 and box#148 all ship unproven.
EOF
exit 1
fi
echo "drill-recorded: version '$ver' has a drill record at $record"