From e27acd8ab96faa90564249d09ac07d50b7c66899 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 5 Aug 2026 13:54:44 +0000 Subject: [PATCH] docs(runner-probes): arming is two layers, because a commit cannot contain its own SHA (#202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @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 --- changelog.d/202.md | 14 +++++-- docs/RUNNER-PROBES.md | 98 ++++++++++++++++++++++++++----------------- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/changelog.d/202.md b/changelog.d/202.md index c824c63..66e445d 100644 --- a/changelog.d/202.md +++ b/changelog.d/202.md @@ -19,9 +19,17 @@ assumed: a fleet identity gets 403 on org repo creation and 201 in its own namespace (#202). -- It carries an executable arming procedure — fork ref, canonical SHA, both - `CEREMONY_SELF_REF` carriers rewritten, and what the result must record - (#202). +- It carries an executable two-layer arming procedure: an immutable candidate + code SHA and an armed workflow commit on top of it. A single layer is + self-referential — rewriting a workflow makes a new commit, and a commit + cannot contain its own object ID (#202). + +- Callers are pinned by layer: composite actions to the candidate code SHA, + reusable workflows to the armed SHA, which is the only revision whose inner + checkout points at the fork (#202). + +- A `git grep` over the armed tree gates the rewrite non-zero, so a partial one + refuses instead of silently testing canonical main (#202). - Probe results are written to an issue in the probe repo and carried to the ceremony issue by a human, so the probe holds no path that can write to the diff --git a/docs/RUNNER-PROBES.md b/docs/RUNNER-PROBES.md index 65354f0..9315c23 100644 --- a/docs/RUNNER-PROBES.md +++ b/docs/RUNNER-PROBES.md @@ -61,8 +61,9 @@ exists rather than assuming it here. 1. Reset the repo to a clean state — the probe's own fixtures only, no leftovers from the last one. A probe that inherits state is a probe whose result you cannot attribute. -2. **Arm it against the candidate ref** (below), if the probe is about - ceremony's own code rather than about a bare API call. +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. 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 @@ -83,48 +84,69 @@ exists rather than assuming it here. ## Arming a candidate ref A probe that exercises ceremony's own machinery needs the candidate tree -reachable from a `uses:` line. The shape is the drill rehearsal's, reused -rather than reinvented (`drills/README.md` step 2): +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. -1. **The candidate is a commit SHA, on a fork ref.** Push the candidate tree to - a fork under the identity running the probe — - `/ceremony@probe-` — and take its canonical 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. -2. **Rewrite the COORDINATE, not only the ref.** The candidate SHA exists only - in your fork, so a stub still saying `heavy-duty/ceremony/...@` cannot - resolve it — it would fail, or worse, resolve something else. In the probe - repo, every caller `uses:` becomes - `/ceremony/@`. -3. **Rewrite the candidate's own self-checkout, both halves.** `labels.yml` and - the release doors hardcode `repository: heavy-duty/ceremony` beside - `ref: ${{ env.CEREMONY_SELF_REF }}`. Changing only the ref makes the - candidate fetch your SHA *from the canonical repository*, where it does not - exist. So: every `repository:` carrier becomes `/ceremony`, and - both `CEREMONY_SELF_REF` values become the canonical SHA. +**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). - **Then prove the rewrite was total**, because a partial one silently tests - canonical `main` instead of the candidate: +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 — `/ceremony@probe-` — and record its SHA. That is + 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. +2. **Commit the arming on top of it.** In that same fork branch, rewrite every + workflow carrier — `repository:` → `/ceremony`, and both + `CEREMONY_SELF_REF` values → the **candidate code SHA** from step 1. Record + the resulting SHA. That is the **armed workflow SHA**. +3. **Pin the probe repo's callers by layer**, because they are not the same + thing: + - composite-action callers → + `/ceremony/actions/@`; + - reusable-workflow callers → + `/ceremony/.github/workflows/@`, since + that is the only revision whose inner checkout is rewritten. +4. **Gate the rewrite mechanically, against the armed tree** — not against + whichever checkout happens to be current, and not by eyeballing prose: ```sh - grep -rn 'heavy-duty/ceremony' .github/ docs/CONSUMERS.md + git -C grep -nE \ + '(uses:|repository:)[[:space:]]*heavy-duty/ceremony' -- .github \ + && { echo "arming incomplete" >&2; exit 1; } ``` - Every remaining hit must be prose. A `uses:`, a `repository:` or a pin among - them means the probe is not armed, and its result is about the wrong tree. -4. **Invoke the probe by the event it is about**, and record which: a - `workflow_dispatch` of the caller, or the real board event the probe is - testing. A probe that fires a different event than the one under test - proves something else. -5. **Record the fork ref, the rewritten pin, the workflow invoked and the run - number** in the probe repo's result issue. Those four are what make the - result reproducible; without the pin especially, a later reader cannot tell - which tree answered. -6. **Reset removes the candidate-specific EXECUTABLE state**: the caller stubs, - the probe workflow, the candidate branch — so the next probe 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. + Exit non-zero on any hit. A partial rewrite does not fail loudly on its + own — it silently tests canonical `main`, and the probe's answer is then + about the wrong tree. +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, the candidate and armed branches — so the next probe + 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. ## Who may reset it