cast/drills/README.md

146 lines
6.8 KiB
Markdown
Raw Normal View History

refactor: one drill record per version, in drills/ Drill records move from sections inside drill/RUNS.md to one file per version: drills/<version>.md. The old guard parsed headings — em-dash field matching, an optional ' — DATE' tail, a whole-version comparison so 0.2.0-rc1 could not satisfy 0.2.0, a '(NF == 5 || $6 == dash)' tail constraint to match box, and a non-blank body rule. All of that existed only because records shared one file, and this repo shipped two defects out of the complexity in review: the sed '/./,$!d' whitespace bypass, and heading-grammar drift from box's stricter form. One file per version makes nearly all of it unrepresentable — 0.2.0.md and 0.2.0-rc1.md are simply different files, so the whole-version rule is the filesystem's rather than a comparison anyone can get wrong. One rule survives: a file of only whitespace is not a record. Plain drills/, not .drills/ — dot-directories are invisible to globs without dotglob, the cause of #118/#121 here and box#116. drill/RUNS.md is deleted. It was created in this same unmerged PR and held only format documentation, no real records; the useful reasoning moves to drills/README.md. (box keeps ITS drill/RUNS.md, a genuine harness log with real run history.) The docs also drop an over-constrained ordering claim: the three repos' drills are INDEPENDENT, run in any order and any sitting. What makes that safe is that each pins the same fixed set of candidate refs — and that pinning, not sequencing, is what dissolves the box/rig recursion, since refs are static identifiers that exist as soon as the release branches do. Each repo also drills a different thing: box the isolation contract, rig convergence, cast promotion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:27:09 +00:00
# Drills
Per-release evidence: **one file per version**, named `<version>.md`, where
`<version>` matches `package.json`'s `version` exactly — `0.2.0` is recorded in
`0.2.0.md`, `0.2.0-rc1` in `0.2.0-rc1.md`.
A release PR's version must have its file here, holding at least one
non-whitespace character, before CI will let it merge
(`.github/scripts/drill-recorded.sh`, wired into ci.yml).
## One file per version, and why the parser went away
Records used to share a single `drill/RUNS.md`, which meant the gate had to
*parse* it: a heading grammar, an em-dash field match, an optional ` — DATE`
tail, a whole-version comparison, a non-blank-body rule. That machinery existed
only because records shared a file — and it shipped two defects in review: a
`sed '/./,$!d'` extraction where `.` matches a space (so a heading followed by
one tab satisfied the gate), and heading-grammar drift from box's stricter form.
One file per version makes nearly all of that unrepresentable. `0.2.0.md` and
`0.2.0-rc1.md` are different files, so the whole-version rule is the
filesystem's rather than a comparison anyone can get wrong, and there is no
heading to drift. One rule survives: a file of only whitespace is not a record.
The directory is plain `drills/`, **not** `.drills/`. Dot-prefixed directories
are invisible to globs without `dotglob` — the blind spot behind #118, #121 and
box#116.
## What a record should contain
- **What ran** — which legs, against which manifest.
- **On what host** — the instances, their versions, who operated it.
- **The pinned candidate refs** — box, rig and cast SHAs under test.
- **The numbers** — counts, elapsed time, whatever the legs emit.
- **What failed** — and "nothing failed" is itself a finding worth a line.
**A failed drill is still a valid record.** The gate wants evidence, not
success. A maintainer may ship on a failed or partial drill — what they may not
do is ship on silence, so a **waiver** is also a legitimate record: say who
waived it, why, and what is untested. Requiring a record makes skipping a
deliberate, reviewable commit instead of the default outcome of forgetting.
## This directory is the record, not the instrument
cast has **no drill harness script of its own**. Its legs run by hand against
the documented procedure: two live Coolify instances and the full A→B
promotion —
team → apply → diff (idempotent) → smoke → inventory → emit-draft →
fleet → destroy → read-only guard
A harness would make the run reproducible; it would not make it recorded. Those
are separate problems, and this directory is the second one.
## Per-repo, by construction
cast records **cast's own** legs. It does not read box's or rig's drill records
to decide whether cast may ship: a cross-repo lookup silently degrades to
"pass" the moment it fails to resolve — the unreadable-rollup class of bug,
where a guard that cannot read its input reports the happy answer.
**The three repos' drills are independent.** Run them in any order, on any
schedule, in separate sittings. They are not phases of one script.
What makes that safe is that every drill **pins the same fixed set of candidate
refs** (`RIG_REPO` / `RIG_REF` are mint-time variables, default
`heavy-duty/rig@main`), so each drill exercises exactly the combination that
will ship rather than whatever `main` happens to be that afternoon. The run
drills **candidate refs, not released artifacts**.
That pinning — **not sequencing** — is what dissolves the box↔rig recursion.
box and rig *are* mutually recursive: rig builds the host that runs box, and
box's seed calls rig back to converge the guest. But candidate 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. No repo has to be released before another can be drilled, and there is
**no fixed order in which the three releases must be published**.
Each repo also drills a **different thing**: box asserts the isolation contract
(the VM trust boundary), rig asserts convergence (a machine reaches its role,
idempotently), cast asserts promotion (A→B reproduces, and the diff is
idempotent). Three different exercises sharing a substrate — which is exactly
why the records are per-repo.
cast's legs are the **least coupled** of the three. Two Coolify instances can
be stood up by hand; the July drill did exactly that for instance B, via a
parameterised compose file. Nothing about cast's drill requires box or rig to
have been drilled first, or at all, on that day.
Within a single drill you obviously bring the substrate up before probing it —
a host before a guest before Coolify. That is how you run *a* drill, not an
ordering rule *between repos*.
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.
**Drilling the candidate is drilling the release.** A release PR's diff is the
version file and `CHANGELOG.md` — nothing executable differs between the tree
that was drilled and the tree that ships, so the evidence carries across the
ceremony commit.
Each record cites the shared **run ID** naming the pinned set, plus the other
two repos' SHAs — which is what lets separate records be reassembled into one
picture, while each repo's evidence still lives in its own tree.
## Worked example
Illustrative only. The version below is a **placeholder that can never collide
with a real release** — a realistic-looking version here would be a real record
for it, and the gate would wave that release through on documentation. The
mechanism changed with the move to one file per version; the caution did not.
`drills/9.9.9.md`:
```markdown
# Release drill — 9.9.9 — YYYY-MM-DD
Run ID: `drill-YYYYMMDD-NN` (names the pinned candidate set).
Stack under test: box `<sha>`, rig `<sha>`, cast `<sha>` — candidate refs,
pinned at mint time via RIG_REPO/RIG_REF.
Instances: A `coolify-a.example` (v4.x), B `coolify-b.example` (v4.x),
B stood up by hand from the parameterised compose file.
Manifest: `examples/two-env.yaml`, 3 applications, 2 environments.
Operator: @maintainer. Elapsed: 41m.
| Leg | Result | Notes |
|---|---|---|
| team | pass | 2 teams, 4 members reconciled |
| apply (A→B) | pass | 3 apps created, 11 env vars set |
| diff (idempotent) | pass | second apply: 0 changes |
| smoke | pass | 3/3 endpoints 200 |
| inventory | pass | 3 apps, 2 envs, matches manifest |
| emit-draft | pass | draft matches inventory round-trip |
| fleet | pass | both instances listed, versions read |
| destroy | pass | 3 apps removed, absence asserted |
| read-only guard | pass | write refused against B with the guard on |
Failures: none. One rough edge: `smoke` needed a 20s retry window on B —
filed as #NNN, not a release blocker.
```
## Records
*None yet.* cast has recorded no drill runs.