2026-07-22 22:19:50 +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
|
|
|
|
|
|
2026-07-24 13:16:42 +00:00
|
|
|
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 step** — `delete_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.
|
2026-07-24 11:51:55 +00:00
|
|
|
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.
|
2026-07-22 23:40:38 +00:00
|
|
|
|
2026-07-24 11:51:55 +00:00
|
|
|
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.
|
2026-07-24 10:24:31 +00:00
|
|
|
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).
|
2026-07-22 22:19:50 +00:00
|
|
|
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
|
2026-07-24 13:16:42 +00:00
|
|
|
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.
|
2026-07-22 22:19:50 +00:00
|
|
|
|
2026-08-03 21:41:34 +00:00
|
|
|
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).
|
|
|
|
|
|
2026-07-22 22:19:50 +00:00
|
|
|
`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.
|
docs(drills): the standing runner-probe venue, and why it is not a drill (#202)
@andres ruled option A (#5631): one standing never-archived repo. This is the
runbook half.
The distinction the document exists to make: a drill is disposable by design
and ends with the builder archiving it. This venue is the opposite — it exists
so that runner-only facts can be measured on demand, and archiving it defeats
the purpose. That is not hypothetical: all three drill repos were archived
correctly, by the rule, and each then had to be un-archived or replaced. The
request came three times in two days across #192 and #198 and never became
anything.
What the runbook pins, all of it measured rather than asserted:
* a probe MUST run as an Actions job under ${{ github.token }} — the same
DELETE answers 500 there and 204 under a PAT, so a probe run any other way
produces a confident wrong answer;
* probe results are written into the forge, not left in a job log, because
logs age out and #192's run 701 survived only because it wrote into an
issue;
* no probe touches ceremony's own board — the venue exists so the live board
is not the fixture;
* the three probes it already owes (#192's live label lift, #205's dispatch
measurement, a 0.6.0 consumer exercise after #198).
STANDING THE REPO UP IS THE OPERATOR'S STEP, and this is the part I could not
do rather than the part I chose not to. Measured today with this identity:
POST /api/v1/orgs/heavy-duty/repos -> 403 not allowed in organization
POST /api/v1/user/repos -> 201 personal namespace only
Same shape as the drill delete: a deliberate boundary, not a misconfiguration.
The runbook says so, says not to retry it, and says not to work around it by
using a personal namespace where the org's runner and secrets do not reach.
test/run.sh 22/22, shellcheck 0.10.0, actionlint, self-ref all clean.
Refs #202
2026-08-05 13:16:20 +00:00
|
|
|
|
|
|
|
|
# The runner-probe venue
|
|
|
|
|
|
|
|
|
|
**Not a drill.** A drill rehearses the release doors on a disposable repo and
|
|
|
|
|
ends. This is the opposite shape: one **standing** repo that exists so that
|
|
|
|
|
runner-only facts can be measured on demand, and it is **never archived**.
|
|
|
|
|
|
|
|
|
|
`heavy-duty/ceremony-runner-probe` — private, standing, reset between probes.
|
|
|
|
|
Ruled by the operator as option (A) of ceremony#202 (#5631).
|
|
|
|
|
|
|
|
|
|
## Why a standing repo, when drills are disposable
|
|
|
|
|
|
|
|
|
|
Some facts are only true inside Actions, under the token Actions injects, and
|
|
|
|
|
no local harness or PAT can reproduce them. The worked example is ceremony#192:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
DELETE /issues/{n}/labels/{id} -> 500 under ${{ github.token }} in a workflow
|
|
|
|
|
DELETE /issues/{n}/labels/{id} -> 204 under a maintainer PAT, same call
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A probe that runs anywhere else passes and proves nothing. Before this venue
|
|
|
|
|
existed the answer was "un-archive a drill repo", which was requested three
|
|
|
|
|
times in two days across two issues and never became anything — the three
|
|
|
|
|
drill repos (`ceremony-drill-0.4.1`, `-0.4.1-final`, `-191`) are all archived,
|
|
|
|
|
and each was minted for one probe and then wanted again.
|
|
|
|
|
|
|
|
|
|
## The disposal rule above does NOT apply here
|
|
|
|
|
|
|
|
|
|
The rehearsal section says the builder archives the scratch repo and the
|
|
|
|
|
operator deletes it. **That rule is for drills.** Archiving this repo defeats
|
|
|
|
|
its entire purpose, and it is the failure mode the three archived drill repos
|
|
|
|
|
demonstrate — each was archived correctly, by the rule, and each then had to be
|
|
|
|
|
un-archived or replaced.
|
|
|
|
|
|
|
|
|
|
So: never archive it, never delete it, and if you find it archived, un-archive
|
|
|
|
|
it rather than minting a fourth one.
|
|
|
|
|
|
|
|
|
|
## Standing it up is the operator's step
|
|
|
|
|
|
|
|
|
|
Bot identities cannot create repositories in `heavy-duty`. Measured
|
|
|
|
|
2026-08-05 with a fleet identity holding the `repo` scope:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
POST /api/v1/orgs/heavy-duty/repos -> 403 "not allowed to create repository in organization"
|
|
|
|
|
POST /api/v1/user/repos -> 201 (personal namespace only)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This is the same shape as the drill delete: a deliberate permission boundary,
|
|
|
|
|
not a misconfiguration, and the same rule applies — do not retry it, and do not
|
|
|
|
|
work around it by putting the venue in a personal namespace, where the org's
|
|
|
|
|
runner and secrets do not reach. Ask the operator.
|
|
|
|
|
|
|
|
|
|
## Running a probe
|
|
|
|
|
|
|
|
|
|
1. Reset the repo to a clean state — the probe's own fixtures only, no
|
|
|
|
|
leftovers from the last one. A probe that inherits state is a probe whose
|
|
|
|
|
result you cannot attribute.
|
|
|
|
|
2. Install whatever the probe needs: a workflow that exercises the call, and
|
|
|
|
|
`docs/CONSUMERS.md`'s caller stubs if the probe is about the reconcilers.
|
|
|
|
|
3. **Run it as an Actions job under `${{ github.token }}`.** This is the whole
|
|
|
|
|
point of the venue and the one step that cannot be shortcut. A `curl` from a
|
|
|
|
|
laptop with a PAT answers a different question — see the 204/500 split
|
|
|
|
|
above — and a probe run that way is worse than no probe, because it produces
|
|
|
|
|
a confident wrong answer.
|
|
|
|
|
4. Write the results **into the forge** — an issue or a comment in this repo —
|
|
|
|
|
rather than leaving them in a job log. Logs age out; ceremony#192's run 701
|
|
|
|
|
survived because the job wrote its findings into an issue it created.
|
|
|
|
|
5. Record the run number and the repo in whatever issue the probe serves.
|
|
|
|
|
|
|
|
|
|
## What must never happen here
|
|
|
|
|
|
|
|
|
|
No probe touches `heavy-duty/ceremony`'s board. No labels, no comments, no
|
|
|
|
|
runs attributable to a probe. The venue exists so that the live board does not
|
|
|
|
|
have to be the test fixture.
|
|
|
|
|
|
|
|
|
|
## The probes this venue owes
|
|
|
|
|
|
|
|
|
|
- **ceremony#192** — that the repaired sweep actually lifts a label under the
|
|
|
|
|
workflow token, which is the half its acceptance criteria cannot get from
|
|
|
|
|
the hermetic contract tests.
|
|
|
|
|
- **ceremony#205** — whether `POST /actions/workflows/{file}/dispatches`
|
|
|
|
|
works on this instance with a valid ref and inputs. Measured so far:
|
|
|
|
|
`GET /actions/workflows` 404s and the dispatch route answers 500 rather than
|
|
|
|
|
a 4xx, which is not enough to port against.
|
|
|
|
|
- A 0.6.0 consumer exercise once ceremony#198 has merged.
|