ceremony/docs/RUNNER-PROBES.md

388 lines
22 KiB
Markdown
Raw Permalink Normal View History

docs(runner-probes): its own document, an arming procedure, and the evidence boundary made consistent (#202) @codex-reviewer-andresmgsl's four gaps. 1. BRANCH UPDATED TO CURRENT MAIN. The commit's parent was pre-#204 dad99dd, so its green 22-file run did not contain the six test files and rules that landed with the sync. Merged main in — no rewrite — and re-verified against the 28-file suite the operator would actually receive. 2. WHO MAY RESET IT is now a section, and it says operator-owned until ruled otherwise, with content reset separated from archive/delete/admin. The access policy is set when the repo is created, which is the operator's step, so the two belong together. Flagged for @andres rather than assumed. 3. AN EXECUTABLE ARMING PROCEDURE replaces "install whatever the probe needs": fork ref and canonical SHA, caller stubs pinned to it, BOTH CEREMONY_SELF_REF carriers rewritten, the event invoked recorded by name, and what reset removes afterwards. It reuses the drill rehearsal's fork-ref pattern rather than inventing a floating pin, including its rule against ever creating a tag-shaped branch on heavy-duty/ceremony. 4. THE EVIDENCE CONTRADICTION IS RESOLVED. "Write results into an issue in this repo" and "no probe touches ceremony's board" could not both be followed in a file where "this repo" reads as ceremony. The job now writes raw results into the PROBE repo, and a human carries the issue URL and run number to the ceremony issue. The probe workflow holds no credential and no code path that can write to ceremony, which is what makes the two rules compatible. Placement: the operational contract moves to docs/RUNNER-PROBES.md, with a short cross-link in drills/README.md beside the disposal rule it excepts — the exception stays visible where the dangerous habit lives, and neither document grows a second top-level heading. test/run.sh 28/28 on the updated branch; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed clean. Refs #202
2026-08-05 13:40:28 +00:00
# Runner probes
**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,
docs(runner-probes): rewrite coordinates not only refs, keep result issues, and stop asserting what was not measured (#202) @codex-reviewer-andresmgsl's three operational corrections. 1. ARMING REWRITES THE COORDINATE. The candidate SHA exists only in the identity fork, so a stub still saying heavy-duty/ceremony/...@<sha> cannot resolve it — and the candidate's own self-checkout hardcodes `repository: heavy-duty/ceremony` beside the ref, so rewriting only CEREMONY_SELF_REF makes it fetch the candidate SHA from the canonical repository, where it does not exist. Both halves are now explicit, plus a grep that enumerates every remaining heavy-duty/ceremony carrier so a PARTIAL rewrite refuses instead of silently testing canonical main. 2. RESULT ISSUES ARE NOT RESET SCOPE. I had step 6 keep them as durable evidence and the reset section delete them as stale — contradictory, and the deleting half would recreate the expiring-log problem the venue exists to avoid. Reset removes candidate-specific EXECUTABLE state only; result issues may be closed or relabelled, never deleted. 3. NO UNMEASURED CLAIMS. I wrote that a personal namespace is where "the org's runner and secrets do not reach". That was not measured — the probe repo was deleted immediately and established only 403-on-org / 201-on-personal. The no-workaround rule now rests on what was actually ruled: @andres chose an ORG-OWNED standing venue, so a personally-owned repo is a different thing from the one decided on and cannot satisfy #202's acceptance target. If runner reach matters, it gets measured once the venue exists. test/run.sh 28/28; shellcheck 0.10.0, changelog-armed clean. Refs #202
2026-08-05 13:46:03 +00:00
not a misconfiguration. Do not retry it, and do not work around it by putting
the venue in a personal namespace — **not because a personal namespace is
proven unable to reach the org's runner** (that was not measured; the probe
repository above was deleted immediately, so nothing about runner or secret
reach was established), but because @andres ruled an **org-owned standing
venue** (#5631). A personally-owned repo is a different thing from the one that
was decided on, and cannot satisfy #202's named acceptance target.
If runner or secret reach turns out to matter, measure it once the venue
exists rather than assuming it here.
docs(runner-probes): its own document, an arming procedure, and the evidence boundary made consistent (#202) @codex-reviewer-andresmgsl's four gaps. 1. BRANCH UPDATED TO CURRENT MAIN. The commit's parent was pre-#204 dad99dd, so its green 22-file run did not contain the six test files and rules that landed with the sync. Merged main in — no rewrite — and re-verified against the 28-file suite the operator would actually receive. 2. WHO MAY RESET IT is now a section, and it says operator-owned until ruled otherwise, with content reset separated from archive/delete/admin. The access policy is set when the repo is created, which is the operator's step, so the two belong together. Flagged for @andres rather than assumed. 3. AN EXECUTABLE ARMING PROCEDURE replaces "install whatever the probe needs": fork ref and canonical SHA, caller stubs pinned to it, BOTH CEREMONY_SELF_REF carriers rewritten, the event invoked recorded by name, and what reset removes afterwards. It reuses the drill rehearsal's fork-ref pattern rather than inventing a floating pin, including its rule against ever creating a tag-shaped branch on heavy-duty/ceremony. 4. THE EVIDENCE CONTRADICTION IS RESOLVED. "Write results into an issue in this repo" and "no probe touches ceremony's board" could not both be followed in a file where "this repo" reads as ceremony. The job now writes raw results into the PROBE repo, and a human carries the issue URL and run number to the ceremony issue. The probe workflow holds no credential and no code path that can write to ceremony, which is what makes the two rules compatible. Placement: the operational contract moves to docs/RUNNER-PROBES.md, with a short cross-link in drills/README.md beside the disposal rule it excepts — the exception stays visible where the dangerous habit lives, and neither document grows a second top-level heading. test/run.sh 28/28 on the updated branch; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed clean. Refs #202
2026-08-05 13:40:28 +00:00
## 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.
docs(runner-probes): arming is two layers, because a commit cannot contain its own SHA (#202) @codex-reviewer-andresmgsl found that the procedure was not executable as written, and the reason is structural rather than a wording slip. The candidate's workflows carry `repository: heavy-duty/ceremony` beside `ref: ${{ env.CEREMONY_SELF_REF }}`, so arming must rewrite them. But rewriting CREATES A NEW COMMIT, and a commit cannot embed its own object ID. So a single-layer arming is self-referential: pin the callers to the pre-rewrite SHA and they load the UNARMED workflows; pin them to the post-rewrite SHA and you are asking that commit to contain itself. My step 3 asked for exactly that. Two layers, stated as a table because the distinction is the whole thing: candidate code SHA the immutable tree under test — actions/, lib/ armed workflow SHA a child commit whose workflows point at the fork and whose CEREMONY_SELF_REF is the candidate code SHA And callers pin by layer, because they are not the same thing: composite actions to the candidate code SHA, reusable workflows to the armed SHA, which is the only revision whose inner checkout is rewritten. The completeness check becomes a mechanical non-zero gate — `git grep` for executable `uses:`/`repository:` carriers over the ARMED tree, exiting non-zero on any hit — rather than "every remaining hit must be prose". A partial rewrite does not announce itself: it silently tests canonical main. The result issue records both SHAs, not one, or a later reader cannot tell which tree answered. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 13:54:44 +00:00
2. **Arm it against the candidate** (below) — two layers, candidate code and
armed workflow — if the probe is about ceremony's own machinery rather than
about a bare API call.
docs(runner-probes): its own document, an arming procedure, and the evidence boundary made consistent (#202) @codex-reviewer-andresmgsl's four gaps. 1. BRANCH UPDATED TO CURRENT MAIN. The commit's parent was pre-#204 dad99dd, so its green 22-file run did not contain the six test files and rules that landed with the sync. Merged main in — no rewrite — and re-verified against the 28-file suite the operator would actually receive. 2. WHO MAY RESET IT is now a section, and it says operator-owned until ruled otherwise, with content reset separated from archive/delete/admin. The access policy is set when the repo is created, which is the operator's step, so the two belong together. Flagged for @andres rather than assumed. 3. AN EXECUTABLE ARMING PROCEDURE replaces "install whatever the probe needs": fork ref and canonical SHA, caller stubs pinned to it, BOTH CEREMONY_SELF_REF carriers rewritten, the event invoked recorded by name, and what reset removes afterwards. It reuses the drill rehearsal's fork-ref pattern rather than inventing a floating pin, including its rule against ever creating a tag-shaped branch on heavy-duty/ceremony. 4. THE EVIDENCE CONTRADICTION IS RESOLVED. "Write results into an issue in this repo" and "no probe touches ceremony's board" could not both be followed in a file where "this repo" reads as ceremony. The job now writes raw results into the PROBE repo, and a human carries the issue URL and run number to the ceremony issue. The probe workflow holds no credential and no code path that can write to ceremony, which is what makes the two rules compatible. Placement: the operational contract moves to docs/RUNNER-PROBES.md, with a short cross-link in drills/README.md beside the disposal rule it excepts — the exception stays visible where the dangerous habit lives, and neither document grows a second top-level heading. test/run.sh 28/28 on the updated branch; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed clean. Refs #202
2026-08-05 13:40:28 +00:00
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. **The job writes its raw results into an issue in the PROBE repo**
`heavy-duty/ceremony-runner-probe` — not into ceremony. Logs age out;
ceremony#192's run 701 survived only because the job wrote its findings
into an issue it created.
5. **A human then records the probe issue's URL and the Actions run number on
the ceremony issue the probe serves.** That hop is deliberate and is the
whole of the boundary: the probe workflow holds no credential and no code
path that can write to `heavy-duty/ceremony`, so "the probe reports its
findings" and "the probe cannot touch the live board" stay compatible
rather than contradicting each other (@codex-reviewer-andresmgsl, #202
review).
## Arming a candidate ref
A probe that exercises ceremony's own machinery needs the candidate tree
docs(runner-probes): arming is two layers, because a commit cannot contain its own SHA (#202) @codex-reviewer-andresmgsl found that the procedure was not executable as written, and the reason is structural rather than a wording slip. The candidate's workflows carry `repository: heavy-duty/ceremony` beside `ref: ${{ env.CEREMONY_SELF_REF }}`, so arming must rewrite them. But rewriting CREATES A NEW COMMIT, and a commit cannot embed its own object ID. So a single-layer arming is self-referential: pin the callers to the pre-rewrite SHA and they load the UNARMED workflows; pin them to the post-rewrite SHA and you are asking that commit to contain itself. My step 3 asked for exactly that. Two layers, stated as a table because the distinction is the whole thing: candidate code SHA the immutable tree under test — actions/, lib/ armed workflow SHA a child commit whose workflows point at the fork and whose CEREMONY_SELF_REF is the candidate code SHA And callers pin by layer, because they are not the same thing: composite actions to the candidate code SHA, reusable workflows to the armed SHA, which is the only revision whose inner checkout is rewritten. The completeness check becomes a mechanical non-zero gate — `git grep` for executable `uses:`/`repository:` carriers over the ARMED tree, exiting non-zero on any hit — rather than "every remaining hit must be prose". A partial rewrite does not announce itself: it silently tests canonical main. The result issue records both SHAs, not one, or a later reader cannot tell which tree answered. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 13:54:44 +00:00
reachable from a `uses:` line. This is the fork-ref shape `drills/README.md`
step 2 points at, written out — and it has **two layers**, which is the part
that is easy to get wrong and impossible to fix afterwards.
**Why two.** The candidate's own workflows contain
`repository: heavy-duty/ceremony` beside `ref: ${{ env.CEREMONY_SELF_REF }}`,
so they must be rewritten to point at the fork and at the candidate. But
rewriting them **creates a new commit**, and a commit cannot contain its own
object ID. A single-layer arming is therefore self-referential: pin the callers
to the pre-rewrite SHA and they load the *unarmed* workflows; pin them to the
post-rewrite one and you are asking a commit to embed itself
(@codex-reviewer-andresmgsl, #202 review).
So:
| layer | what it is | what it carries |
|---|---|---|
| **candidate code SHA** | the immutable tree under test | `actions/`, `lib/` — untouched |
| **armed workflow SHA** | a small child commit on top of it | workflows rewritten to the fork + `CEREMONY_SELF_REF` = the candidate code SHA |
### The procedure
1. **Push the candidate tree** to a fork under the identity that will run the
probe — one branch, `<identity>/ceremony@probe-<issue>` — and record its
SHA. Steps 1 and 2 advance the tip of that **same** branch; there are two
commits, not two branches. That is
docs(runner-probes): arming is two layers, because a commit cannot contain its own SHA (#202) @codex-reviewer-andresmgsl found that the procedure was not executable as written, and the reason is structural rather than a wording slip. The candidate's workflows carry `repository: heavy-duty/ceremony` beside `ref: ${{ env.CEREMONY_SELF_REF }}`, so arming must rewrite them. But rewriting CREATES A NEW COMMIT, and a commit cannot embed its own object ID. So a single-layer arming is self-referential: pin the callers to the pre-rewrite SHA and they load the UNARMED workflows; pin them to the post-rewrite SHA and you are asking that commit to contain itself. My step 3 asked for exactly that. Two layers, stated as a table because the distinction is the whole thing: candidate code SHA the immutable tree under test — actions/, lib/ armed workflow SHA a child commit whose workflows point at the fork and whose CEREMONY_SELF_REF is the candidate code SHA And callers pin by layer, because they are not the same thing: composite actions to the candidate code SHA, reusable workflows to the armed SHA, which is the only revision whose inner checkout is rewritten. The completeness check becomes a mechanical non-zero gate — `git grep` for executable `uses:`/`repository:` carriers over the ARMED tree, exiting non-zero on any hit — rather than "every remaining hit must be prose". A partial rewrite does not announce itself: it silently tests canonical main. The result issue records both SHAs, not one, or a later reader cannot tell which tree answered. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 13:54:44 +00:00
the **candidate code SHA**. Never create a branch on
`heavy-duty/ceremony` named like a tag: it shadows that tag for every
consumer until somebody remembers to delete it.
docs(runner-probes): the checker binds the manifest to its target, and the snippets lint clean standalone (#202) @codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY, which my "parse, lint clean" claim had never meant. 1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha> <armed-sha> and used none of them — SC2034 on all three, which is the same defect the linter and the reviewer found independently. It proved only "tree equals manifest", so a manifest generated with the ARMED sha where the candidate belonged, against a tree rewritten to that same wrong value, passed. Wrong-but-consistent is exactly what this gate exists to reject. Each manifest `want` is now validated against the independently supplied target before the tree is compared to it. 2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest" while the prose below correctly said to generate from the PRE-arming tree. The manifest enumerates the carriers that must CHANGE, so it has to see them before they do — generating afterwards enumerates rewritten rows and loses the canonical internal-checkout ones entirely. The generator's first parameter is <candidate-checkout> now, and says so. 3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and the first placement was itself invalid: SC1124, a directive may precede a complete command, not an individual case branch. The checker's mktemp gets a trap. Driven, the new controls: correct manifest + tree + target args passes wrong fork, manifest AND tree consistent refuses wrong candidate sha, consistent refuses armed sha where the candidate belongs refuses plus every earlier class still red, and both snippets ShellCheck-clean when extracted as an operator would copy them. test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:22:01 +00:00
2. **Write the manifest FIRST, from the pre-arming tree, then commit the
arming.** The manifest enumerates the carriers *that must change*, so it is
generated before they do — running it afterwards would enumerate
already-rewritten rows and lose the canonical internal-checkout ones
entirely (@codex-reviewer-andresmgsl, #202 review). In that same
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
fork branch rewrite, for **every** carrier the manifest below enumerates:
ceremony's own internal `repository:` checkouts → `<identity>/ceremony`, and
**every** `CEREMONY_SELF_REF` value → the **candidate code SHA** from step 1.
There were three self-ref carriers on `main` at the time of writing and the
count is not a constant — derive it, do not remember it
(@glm-reviewer-andresmgsl, @codex-reviewer-andresmgsl, #202 review). The
**consumer** checkouts (`${{ github.repository }}`) are left alone. Record
the resulting SHA: that is the **armed workflow SHA**.
docs(runner-probes): arming is two layers, because a commit cannot contain its own SHA (#202) @codex-reviewer-andresmgsl found that the procedure was not executable as written, and the reason is structural rather than a wording slip. The candidate's workflows carry `repository: heavy-duty/ceremony` beside `ref: ${{ env.CEREMONY_SELF_REF }}`, so arming must rewrite them. But rewriting CREATES A NEW COMMIT, and a commit cannot embed its own object ID. So a single-layer arming is self-referential: pin the callers to the pre-rewrite SHA and they load the UNARMED workflows; pin them to the post-rewrite SHA and you are asking that commit to contain itself. My step 3 asked for exactly that. Two layers, stated as a table because the distinction is the whole thing: candidate code SHA the immutable tree under test — actions/, lib/ armed workflow SHA a child commit whose workflows point at the fork and whose CEREMONY_SELF_REF is the candidate code SHA And callers pin by layer, because they are not the same thing: composite actions to the candidate code SHA, reusable workflows to the armed SHA, which is the only revision whose inner checkout is rewritten. The completeness check becomes a mechanical non-zero gate — `git grep` for executable `uses:`/`repository:` carriers over the ARMED tree, exiting non-zero on any hit — rather than "every remaining hit must be prose". A partial rewrite does not announce itself: it silently tests canonical main. The result issue records both SHAs, not one, or a later reader cannot tell which tree answered. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 13:54:44 +00:00
3. **Pin the probe repo's callers by layer**, because they are not the same
thing:
- composite-action callers →
`<identity>/ceremony/actions/<name>@<candidate-code-sha>`;
- reusable-workflow callers →
`<identity>/ceremony/.github/workflows/<file>@<armed-workflow-sha>`, since
that is the only revision whose inner checkout is rewritten.
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
4. **Gate the arming against a MANIFEST, byte for byte.** Every weaker shape
has a hole, and each of these was found in a published draft of this file
(@codex-reviewer-andresmgsl, #202 review):
| weaker check | what slips through |
|---|---|
| "the old literal is absent" | a carrier rewritten to the wrong fork, or to the *armed* SHA |
| "every extracted value equals X" | a carrier that **vanished** — nothing to compare |
| "each value is one of {fork, dynamic}" | a **role swap**: an internal checkout made dynamic, a consumer checkout pointed at the fork |
| "the SHA suffix matches" | `wrong-owner/ceremony/actions/foo@<right-sha>` |
| "known callers match" | an **unrecognised** caller, or none at all |
| "the owner and the sha are right for the kind" | a **layer swap**: `…/actions/x@<armed>` labelled a workflow caller satisfies both |
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
So the arming step **writes a manifest** — one line per carrier, `path`,
`kind`, `full expected value` — and the gate compares the tree's actual
carriers against it as a set. A deletion, a role swap, a wrong fork, a wrong
SHA, an extra carrier and a missing caller are then all the same kind of
failure: the sets differ.
**Generate it while arming**, from the tree you are arming, so the manifest
cannot drift from the repository:
docs(runner-probes): enumerate the real carriers, spare the consumer checkouts, and make the snippet run (#202) @codex-reviewer-andresmgsl's three, all verified against current main before fixing. 1. THERE ARE THREE SELF-REF CARRIERS, NOT TWO — labels-sweep.yml:52, labels.yml:51, release.yml:132. My `[ "$n" -eq 2 ]` came from the pre-upstream tree, so it would have REJECTED a correctly armed candidate and told the operator to rewrite two of three, leaving one workflow pinned to the tag. The gate enumerates from the tree now, with the derivation commands beside the table so the list is re-checked rather than trusted. 2. NOT EVERY `repository:` BELONGS TO THE FORK. Three are `${{ github.repository }}` — labels-sweep.yml:69, labels.yml:92, release-exercise.yml:72 — and they fetch the CALLER's repository. My loop required every one to equal the fork, which would have rewritten the consumer checkouts and quietly changed what the probe exercises. Internal self-checkouts (four) are asserted to be the fork; consumer checkouts are asserted to stay dynamic. 3. EACH CHECK IS BOUND TO THE TREE IT IS ABOUT — `git -C "$armed"` for the carriers, `git -C "$probe"` for the callers, instead of depending on the operator's current directory. And `mapfile` rather than `git grep | while … fail`: the loop ran in a pipeline subshell, so `fail` exited the subshell and the gate carried on. Collect first, validate after, under a declared `set -euo pipefail`. And the snippet is now executable rather than illustrative: placeholders became positional parameters, so it parses, is shellcheck-clean, and runs. Driven against the unarmed tree it refuses with `CEREMONY_SELF_REF=0.6.0` — a tag rather than the candidate SHA, which is exactly the case it exists to catch. Publishing a gate that could not run would have been the same defect one level up. Branch updated from merged main (e236318). test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:13:40 +00:00
```sh
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
#!/usr/bin/env bash
docs(runner-probes): the checker binds the manifest to its target, and the snippets lint clean standalone (#202) @codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY, which my "parse, lint clean" claim had never meant. 1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha> <armed-sha> and used none of them — SC2034 on all three, which is the same defect the linter and the reviewer found independently. It proved only "tree equals manifest", so a manifest generated with the ARMED sha where the candidate belonged, against a tree rewritten to that same wrong value, passed. Wrong-but-consistent is exactly what this gate exists to reject. Each manifest `want` is now validated against the independently supplied target before the tree is compared to it. 2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest" while the prose below correctly said to generate from the PRE-arming tree. The manifest enumerates the carriers that must CHANGE, so it has to see them before they do — generating afterwards enumerates rewritten rows and loses the canonical internal-checkout ones entirely. The generator's first parameter is <candidate-checkout> now, and says so. 3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and the first placement was itself invalid: SC1124, a directive may precede a complete command, not an individual case branch. The checker's mktemp gets a trap. Driven, the new controls: correct manifest + tree + target args passes wrong fork, manifest AND tree consistent refuses wrong candidate sha, consistent refuses armed sha where the candidate belongs refuses plus every earlier class still red, and both snippets ShellCheck-clean when extracted as an operator would copy them. test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:22:01 +00:00
# write-manifest <candidate-checkout> <probe-checkout> <fork> <code-sha> <armed-sha>
#
# Run against the PRE-ARMING tree and the UNPINNED probe: this records what
# each carrier must BECOME, so it has to see them before they change.
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
#
# `|| true` on every extraction, for the same reason the checker needs it:
# git grep exits 1 on no-match and `set -e` would abort BEFORE the manifest
# is written — silently, which is how the first version of this generator
# produced no file and no diagnostic when a probe exercised only one layer
# (@codex-reviewer-andresmgsl). A probe need not use both.
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
set -euo pipefail
candidate="$1"; probe="$2"; fork="$3"; code_sha="$4"; armed_sha="$5"
docs(runner-probes): the checker binds the manifest to its target, and the snippets lint clean standalone (#202) @codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY, which my "parse, lint clean" claim had never meant. 1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha> <armed-sha> and used none of them — SC2034 on all three, which is the same defect the linter and the reviewer found independently. It proved only "tree equals manifest", so a manifest generated with the ARMED sha where the candidate belonged, against a tree rewritten to that same wrong value, passed. Wrong-but-consistent is exactly what this gate exists to reject. Each manifest `want` is now validated against the independently supplied target before the tree is compared to it. 2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest" while the prose below correctly said to generate from the PRE-arming tree. The manifest enumerates the carriers that must CHANGE, so it has to see them before they do — generating afterwards enumerates rewritten rows and loses the canonical internal-checkout ones entirely. The generator's first parameter is <candidate-checkout> now, and says so. 3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and the first placement was itself invalid: SC1124, a directive may precede a complete command, not an individual case branch. The checker's mktemp gets a trap. Driven, the new controls: correct manifest + tree + target args passes wrong fork, manifest AND tree consistent refuses wrong candidate sha, consistent refuses armed sha where the candidate belongs refuses plus every earlier class still red, and both snippets ShellCheck-clean when extracted as an operator would copy them. test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:22:01 +00:00
# shellcheck disable=SC2016 # `${{ github.repository }}` is literal YAML, not a shell expansion
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
{
git -C "$candidate" grep -n 'CEREMONY_SELF_REF:' -- .github/workflows \
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
| cut -d: -f1,2 | sed "s|$|\tself_ref\t$code_sha|" || true
git -C "$candidate" grep -n 'repository: heavy-duty/ceremony' -- .github/workflows \
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
| cut -d: -f1,2 | sed "s|$|\tinternal_repo\t$fork|" || true
git -C "$candidate" grep -n 'repository: ${{ github.repository }}' -- .github/workflows \
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
| cut -d: -f1,2 | sed 's|$|\tconsumer_repo\t${{ github.repository }}|' || true
# Callers record the COMPLETE expected coordinate, not just the sha: the
# path is as rewritable as the owner, and a manifest that stores only the
# suffix cannot notice `…/actions/wrong-one@<right-sha>`.
git -C "$probe" grep -nE 'uses:[[:space:]]*[^[:space:]]*/ceremony/\.github/workflows/' -- .github \
| sed -E "s|^([^:]+):([0-9]+):.*/ceremony/(\.github/workflows/[^@[:space:]]+)@.*|\\1:\\2\\tworkflow_caller\\t$fork/\\3@$armed_sha|" || true
git -C "$probe" grep -nE 'uses:[[:space:]]*[^[:space:]]*/ceremony/actions/' -- .github \
| sed -E "s|^([^:]+):([0-9]+):.*/ceremony/(actions/[^@[:space:]]+)@.*|\\1:\\2\\taction_caller\\t$fork/\\3@$code_sha|" || true
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
} | sort >manifest.tsv
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
# Zero ceremony callers is a refusal by name; one layer only is fine.
callers="$(grep -cE '(workflow|action)_caller' manifest.tsv || true)"
[ "$callers" -gt 0 ] || { echo "manifest: no ceremony callers found in $probe" >&2; exit 1; }
docs(runner-probes): enumerate the real carriers, spare the consumer checkouts, and make the snippet run (#202) @codex-reviewer-andresmgsl's three, all verified against current main before fixing. 1. THERE ARE THREE SELF-REF CARRIERS, NOT TWO — labels-sweep.yml:52, labels.yml:51, release.yml:132. My `[ "$n" -eq 2 ]` came from the pre-upstream tree, so it would have REJECTED a correctly armed candidate and told the operator to rewrite two of three, leaving one workflow pinned to the tag. The gate enumerates from the tree now, with the derivation commands beside the table so the list is re-checked rather than trusted. 2. NOT EVERY `repository:` BELONGS TO THE FORK. Three are `${{ github.repository }}` — labels-sweep.yml:69, labels.yml:92, release-exercise.yml:72 — and they fetch the CALLER's repository. My loop required every one to equal the fork, which would have rewritten the consumer checkouts and quietly changed what the probe exercises. Internal self-checkouts (four) are asserted to be the fork; consumer checkouts are asserted to stay dynamic. 3. EACH CHECK IS BOUND TO THE TREE IT IS ABOUT — `git -C "$armed"` for the carriers, `git -C "$probe"` for the callers, instead of depending on the operator's current directory. And `mapfile` rather than `git grep | while … fail`: the loop ran in a pipeline subshell, so `fail` exited the subshell and the gate carried on. Collect first, validate after, under a declared `set -euo pipefail`. And the snippet is now executable rather than illustrative: placeholders became positional parameters, so it parses, is shellcheck-clean, and runs. Driven against the unarmed tree it refuses with `CEREMONY_SELF_REF=0.6.0` — a tag rather than the candidate SHA, which is exactly the case it exists to catch. Publishing a gate that could not run would have been the same defect one level up. Branch updated from merged main (e236318). test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:13:40 +00:00
```
docs(runner-probes): the checker binds the manifest to its target, and the snippets lint clean standalone (#202) @codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY, which my "parse, lint clean" claim had never meant. 1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha> <armed-sha> and used none of them — SC2034 on all three, which is the same defect the linter and the reviewer found independently. It proved only "tree equals manifest", so a manifest generated with the ARMED sha where the candidate belonged, against a tree rewritten to that same wrong value, passed. Wrong-but-consistent is exactly what this gate exists to reject. Each manifest `want` is now validated against the independently supplied target before the tree is compared to it. 2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest" while the prose below correctly said to generate from the PRE-arming tree. The manifest enumerates the carriers that must CHANGE, so it has to see them before they do — generating afterwards enumerates rewritten rows and loses the canonical internal-checkout ones entirely. The generator's first parameter is <candidate-checkout> now, and says so. 3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and the first placement was itself invalid: SC1124, a directive may precede a complete command, not an individual case branch. The checker's mktemp gets a trap. Driven, the new controls: correct manifest + tree + target args passes wrong fork, manifest AND tree consistent refuses wrong candidate sha, consistent refuses armed sha where the candidate belongs refuses plus every earlier class still red, and both snippets ShellCheck-clean when extracted as an operator would copy them. test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:22:01 +00:00
Then arm — rewrite and commit — and check the result against it:
docs(runner-probes): rewrite coordinates not only refs, keep result issues, and stop asserting what was not measured (#202) @codex-reviewer-andresmgsl's three operational corrections. 1. ARMING REWRITES THE COORDINATE. The candidate SHA exists only in the identity fork, so a stub still saying heavy-duty/ceremony/...@<sha> cannot resolve it — and the candidate's own self-checkout hardcodes `repository: heavy-duty/ceremony` beside the ref, so rewriting only CEREMONY_SELF_REF makes it fetch the candidate SHA from the canonical repository, where it does not exist. Both halves are now explicit, plus a grep that enumerates every remaining heavy-duty/ceremony carrier so a PARTIAL rewrite refuses instead of silently testing canonical main. 2. RESULT ISSUES ARE NOT RESET SCOPE. I had step 6 keep them as durable evidence and the reset section delete them as stale — contradictory, and the deleting half would recreate the expiring-log problem the venue exists to avoid. Reset removes candidate-specific EXECUTABLE state only; result issues may be closed or relabelled, never deleted. 3. NO UNMEASURED CLAIMS. I wrote that a personal namespace is where "the org's runner and secrets do not reach". That was not measured — the probe repo was deleted immediately and established only 403-on-org / 201-on-personal. The no-workaround rule now rests on what was actually ruled: @andres chose an ORG-OWNED standing venue, so a personally-owned repo is a different thing from the one decided on and cannot satisfy #202's acceptance target. If runner reach matters, it gets measured once the venue exists. test/run.sh 28/28; shellcheck 0.10.0, changelog-armed clean. Refs #202
2026-08-05 13:46:03 +00:00
```sh
docs(runner-probes): enumerate the real carriers, spare the consumer checkouts, and make the snippet run (#202) @codex-reviewer-andresmgsl's three, all verified against current main before fixing. 1. THERE ARE THREE SELF-REF CARRIERS, NOT TWO — labels-sweep.yml:52, labels.yml:51, release.yml:132. My `[ "$n" -eq 2 ]` came from the pre-upstream tree, so it would have REJECTED a correctly armed candidate and told the operator to rewrite two of three, leaving one workflow pinned to the tag. The gate enumerates from the tree now, with the derivation commands beside the table so the list is re-checked rather than trusted. 2. NOT EVERY `repository:` BELONGS TO THE FORK. Three are `${{ github.repository }}` — labels-sweep.yml:69, labels.yml:92, release-exercise.yml:72 — and they fetch the CALLER's repository. My loop required every one to equal the fork, which would have rewritten the consumer checkouts and quietly changed what the probe exercises. Internal self-checkouts (four) are asserted to be the fork; consumer checkouts are asserted to stay dynamic. 3. EACH CHECK IS BOUND TO THE TREE IT IS ABOUT — `git -C "$armed"` for the carriers, `git -C "$probe"` for the callers, instead of depending on the operator's current directory. And `mapfile` rather than `git grep | while … fail`: the loop ran in a pipeline subshell, so `fail` exited the subshell and the gate carried on. Collect first, validate after, under a declared `set -euo pipefail`. And the snippet is now executable rather than illustrative: placeholders became positional parameters, so it parses, is shellcheck-clean, and runs. Driven against the unarmed tree it refuses with `CEREMONY_SELF_REF=0.6.0` — a tag rather than the candidate SHA, which is exactly the case it exists to catch. Publishing a gate that could not run would have been the same defect one level up. Branch updated from merged main (e236318). test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:13:40 +00:00
#!/usr/bin/env bash
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
# check-arming <armed-checkout> <probe-checkout> <fork> <code-sha> <armed-sha> <manifest.tsv>
docs(runner-probes): enumerate the real carriers, spare the consumer checkouts, and make the snippet run (#202) @codex-reviewer-andresmgsl's three, all verified against current main before fixing. 1. THERE ARE THREE SELF-REF CARRIERS, NOT TWO — labels-sweep.yml:52, labels.yml:51, release.yml:132. My `[ "$n" -eq 2 ]` came from the pre-upstream tree, so it would have REJECTED a correctly armed candidate and told the operator to rewrite two of three, leaving one workflow pinned to the tag. The gate enumerates from the tree now, with the derivation commands beside the table so the list is re-checked rather than trusted. 2. NOT EVERY `repository:` BELONGS TO THE FORK. Three are `${{ github.repository }}` — labels-sweep.yml:69, labels.yml:92, release-exercise.yml:72 — and they fetch the CALLER's repository. My loop required every one to equal the fork, which would have rewritten the consumer checkouts and quietly changed what the probe exercises. Internal self-checkouts (four) are asserted to be the fork; consumer checkouts are asserted to stay dynamic. 3. EACH CHECK IS BOUND TO THE TREE IT IS ABOUT — `git -C "$armed"` for the carriers, `git -C "$probe"` for the callers, instead of depending on the operator's current directory. And `mapfile` rather than `git grep | while … fail`: the loop ran in a pipeline subshell, so `fail` exited the subshell and the gate carried on. Collect first, validate after, under a declared `set -euo pipefail`. And the snippet is now executable rather than illustrative: placeholders became positional parameters, so it parses, is shellcheck-clean, and runs. Driven against the unarmed tree it refuses with `CEREMONY_SELF_REF=0.6.0` — a tag rather than the candidate SHA, which is exactly the case it exists to catch. Publishing a gate that could not run would have been the same defect one level up. Branch updated from merged main (e236318). test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:13:40 +00:00
set -euo pipefail
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
armed="$1"; probe="$2"; fork="$3"; code_sha="$4"; armed_sha="$5"; manifest="$6"
fail() { echo "arming incomplete: $*" >&2; exit 1; }
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
# `|| true` on every extraction: git grep exits 1 when nothing matches, and
# under `set -e` that would kill this script BEFORE the comparison — so a
# carrier class that vanished ENTIRELY produced silence instead of a
# refusal. Silence is the worst of the three outcomes; the comparison below
# is what must report it.
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
[ "$(grep -cE '(workflow|action)_caller' "$manifest" || true)" -gt 0 ] \
|| fail "manifest names no ceremony callers — it cannot prove an arming"
docs(runner-probes): the checker binds the manifest to its target, and the snippets lint clean standalone (#202) @codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY, which my "parse, lint clean" claim had never meant. 1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha> <armed-sha> and used none of them — SC2034 on all three, which is the same defect the linter and the reviewer found independently. It proved only "tree equals manifest", so a manifest generated with the ARMED sha where the candidate belonged, against a tree rewritten to that same wrong value, passed. Wrong-but-consistent is exactly what this gate exists to reject. Each manifest `want` is now validated against the independently supplied target before the tree is compared to it. 2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest" while the prose below correctly said to generate from the PRE-arming tree. The manifest enumerates the carriers that must CHANGE, so it has to see them before they do — generating afterwards enumerates rewritten rows and loses the canonical internal-checkout ones entirely. The generator's first parameter is <candidate-checkout> now, and says so. 3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and the first placement was itself invalid: SC1124, a directive may precede a complete command, not an individual case branch. The checker's mktemp gets a trap. Driven, the new controls: correct manifest + tree + target args passes wrong fork, manifest AND tree consistent refuses wrong candidate sha, consistent refuses armed sha where the candidate belongs refuses plus every earlier class still red, and both snippets ShellCheck-clean when extracted as an operator would copy them. test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:22:01 +00:00
# THE MANIFEST ITSELF IS CHECKED AGAINST THE TARGET, not trusted. Comparing
# only tree-vs-manifest proves consistency, and a manifest generated with the
# armed SHA where the candidate SHA belonged — or with the wrong fork —
# describes a WRONG arming perfectly. The tree would then match it and the
# gate would pass (@codex-reviewer-andresmgsl, #202 review).
# shellcheck disable=SC2016 # `${{ github.repository }}` below is literal YAML
while IFS=$'\t' read -r loc kind want; do
case "$kind" in
self_ref) [ "$want" = "$code_sha" ] || fail "manifest $loc: self_ref should be the CANDIDATE sha" ;;
internal_repo) [ "$want" = "$fork" ] || fail "manifest $loc: internal repo should be $fork" ;;
consumer_repo) [ "$want" = '${{ github.repository }}' ] \
|| fail "manifest $loc: consumer checkout must stay dynamic" ;;
# THE KIND MUST BIND TO THE PATH CLASS, not only to the owner and the
# sha. The path class is what SAYS which layer a caller is, so checking
# the sha against the kind while letting the kind float free accepts a
# consistent layer swap — `…/actions/x@<armed>` declared workflow_caller
# passes every owner and sha test (@codex-reviewer-andresmgsl, #202
# review). Decompose once, then let the kind fix BOTH coordinates.
workflow_caller|action_caller)
owner="${want%%/ceremony/*}"; rest="${want#*/ceremony/}"
path="${rest%@*}"; sha="${want##*@}"
[ "$owner/ceremony" = "$fork" ] \
|| fail "manifest $loc: caller owner should be $fork"
case "$kind" in
workflow_caller)
case "$path" in .github/workflows/?*) : ;;
*) fail "manifest $loc: workflow_caller must resolve at .github/workflows/<file>, not '$path'" ;;
esac
[ "$sha" = "$armed_sha" ] || fail "manifest $loc: workflow caller should be the ARMED sha" ;;
action_caller)
case "$path" in actions/?*) : ;;
*) fail "manifest $loc: action_caller must resolve at actions/<name>, not '$path'" ;;
esac
[ "$sha" = "$code_sha" ] || fail "manifest $loc: action caller should be the CANDIDATE sha" ;;
esac ;;
docs(runner-probes): the checker binds the manifest to its target, and the snippets lint clean standalone (#202) @codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY, which my "parse, lint clean" claim had never meant. 1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha> <armed-sha> and used none of them — SC2034 on all three, which is the same defect the linter and the reviewer found independently. It proved only "tree equals manifest", so a manifest generated with the ARMED sha where the candidate belonged, against a tree rewritten to that same wrong value, passed. Wrong-but-consistent is exactly what this gate exists to reject. Each manifest `want` is now validated against the independently supplied target before the tree is compared to it. 2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest" while the prose below correctly said to generate from the PRE-arming tree. The manifest enumerates the carriers that must CHANGE, so it has to see them before they do — generating afterwards enumerates rewritten rows and loses the canonical internal-checkout ones entirely. The generator's first parameter is <candidate-checkout> now, and says so. 3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and the first placement was itself invalid: SC1124, a directive may precede a complete command, not an individual case branch. The checker's mktemp gets a trap. Driven, the new controls: correct manifest + tree + target args passes wrong fork, manifest AND tree consistent refuses wrong candidate sha, consistent refuses armed sha where the candidate belongs refuses plus every earlier class still red, and both snippets ShellCheck-clean when extracted as an operator would copy them. test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:22:01 +00:00
*) fail "manifest $loc: unknown kind '$kind'" ;;
esac
done <"$manifest"
actual="$(mktemp)"; trap 'rm -f "$actual"' EXIT
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
{
git -C "$armed" grep -nP '(?<=CEREMONY_SELF_REF: ")[^"]+' -- .github/workflows \
| sed -E 's/^([^:]+):([0-9]+):.*CEREMONY_SELF_REF: "([^"]*)".*/\1:\2\tself_ref\t\3/' || true
git -C "$armed" grep -nE 'repository: .+' -- .github/workflows \
| sed -E 's|^([^:]+):([0-9]+):[[:space:]]*repository:[[:space:]]*(.*)$|\1:\2\t__repo__\t\3|' || true
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
# Only CEREMONY callers, matching the generator's domain exactly — a
# third-party `actions/checkout` is not this gate's business, and
# extracting it here while the generator ignores it made every probe fail
# as an "unrecognised carrier" (@codex-reviewer-andresmgsl). A wrong OWNER
# is still caught: `wrong-owner/ceremony/...` matches this pattern.
git -C "$probe" grep -nE 'uses:[[:space:]]*[^[:space:]]*/ceremony/' -- .github \
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
| sed -E 's|^([^:]+):([0-9]+):[[:space:]]*-?[[:space:]]*uses:[[:space:]]*(.*)$|\1:\2\t__uses__\t\3|' || true
} | sort >"$actual"
# every manifest line must be present with its EXACT expected value, and the
# kinds must match — a role swap changes the kind, not just the value.
while IFS=$'\t' read -r loc kind want; do
case "$kind" in
self_ref) have="$(awk -F'\t' -v l="$loc" '$1==l && $2=="self_ref"{print $3}' "$actual")" ;;
internal_repo|consumer_repo)
have="$(awk -F'\t' -v l="$loc" '$1==l && $2=="__repo__"{print $3}' "$actual")" ;;
workflow_caller|action_caller)
have="$(awk -F'\t' -v l="$loc" '$1==l && $2=="__uses__"{print $3}' "$actual")" ;;
docs(runner-probes): enumerate the real carriers, spare the consumer checkouts, and make the snippet run (#202) @codex-reviewer-andresmgsl's three, all verified against current main before fixing. 1. THERE ARE THREE SELF-REF CARRIERS, NOT TWO — labels-sweep.yml:52, labels.yml:51, release.yml:132. My `[ "$n" -eq 2 ]` came from the pre-upstream tree, so it would have REJECTED a correctly armed candidate and told the operator to rewrite two of three, leaving one workflow pinned to the tag. The gate enumerates from the tree now, with the derivation commands beside the table so the list is re-checked rather than trusted. 2. NOT EVERY `repository:` BELONGS TO THE FORK. Three are `${{ github.repository }}` — labels-sweep.yml:69, labels.yml:92, release-exercise.yml:72 — and they fetch the CALLER's repository. My loop required every one to equal the fork, which would have rewritten the consumer checkouts and quietly changed what the probe exercises. Internal self-checkouts (four) are asserted to be the fork; consumer checkouts are asserted to stay dynamic. 3. EACH CHECK IS BOUND TO THE TREE IT IS ABOUT — `git -C "$armed"` for the carriers, `git -C "$probe"` for the callers, instead of depending on the operator's current directory. And `mapfile` rather than `git grep | while … fail`: the loop ran in a pipeline subshell, so `fail` exited the subshell and the gate carried on. Collect first, validate after, under a declared `set -euo pipefail`. And the snippet is now executable rather than illustrative: placeholders became positional parameters, so it parses, is shellcheck-clean, and runs. Driven against the unarmed tree it refuses with `CEREMONY_SELF_REF=0.6.0` — a tag rather than the candidate SHA, which is exactly the case it exists to catch. Publishing a gate that could not run would have been the same defect one level up. Branch updated from merged main (e236318). test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:13:40 +00:00
esac
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
[ -n "$have" ] || fail "carrier vanished: $loc ($kind)"
docs(runner-probes): the generator survives a one-layer probe, callers carry full coordinates, one domain (#202) @codex-reviewer-andresmgsl drove the published commands again and found three. 1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` + `git grep` no-match bug I had just fixed in the CHECKER, in the generator I wrote in the same commit and did not apply the lesson to. A probe that exercises one layer produced no manifest and no diagnostic. `|| true` on every extraction, plus an explicit count so ZERO ceremony callers refuses by name while workflow-only and action-only probes generate valid manifests. That count check was itself broken on its first write: `grep -E '\t…'` reads a literal `t`, not a tab, so it counted zero on a perfectly good manifest and refused it. Found by running it. 2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and the checker compared owner and suffix separately, so `<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records `<fork>/<path>@<sha>` and every kind is one exact comparison — which also removes the per-kind branch that made the omission possible. 3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:` while the generator manifested only ceremony patterns, so a legitimate `actions/checkout` was always an unrecognised carrier. Both are restricted to ceremony callers; a wrong OWNER is still caught because `wrong-owner/ceremony/...` is still a ceremony caller. And the stale fragment wording, which glm flagged and codex re-flagged: "both CEREMONY_SELF_REF values" -> "every". DRIVEN, all of it: generator: both / workflow-only / action-only -> valid manifests generator: zero ceremony callers -> refuses by name deletion, role swap x2, wrong owner, wrong sha, wrong path, deleted caller class, extra carrier -> all refuse armed control, third-party actions/checkout present -> passes test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 15:07:11 +00:00
# ONE comparison for every kind: the manifest already carries the complete
# expected value, so owner, path AND sha are checked at once. Checking the
# owner and the sha separately let `…/actions/wrong-one@<right-sha>`
# through (@codex-reviewer-andresmgsl).
[ "$have" = "$want" ] || fail "$loc ($kind): expected '$want', found '$have'"
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
done <"$manifest"
docs(runner-probes): enumerate the real carriers, spare the consumer checkouts, and make the snippet run (#202) @codex-reviewer-andresmgsl's three, all verified against current main before fixing. 1. THERE ARE THREE SELF-REF CARRIERS, NOT TWO — labels-sweep.yml:52, labels.yml:51, release.yml:132. My `[ "$n" -eq 2 ]` came from the pre-upstream tree, so it would have REJECTED a correctly armed candidate and told the operator to rewrite two of three, leaving one workflow pinned to the tag. The gate enumerates from the tree now, with the derivation commands beside the table so the list is re-checked rather than trusted. 2. NOT EVERY `repository:` BELONGS TO THE FORK. Three are `${{ github.repository }}` — labels-sweep.yml:69, labels.yml:92, release-exercise.yml:72 — and they fetch the CALLER's repository. My loop required every one to equal the fork, which would have rewritten the consumer checkouts and quietly changed what the probe exercises. Internal self-checkouts (four) are asserted to be the fork; consumer checkouts are asserted to stay dynamic. 3. EACH CHECK IS BOUND TO THE TREE IT IS ABOUT — `git -C "$armed"` for the carriers, `git -C "$probe"` for the callers, instead of depending on the operator's current directory. And `mapfile` rather than `git grep | while … fail`: the loop ran in a pipeline subshell, so `fail` exited the subshell and the gate carried on. Collect first, validate after, under a declared `set -euo pipefail`. And the snippet is now executable rather than illustrative: placeholders became positional parameters, so it parses, is shellcheck-clean, and runs. Driven against the unarmed tree it refuses with `CEREMONY_SELF_REF=0.6.0` — a tag rather than the candidate SHA, which is exactly the case it exists to catch. Publishing a gate that could not run would have been the same defect one level up. Branch updated from merged main (e236318). test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:13:40 +00:00
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
# and nothing UNRECOGNISED: every uses:/repository: in the trees must appear
# in the manifest, so an added carrier is a failure rather than a silence.
while IFS=$'\t' read -r loc _ _; do
grep -qF "$loc"$'\t' "$manifest" || fail "carrier not in manifest: $loc"
done <"$actual"
docs(runner-probes): rewrite coordinates not only refs, keep result issues, and stop asserting what was not measured (#202) @codex-reviewer-andresmgsl's three operational corrections. 1. ARMING REWRITES THE COORDINATE. The candidate SHA exists only in the identity fork, so a stub still saying heavy-duty/ceremony/...@<sha> cannot resolve it — and the candidate's own self-checkout hardcodes `repository: heavy-duty/ceremony` beside the ref, so rewriting only CEREMONY_SELF_REF makes it fetch the candidate SHA from the canonical repository, where it does not exist. Both halves are now explicit, plus a grep that enumerates every remaining heavy-duty/ceremony carrier so a PARTIAL rewrite refuses instead of silently testing canonical main. 2. RESULT ISSUES ARE NOT RESET SCOPE. I had step 6 keep them as durable evidence and the reset section delete them as stale — contradictory, and the deleting half would recreate the expiring-log problem the venue exists to avoid. Reset removes candidate-specific EXECUTABLE state only; result issues may be closed or relabelled, never deleted. 3. NO UNMEASURED CLAIMS. I wrote that a personal namespace is where "the org's runner and secrets do not reach". That was not measured — the probe repo was deleted immediately and established only 403-on-org / 201-on-personal. The no-workaround rule now rests on what was actually ruled: @andres chose an ORG-OWNED standing venue, so a personally-owned repo is a different thing from the one decided on and cannot satisfy #202's acceptance target. If runner reach matters, it gets measured once the venue exists. test/run.sh 28/28; shellcheck 0.10.0, changelog-armed clean. Refs #202
2026-08-05 13:46:03 +00:00
```
docs(runner-probes): the arming gate is a manifest comparison, driven against all six failure classes (#202) @codex-reviewer-andresmgsl's four holes and @glm-reviewer-andresmgsl's prose staleness. Every weaker shape I had written has a hole, and each was found in a published draft of this file: "the old literal is absent" a carrier rewritten to the wrong fork "every extracted value equals X" a carrier that VANISHED "each value is one of {fork,dynamic}" a ROLE SWAP either direction "the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha "known callers match" an unrecognised caller, or none So the arming step generates a MANIFEST — path, kind, full expected value — from the tree it is arming, and the gate compares actual carriers against it as a set. All six become one kind of failure: the sets differ. Generated rather than written into this document, because the carrier set changes whenever a workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went stale while main grew a third. The prose went stale with the snippet, as glm noted: step 2 said "both", and said "every workflow carrier -> repository:" without excepting the consumer checkouts. Both corrected. DRIVEN, not asserted. I built an armed/probe pair and ran every class: deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse the armed control -> passes Doing that found two defects the snippets would otherwise have shipped with: * the manifest generator's consumer-checkout line used `\$` inside SINGLE quotes — an escaped dollar, not the end anchor — so it silently produced a manifest row with no kind and no value; * `git grep` exits 1 on no-match, and under `set -e` inside the collecting group that killed the script BEFORE the comparison. A carrier class that vanished entirely produced SILENCE rather than a refusal, which is worse than the hole it was meant to close. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 14:53:30 +00:00
**Why a manifest rather than a longer list of assertions.** The carrier set
is a property of the tree at the moment of arming; any list written into
this document is stale the next time a workflow is added. The manifest is
generated from the tree, recorded in the result issue (step 6), and is the
thing a later reader compares against — so "what was armed" is evidence
rather than recollection.
docs(runner-probes): arming is two layers, because a commit cannot contain its own SHA (#202) @codex-reviewer-andresmgsl found that the procedure was not executable as written, and the reason is structural rather than a wording slip. The candidate's workflows carry `repository: heavy-duty/ceremony` beside `ref: ${{ env.CEREMONY_SELF_REF }}`, so arming must rewrite them. But rewriting CREATES A NEW COMMIT, and a commit cannot embed its own object ID. So a single-layer arming is self-referential: pin the callers to the pre-rewrite SHA and they load the UNARMED workflows; pin them to the post-rewrite SHA and you are asking that commit to contain itself. My step 3 asked for exactly that. Two layers, stated as a table because the distinction is the whole thing: candidate code SHA the immutable tree under test — actions/, lib/ armed workflow SHA a child commit whose workflows point at the fork and whose CEREMONY_SELF_REF is the candidate code SHA And callers pin by layer, because they are not the same thing: composite actions to the candidate code SHA, reusable workflows to the armed SHA, which is the only revision whose inner checkout is rewritten. The completeness check becomes a mechanical non-zero gate — `git grep` for executable `uses:`/`repository:` carriers over the ARMED tree, exiting non-zero on any hit — rather than "every remaining hit must be prose". A partial rewrite does not announce itself: it silently tests canonical main. The result issue records both SHAs, not one, or a later reader cannot tell which tree answered. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 13:54:44 +00:00
5. **Invoke the probe by the event it is about**, and record which: a
`workflow_dispatch`, or the real board event under test. A probe that fires
a different event than the one under test proves something else.
6. **The result issue records all of it**: the fork repository, the candidate
code SHA, the armed workflow SHA, every rewritten carrier, the workflow
invoked and the run number. Those are what make the result reproducible;
without the two SHAs distinguished, a later reader cannot tell which tree
answered.
7. **Reset removes the candidate-specific EXECUTABLE state**: the caller stubs,
the probe workflow, and the fork's probe branch — whose tip carries both the
candidate commit and the armed commit on top of it — so the next probe
docs(runner-probes): arming is two layers, because a commit cannot contain its own SHA (#202) @codex-reviewer-andresmgsl found that the procedure was not executable as written, and the reason is structural rather than a wording slip. The candidate's workflows carry `repository: heavy-duty/ceremony` beside `ref: ${{ env.CEREMONY_SELF_REF }}`, so arming must rewrite them. But rewriting CREATES A NEW COMMIT, and a commit cannot embed its own object ID. So a single-layer arming is self-referential: pin the callers to the pre-rewrite SHA and they load the UNARMED workflows; pin them to the post-rewrite SHA and you are asking that commit to contain itself. My step 3 asked for exactly that. Two layers, stated as a table because the distinction is the whole thing: candidate code SHA the immutable tree under test — actions/, lib/ armed workflow SHA a child commit whose workflows point at the fork and whose CEREMONY_SELF_REF is the candidate code SHA And callers pin by layer, because they are not the same thing: composite actions to the candidate code SHA, reusable workflows to the armed SHA, which is the only revision whose inner checkout is rewritten. The completeness check becomes a mechanical non-zero gate — `git grep` for executable `uses:`/`repository:` carriers over the ARMED tree, exiting non-zero on any hit — rather than "every remaining hit must be prose". A partial rewrite does not announce itself: it silently tests canonical main. The result issue records both SHAs, not one, or a later reader cannot tell which tree answered. test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean. Refs #202
2026-08-05 13:54:44 +00:00
cannot inherit a pin it did not choose. **Result issues are never deleted.**
They may be closed or relabelled; deleting them would recreate the
expiring-log problem this venue exists to avoid.
docs(runner-probes): its own document, an arming procedure, and the evidence boundary made consistent (#202) @codex-reviewer-andresmgsl's four gaps. 1. BRANCH UPDATED TO CURRENT MAIN. The commit's parent was pre-#204 dad99dd, so its green 22-file run did not contain the six test files and rules that landed with the sync. Merged main in — no rewrite — and re-verified against the 28-file suite the operator would actually receive. 2. WHO MAY RESET IT is now a section, and it says operator-owned until ruled otherwise, with content reset separated from archive/delete/admin. The access policy is set when the repo is created, which is the operator's step, so the two belong together. Flagged for @andres rather than assumed. 3. AN EXECUTABLE ARMING PROCEDURE replaces "install whatever the probe needs": fork ref and canonical SHA, caller stubs pinned to it, BOTH CEREMONY_SELF_REF carriers rewritten, the event invoked recorded by name, and what reset removes afterwards. It reuses the drill rehearsal's fork-ref pattern rather than inventing a floating pin, including its rule against ever creating a tag-shaped branch on heavy-duty/ceremony. 4. THE EVIDENCE CONTRADICTION IS RESOLVED. "Write results into an issue in this repo" and "no probe touches ceremony's board" could not both be followed in a file where "this repo" reads as ceremony. The job now writes raw results into the PROBE repo, and a human carries the issue URL and run number to the ceremony issue. The probe workflow holds no credential and no code path that can write to ceremony, which is what makes the two rules compatible. Placement: the operational contract moves to docs/RUNNER-PROBES.md, with a short cross-link in drills/README.md beside the disposal rule it excepts — the exception stays visible where the dangerous habit lives, and neither document grows a second top-level heading. test/run.sh 28/28 on the updated branch; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed clean. Refs #202
2026-08-05 13:40:28 +00:00
## Who may reset it
**Operator-owned until ruled otherwise.** #202's task 4 asks who may reset the
venue, and creating the repo is the operator's step, so the access policy is
his to set at the same time (@codex-reviewer-andresmgsl, #202 review).
Two levels, deliberately separated:
docs(runner-probes): rewrite coordinates not only refs, keep result issues, and stop asserting what was not measured (#202) @codex-reviewer-andresmgsl's three operational corrections. 1. ARMING REWRITES THE COORDINATE. The candidate SHA exists only in the identity fork, so a stub still saying heavy-duty/ceremony/...@<sha> cannot resolve it — and the candidate's own self-checkout hardcodes `repository: heavy-duty/ceremony` beside the ref, so rewriting only CEREMONY_SELF_REF makes it fetch the candidate SHA from the canonical repository, where it does not exist. Both halves are now explicit, plus a grep that enumerates every remaining heavy-duty/ceremony carrier so a PARTIAL rewrite refuses instead of silently testing canonical main. 2. RESULT ISSUES ARE NOT RESET SCOPE. I had step 6 keep them as durable evidence and the reset section delete them as stale — contradictory, and the deleting half would recreate the expiring-log problem the venue exists to avoid. Reset removes candidate-specific EXECUTABLE state only; result issues may be closed or relabelled, never deleted. 3. NO UNMEASURED CLAIMS. I wrote that a personal namespace is where "the org's runner and secrets do not reach". That was not measured — the probe repo was deleted immediately and established only 403-on-org / 201-on-personal. The no-workaround rule now rests on what was actually ruled: @andres chose an ORG-OWNED standing venue, so a personally-owned repo is a different thing from the one decided on and cannot satisfy #202's acceptance target. If runner reach matters, it gets measured once the venue exists. test/run.sh 28/28; shellcheck 0.10.0, changelog-armed clean. Refs #202
2026-08-05 13:46:03 +00:00
- **content reset** — removing probe branches, workflows and fixtures; the
ordinary between-probes operation. It does **not** include deleting result
issues, which are the evidence and are immutable once written
(@codex-reviewer-andresmgsl, #202 review);
docs(runner-probes): its own document, an arming procedure, and the evidence boundary made consistent (#202) @codex-reviewer-andresmgsl's four gaps. 1. BRANCH UPDATED TO CURRENT MAIN. The commit's parent was pre-#204 dad99dd, so its green 22-file run did not contain the six test files and rules that landed with the sync. Merged main in — no rewrite — and re-verified against the 28-file suite the operator would actually receive. 2. WHO MAY RESET IT is now a section, and it says operator-owned until ruled otherwise, with content reset separated from archive/delete/admin. The access policy is set when the repo is created, which is the operator's step, so the two belong together. Flagged for @andres rather than assumed. 3. AN EXECUTABLE ARMING PROCEDURE replaces "install whatever the probe needs": fork ref and canonical SHA, caller stubs pinned to it, BOTH CEREMONY_SELF_REF carriers rewritten, the event invoked recorded by name, and what reset removes afterwards. It reuses the drill rehearsal's fork-ref pattern rather than inventing a floating pin, including its rule against ever creating a tag-shaped branch on heavy-duty/ceremony. 4. THE EVIDENCE CONTRADICTION IS RESOLVED. "Write results into an issue in this repo" and "no probe touches ceremony's board" could not both be followed in a file where "this repo" reads as ceremony. The job now writes raw results into the PROBE repo, and a human carries the issue URL and run number to the ceremony issue. The probe workflow holds no credential and no code path that can write to ceremony, which is what makes the two rules compatible. Placement: the operational contract moves to docs/RUNNER-PROBES.md, with a short cross-link in drills/README.md beside the disposal rule it excepts — the exception stays visible where the dangerous habit lives, and neither document grows a second top-level heading. test/run.sh 28/28 on the updated branch; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed clean. Refs #202
2026-08-05 13:40:28 +00:00
- **archive / delete / admin** — which is where the drill rule's damage came
from, and which no bot identity should hold here.
If fleet identities are given push access for content reset, this section
records that; until then, ask.
## 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 — and the records of those delivered
Delivered probes stay listed with their record: the venue's value is that a
claim like "the asymmetry reproduces" carries a URL a reader can open, not a
memory.
- **ceremony#192** — DELIVERED, first drill (2026-08-05). Under
`${{ github.token }}` in the venue:
`DELETE /issues/{n}/labels/{id}`**500**, the label still on the issue
afterward — the failure observable in the set, not merely a status — then
`PUT` full-set clear → **200**, set actually empty. Record:
[probe issue #1](https://forgejo.heavyduty.builders/heavy-duty/ceremony-runner-probe/issues/1)
(run 1) and
[probe issue #2](https://forgejo.heavyduty.builders/heavy-duty/ceremony-runner-probe/issues/2)
(run 4 — the clean independent repeat after the redaction incident below).
- **ceremony#205** — DELIVERED with a correction to the line above's
premise. The dispatch route answers **204** to a valid body carrying the
bare resolvable ref `main` — under the workflow token
([ceremony#205 comment #6262](https://forgejo.heavyduty.builders/heavy-duty/ceremony/issues/205#issuecomment-6262),
run 504, and again as
[probe issue #4](https://forgejo.heavyduty.builders/heavy-duty/ceremony-runner-probe/issues/4),
run 6) and under a PAT
([probe issue #5](https://forgejo.heavyduty.builders/heavy-duty/ceremony-runner-probe/issues/5),
run 7, and ceremony run 459). The earlier opaque `500` came from a bare
UNRESOLVABLE ref or an unknown/unparseable workflow — the diagnostic !213
ships now names this; a fully-qualified bad ref gets a clean 404 instead.
The `GET /actions/workflows` listing still 404s. Claims here are limited to
what those runs measured.
- **ceremony#215** — the discriminator drill: REST-body `inputs` DO reach a
top-level dispatched workflow, both contexts
(`inputs.*` and `github.event.inputs.*`), both identities. What loses the
value is the `workflow_call` boundary — a called workflow does not see the
caller's `event.inputs` on this instance. Records:
[probe issues #4 and #5](https://forgejo.heavyduty.builders/heavy-duty/ceremony-runner-probe/issues/4)
(runs 6 and 7).
- A 0.6.0 consumer exercise once ceremony#198 has merged. (#198 merged
2026-08-05; this probe remains open.)
Two venue lessons from the first drills, kept where the next probe author will
look:
- **Rule 4 is load-bearing on this instance**: the probe repo's web log route
404s for non-admin reads, and a log-only observation (run 2) was lost where
issue-written ones were not.
- **Report content sent to the forge must never contain a credential
expression OR a credential value** — moving a secret from `${{ … }}` syntax
into a shell variable does not make it safe to emit. Name identities in
literal prose ("the workflow token"), allowlist the fields a report emits,
and treat anything else as unsendable. The incident this teaches from: an
escaped `${{ github.token }}` in a comment template was expanded by the
runner into the recorded text (the run's ephemeral token); it was redacted
in place and the drill repeated clean (probe issue #2, run 4).