ceremony/drills
cluade-reviewer-andresmgsl e035130f65
Some checks failed
CI / test (pull_request) Failing after 33s
CI / release-exercise (pull_request) Successful in 12s
CI / self-guards (pull_request) Successful in 6s
CI / action-exercise (pull_request) Successful in 6s
CI / docs-sync-exercise (pull_request) Successful in 6s
Refs guard / refs-not-closing (pull_request) Failing after 5s
labels / labels (pull_request) Successful in 43s
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198)
`git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`.
18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and
`lib/forge-forgejo.sh` conflict in none and come out byte-identical.

The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF
carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md`
keep this forge's; CHANGELOG keeps both sides and names the upstream commit
this tree carries.

The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole
functions to files this tree already owned, so `git merge` took its side
without raising a conflict — and with them, EIGHT runtime `gh` call sites
that #188 had removed. Seven are ported onto the shim: two reads and four
comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT
read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot
declare a client for and whose Forgejo equivalent this instance answers with
500 rather than a 4xx — named with its reason rather than ported on a guess.

test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the
diff could not: four reviewers reading it each found a different subset, and
the contract suite stubs `gh`, so a reintroduced call site passes it.

Three seams the resolution decides are silent when resolved wrongly, and each
now has a case that fails on the wrong one: the merged record's `merged_at`
third column (without it every sort key ties and the highest PR number comes
back), the open gather's one-BODY-row-per-line feed (a whole decoded body as
one record loses every declaration including the first), and the whole-board
read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged.

The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would
drop every `Closes #N` link on the open side and reclaim a claim the PR was
holding — the existing base64 round-trip case is red without it.

actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather
is GraphQL, which Forgejo does not serve at all. #199 ports it.

test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean.

Refs #198
2026-08-05 11:56:23 +00:00
..
0.1.0.md drill: the 0.1.0 record, and the first-release caveat in the doctrine 2026-07-22 23:40:38 +00:00
0.2.0.md docs: record archived drill repository 2026-07-24 12:16:31 +00:00
0.3.0.md docs: record 0.3.0 live drill 2026-07-24 17:12:55 +00:00
0.4.0.md docs: record 0.4.0 live drill 2026-07-29 10:58:28 +00:00
0.4.1.md Merge main into release/0.4.1 — carry #191's doors and #194's drill record 2026-08-04 16:00:42 +00:00
0.5.0.md drill: probe 5 states suite coverage, not live dogfood 2026-08-03 12:28:40 +00:00
0.6.0.md drill 0.6.0: probes 5 and 6, the setup corrections, and the disposal state 2026-08-05 09:14:55 +00:00
README.md docs: define doors-unchanged drill records 2026-08-03 21:41:34 +00:00

Drills

What a drill means in this repo: an end-to-end rehearsal of both doors of the release workflow on a disposable repo. The contract suite proves every decision offline — facts → decide → notes against fixtures, the merge door's step sequence replayed in release-exercise.yml — but the doors themselves only ever run live: gating on a real push event, the tag create, the publish, the -dev re-arm (release.yml's "what is honestly untested"). The drill is where they run live before a version rests on them.

The rehearsal

  1. Create a scratch private repo. It is disposable by design — but the disposal is split, because the builder cannot perform the delete: at the end the builder archives it (PATCH /repos/{owner}/{repo} with archived: true, inside the repo scope every fleet identity holds), and deleting it is the operator's stepdelete_repo is deliberately absent from bot tokens, fleet doctrine and not a misconfiguration, so no builder that will ever run a drill can do it. Do not retry the delete and do not wait on it: both 0.2.0 drills ended at that wall independently (#135) — one builder held its release draft in state:building re-trying a 403 that cannot succeed, the other wrote a record asserting a delete that had not happened. Cleanup gates nothing — not ready-for-review, not the review panel, not the merge. The archived leftover is safe to leave: private, no consumers, and outside heavy-duty/ceremony's ref namespace — the namespace the "never a branch named like the tag" rule below protects.

  2. Install the docs/CONSUMERS.md caller stubs, pinned to a fork ref carrying the release candidate tree. The candidate's CEREMONY_SELF_REF is by construction the tag this release has not created yet, so the consumer path cannot resolve directly from the candidate. Rewrite that pin to a canonical candidate SHA in every carrier on the fork ref, and record the fork ref and rewritten pin in the drill record.

    Never create a branch named like the tag on heavy-duty/ceremony to paper over this deadlock: it would shadow the tag for every consumer until someone remembers to delete it. The 0.1.0 drill (#11) is the worked example of this standing fork-ref shape.

  3. Give it a fixture VERSION / CHANGELOG.md / changelog.d/ / drills/ in the armed state (X.Y.Z-dev, the fragments directory with its README.md marker plus at least one fragment for the ceremony to consume).

  4. Exercise both doors, one probe at a time:

    1. a merge-door ceremony publishes exactly one release and re-arms main to -dev;
    2. a mislabeled ordinary PR is a green NOTICE no-op;
    3. a bare-version PR without the release label refuses;
    4. a re-run of the completed ceremony refuses;
    5. a tag-door release from a manual tag;
    6. a mismatched tag refuses.

    Every refusal must refuse creating nothing — a probe that leaves a tag or a release behind on a refusal path is a failed probe.

The record

One file per version, drills/X.Y.Z.md — the shape the siblings use: what was run, where, the result of each probe, failures written down plainly. The record is the evidence; the scratch repo is the evidence's scaffolding. The record names the scratch repo by full owner/name and states its disposal state as its author observed it when the record was written — archived and pending the operator's delete, or deleted only if the author genuinely performed the delete. Never a disposal the author did not observe: the record is the only thing that survives the drill, and 0.2.0's record shipped its first draft asserting a cleanup that had not happened (#135) — false evidence in the one file whose job is to be evidence.

A record has one of three shapes. A rehearsal records the disposable-repo run above. Doors unchanged records the mechanically checked claim below when a new rehearsal would execute the same bytes as the last one. WAIVED records a maintainer's judgement under the standing paragraph below. If the doors-unchanged conditions do not all hold, the release owes a rehearsal or a waiver; the narrower shape is never a substitute for either.

Doors unchanged

The builder may assert that no disposable-repo rehearsal is owed only when all three conditions below hold at the candidate head. The release PR's panel verifies the claim like any other evidence, and if any reviewer rules a full drill owed, that verdict wins.

  1. git diff <last-rehearsed-tag>..HEAD -- <release-path> contains no change except the CEREMONY_SELF_REF pin line in .github/workflows/release.yml.
  2. The release path is exactly the output of .github/scripts/release-path.sh: .github/workflows/release.yml, bin/, lib/version.sh, lib/decide.sh, lib/facts.sh, and lib/changelog.sh. The script is the record author's copy-paste source; its contract test keeps this inline list and the workflow's direct and transitive dependencies in agreement.
  3. The last rehearsed tag's own record is a full rehearsal, its release is published, and main was re-armed to -dev after it.

The baseline is the last rehearsed tag, never merely the previous tag. A previous-tag baseline could chain one doors-unchanged assertion from another while the doors drift a small diff at a time; the last-rehearsed anchor makes any accumulated release-path change force a new rehearsal.

The record carries all three measurements as observed at its candidate head, never copied from an earlier record. drills/0.4.1.md and drills/0.5.0.md are the worked examples; the latter's amendment from a predicted empty lib/ diff to the observed lib/ruling.sh delta is why each candidate is measured afresh (#233). Re-running its stricter baseline now is also the path-enumeration proof: git diff 0.4.0 0.5.0 -- <release-path> is only the CEREMONY_SELF_REF pin, while adding lib/ruling.sh makes the diff non-empty even though neither release door reads that file (#217, #237).

actions/drill-recorded refuses any bare-version tree whose record is missing or blank. A waived drill is still a record: the file says WAIVED and why — a maintainer's call, visible and reviewable in the release PR's diff, never a silent skip.