|
All checks were successful
CI / test (pull_request) Successful in 1m30s
CI / release-exercise (pull_request) Successful in 10s
CI / self-guards (pull_request) Successful in 7s
CI / action-exercise (pull_request) Successful in 6s
CI / docs-sync-exercise (pull_request) Successful in 6s
labels / labels (pull_request) Successful in 8s
@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 |
||
|---|---|---|
| .. | ||
| 0.1.0.md | ||
| 0.2.0.md | ||
| 0.3.0.md | ||
| 0.4.0.md | ||
| 0.4.1.md | ||
| README.md | ||
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
-
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}witharchived: true, inside thereposcope every fleet identity holds), and deleting it is the operator's step —delete_repois 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 instate:buildingre-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 outsideheavy-duty/ceremony's ref namespace — the namespace the "never a branch named like the tag" rule below protects. -
Install the docs/CONSUMERS.md caller stubs, pinned to a fork ref carrying the release candidate tree. The candidate's
CEREMONY_SELF_REFis 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/ceremonyto 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. -
Give it a fixture
VERSION/CHANGELOG.md/changelog.d//drills/in the armed state (X.Y.Z-dev, the fragments directory with itsREADME.mdmarker plus at least one fragment for the ceremony to consume). -
Exercise both doors, one probe at a time:
- a merge-door ceremony publishes exactly one release and re-arms main
to
-dev; - a mislabeled ordinary PR is a green NOTICE no-op;
- a bare-version PR without the
releaselabel refuses; - a re-run of the completed ceremony refuses;
- a tag-door release from a manual tag;
- 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.
- a merge-door ceremony publishes exactly one release and re-arms main
to
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.
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.
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
- 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.
- 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. - 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. Acurlfrom 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. - 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.
- 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}/dispatchesworks on this instance with a valid ref and inputs. Measured so far:GET /actions/workflows404s 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.