diff --git a/docs/RUNNER-PROBES.md b/docs/RUNNER-PROBES.md index 4f69922..1e98bc2 100644 --- a/docs/RUNNER-PROBES.md +++ b/docs/RUNNER-PROBES.md @@ -144,6 +144,7 @@ So: | "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@` | | "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@` labelled a workflow caller satisfies both | So the arming step **writes a manifest** — one line per carrier, `path`, `kind`, `full expected value` — and the gate compares the tree's actual @@ -167,14 +168,14 @@ So: # produced no file and no diagnostic when a probe exercised only one layer # (@codex-reviewer-andresmgsl). A probe need not use both. set -euo pipefail - armed="$1"; probe="$2"; fork="$3"; code_sha="$4"; armed_sha="$5" # $1 = pre-arming + candidate="$1"; probe="$2"; fork="$3"; code_sha="$4"; armed_sha="$5" # shellcheck disable=SC2016 # `${{ github.repository }}` is literal YAML, not a shell expansion { - git -C "$armed" grep -n 'CEREMONY_SELF_REF:' -- .github/workflows \ + git -C "$candidate" grep -n 'CEREMONY_SELF_REF:' -- .github/workflows \ | cut -d: -f1,2 | sed "s|$|\tself_ref\t$code_sha|" || true - git -C "$armed" grep -n 'repository: heavy-duty/ceremony' -- .github/workflows \ + git -C "$candidate" grep -n 'repository: heavy-duty/ceremony' -- .github/workflows \ | cut -d: -f1,2 | sed "s|$|\tinternal_repo\t$fork|" || true - git -C "$armed" grep -n 'repository: ${{ github.repository }}' -- .github/workflows \ + git -C "$candidate" grep -n 'repository: ${{ github.repository }}' -- .github/workflows \ | 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 @@ -219,10 +220,29 @@ So: 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" ;; - workflow_caller) [ "$want" = "$fork/${want#*/ceremony/}" ] || fail "manifest $loc: caller owner" - [ "${want##*@}" = "$armed_sha" ] || fail "manifest $loc: workflow caller should be the ARMED sha" ;; - action_caller) [ "$want" = "$fork/${want#*/ceremony/}" ] || fail "manifest $loc: caller owner" - [ "${want##*@}" = "$code_sha" ] || fail "manifest $loc: action caller should be the CANDIDATE sha" ;; + # 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@` 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/, 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/, not '$path'" ;; + esac + [ "$sha" = "$code_sha" ] || fail "manifest $loc: action caller should be the CANDIDATE sha" ;; + esac ;; *) fail "manifest $loc: unknown kind '$kind'" ;; esac done <"$manifest"