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:
parent
750d93deda
commit
11dfeb2ecd
8 changed files with 344 additions and 276 deletions
138
.github/scripts/drill-recorded.sh
vendored
138
.github/scripts/drill-recorded.sh
vendored
|
|
@ -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))."
|
||||
|
|
|
|||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
113
drills/README.md
Normal 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.
|
||||
212
test/release.sh
212
test/release.sh
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue