Repository navigation
Foundational CI/CD: supersede #1338, wire squabbler/proven/deep-recon, KYAML+I-JSON gates #1342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 40 commits
8607271
37c45a4
d9a78dd
da9073d
8ed2f3a
620060f
d4b3d28
553bbd7
0aca273
d49f8f7
88699f4
0857173
dc8caa8
477ff71
97cd2e4
583101e
57059c6
48fa3c9
ffe1094
faf2a1b
64ada82
609deeb
0f433ca
98ebde4
04acc6b
83afb5d
107d4c7
c3e9fad
ddc6180
1d9ef56
3dfb5fd
b6ae8f1
110b5b5
f60738c
10191dd
bdc5afd
ab87044
318fa6e
c81a57e
5a5525c
8bc8e53
8868d09
334f4be
6204ec9
709d120
7981e33
34fb477
1cf714d
cf1f562
f061333
f572331
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| # SPDX-License-Identifier: MPL-2.0 | ||
| # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> | ||
| # | ||
| # cicd-squabbler-reusable.yml — run the estate's CI/CD fighter as a gate. | ||
| # | ||
| # The squabbler's one invariant is machine-checked (SPARK, in its repo): | ||
| # green ⇔ a required check actually ran and passed. It never offers a | ||
| # bypass, an admin override, or a dropped required context as a move. That | ||
| # is exactly the failure this repository paid for on 2026-10-24 (a required | ||
| # context nothing could produce, merged around with an `always` bypass), so | ||
| # the tool belongs INSIDE the gate set, not beside it. | ||
| # | ||
| # Two jobs, two different contracts: | ||
| # | ||
| # * `chains` is a HARD gate. `squabble chains` maps reusable-workflow and | ||
| # action dependencies between repositories. Exit 4 means a dependency | ||
| # cycle or a dead upstream — either makes some required check | ||
| # unproducible while the board reads green, so the job fails. Exit 2 is a | ||
| # tool failure and also fails: a gate that cannot look must not pass. | ||
| # Warnings (pin skew, diamonds) are printed and do not fail. | ||
| # | ||
| # * `fight-report` is a REPORT, deliberately. It classifies the calling | ||
| # pull request's reds (self-win | escalate | assign-owner) into the step | ||
| # summary. When it runs, it fails only when the tool itself fails | ||
| # (exit 2): "the planner proposed a move" is information for a human, | ||
| # not a verdict on the PR, and gating on it would fail every PR that is | ||
| # legitimately red for unrelated reasons — twice. It NEVER passes | ||
| # `--apply` and never writes to the repository; enacting a move is a | ||
| # follow-up commit a human (or a later, separately-reviewed | ||
| # automation) makes. | ||
| # | ||
| # Measured on this repository: `fight` reads the gate as rulesets ∪ | ||
| # classic branch protection, and refuses to reason about a gate it can | ||
| # only partially see. The classic-protection endpoint is 403 to the | ||
| # built-in Actions token even though all 24 ruleset contexts are | ||
| # visible, so squabble correctly exits 2. Fight therefore runs only | ||
| # when the caller supplies the optional Administration-read credential | ||
| # declared below AND a preflight proves that credential can read the | ||
| # pull request, the branch-rules endpoint and classic protection (or a | ||
| # genuine 404 for the latter). Absent credential = the summary says | ||
| # NOT RUN, in those words. A supplied credential that cannot see the | ||
| # whole gate = NOT EVALUATED: the summary makes no claim about the PR, | ||
| # and this advisory does not redden every PR for an owner-side | ||
| # credential that needs broadening or reissuing. The pinned upstream's | ||
| # exact hidden-gate wording remains a second guard if visibility | ||
| # changes between probe and classification. Every other exit 2 fails. | ||
| # | ||
| # The squabble binary is installed by the upstream composite action pinned | ||
| # to a commit; the action itself refuses any binary whose sha256 and build | ||
| # attestation do not match its pinned release. `verify-satisfied` is NOT | ||
| # run here even when the credential is supplied: promoting that stronger | ||
| # verdict is a separate, separately-reviewed decision. | ||
| # | ||
| # Consumers pin this reusable by commit SHA like every other estate | ||
| # reusable. Keep caller grants at or above the job-level grants below, or | ||
| # GitHub rejects the run at startup with no check run at all. | ||
|
|
||
| name: CICD Squabbler (reusable) | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| pr-number: | ||
| description: "Pull request number for the fight report; empty skips it (push/schedule runs)." | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| runs-on: | ||
| description: "Runner label; the pinned squabble release is x86_64 Linux only." | ||
| required: false | ||
| type: string | ||
| default: "ubuntu-latest" | ||
| secrets: | ||
| ADMINISTRATION_READ_TOKEN: | ||
| description: "Optional fine-grained/App token with Administration:read plus pull-request, check and status read for squabble fight; absent means NOT RUN, and a credential that cannot see classic branch protection means NOT EVALUATED." | ||
| required: false | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| chains: | ||
| name: Squabble chains (cycles and dead upstreams block) | ||
| runs-on: ${{ inputs.runs-on }} | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: read | ||
| attestations: read | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install the pinned, attested squabble | ||
| id: squabble | ||
| uses: hyperpolymath/cicd-squabbler/.github/actions/squabble@14f853fde902ab2d7631070d87a9dc9122e01013 | ||
| with: | ||
| args: "" | ||
|
|
||
| # Argument form is <path>=<owner/repo>: the checkout is this repo, so | ||
| # its own outbound reusable/action edges are attributed to its slug. | ||
| # No `gh:` sources and no --follow here: following into other repos | ||
| # needs their read access from this token and belongs to the weekly | ||
| # estate sweep, not a per-PR gate. | ||
| - name: Map this repository's CI dependency chains | ||
| env: | ||
| SQUABBLE_BIN: ${{ steps.squabble.outputs.path }} | ||
| REPO: ${{ github.repository }} | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -uo pipefail | ||
| test -x "$SQUABBLE_BIN" || { echo "::error::squabble did not install"; exit 1; } | ||
| rc=0 | ||
| "$SQUABBLE_BIN" chains ".=$REPO" | tee "$RUNNER_TEMP/chains.txt" || rc=$? | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| { | ||
| echo "### Squabble chains" | ||
| echo | ||
| echo '```' | ||
| cat "$RUNNER_TEMP/chains.txt" | ||
| echo '```' | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| if [ "$rc" -eq 4 ]; then | ||
| echo "::error::blocking chain finding (dependency cycle or dead upstream) — see the summary" | ||
|
Comment on lines
+121
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| exit 1 | ||
| fi | ||
| if [ "$rc" -ne 0 ]; then | ||
| echo "::error::squabble chains failed to run (exit $rc) — a gate that cannot look does not pass" | ||
| exit 1 | ||
| fi | ||
|
|
||
| fight-report: | ||
| name: Squabble fight report (advisory) | ||
| if: inputs.pr-number != '' | ||
| runs-on: ${{ inputs.runs-on }} | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| attestations: read | ||
| steps: | ||
| # This is the only job in the reusable that touches a caller secret. | ||
| # Harden it first (Hypatia RE001 / ruling D131) so outbound egress is | ||
| # audited from the first credential-bearing request onward. | ||
| - name: Harden runner (egress audit) | ||
| uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| # Do not equate "a secret string exists" with "fight can inspect": | ||
| # the estate PAT measured on this PR is present but cannot read the | ||
| # Administration endpoint. Probe exactly the read surfaces squabble | ||
| # needs for its gate (PR/base, branch rules, classic protection). A | ||
| # classic-protection 404 is visible evidence of no classic gate; any | ||
| # other classic failure means the gate is only partially visible. | ||
| - name: Probe whether fight can inspect the full gate | ||
| id: fight-credential | ||
| env: | ||
| GH_TOKEN: ${{ secrets.ADMINISTRATION_READ_TOKEN }} | ||
| REPO: ${{ github.repository }} | ||
| PR_NUMBER: ${{ inputs.pr-number }} | ||
| run: | | ||
| set -uo pipefail | ||
| not_evaluated() { | ||
| reason="$1" | ||
| echo "available=false" >> "$GITHUB_OUTPUT" | ||
| echo "::notice title=Squabble fight not evaluated::${reason}. This advisory makes no claim about this pull request's reds. The hard chains gate still ran." | ||
| { | ||
| echo "### Squabble fight report — NOT EVALUATED" | ||
| echo | ||
| echo "${reason}." | ||
| echo "squabble fight reads rulesets and classic branch protection as one gate and refuses to reason about a partially visible gate. This advisory therefore makes no claim about this pull request's reds. Broaden or reissue the Administration-read credential to enable classification. The separate hard \`squabble chains\` gate remains enforced." | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| exit 0 | ||
| } | ||
|
hyperpolymath marked this conversation as resolved.
Resolved
|
||
| case "$PR_NUMBER" in | ||
| ''|*[!0-9]*) echo "::error::pr-number must be a number, got '$PR_NUMBER'"; exit 1 ;; | ||
| esac | ||
| if [ -z "$GH_TOKEN" ]; then | ||
| echo "available=false" >> "$GITHUB_OUTPUT" | ||
| echo "::notice title=Squabble fight not run::No Administration-read credential was supplied, so the advisory fight report did not inspect this pull request. The hard chains gate still ran." | ||
| { | ||
| echo "### Squabble fight report — NOT RUN" | ||
| echo | ||
| echo "No \`ADMINISTRATION_READ_TOKEN\` was supplied by the caller." | ||
| echo "squabble fight reads rulesets and classic branch protection as one gate, and the built-in Actions token cannot read the classic-protection endpoint. This advisory job therefore makes no claim about this pull request's reds. The separate hard \`squabble chains\` gate remains enforced." | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| exit 0 | ||
| fi | ||
| probe_err="$RUNNER_TEMP/fight-probe.err" | ||
| pr_json="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json baseRefName 2>"$probe_err")" \ | ||
| || not_evaluated "The supplied credential could not read this pull request" | ||
| base_ref="$(printf '%s' "$pr_json" | jq -er '.baseRefName | select(type == "string" and length > 0)')" \ | ||
| || { echo "::error::gh pr view returned no usable base branch for the fight preflight"; exit 1; } | ||
| encoded_base_ref="$(printf '%s' "$base_ref" | jq -Rr '@uri')" | ||
| gh api "repos/$REPO/rules/branches/$encoded_base_ref" >/dev/null 2>"$probe_err" \ | ||
| || not_evaluated "The supplied credential could not read this pull request's branch rules" | ||
| if gh api "repos/$REPO/branches/$encoded_base_ref/protection" >/dev/null 2>"$probe_err"; then | ||
| : | ||
| elif grep -Eq 'HTTP 404|Branch not protected' "$probe_err"; then | ||
| : | ||
| else | ||
| not_evaluated "The supplied credential could not read classic branch protection" | ||
| fi | ||
| echo "available=true" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| if: steps.fight-credential.outputs.available == 'true' | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install the pinned, attested squabble | ||
| id: squabble | ||
| if: steps.fight-credential.outputs.available == 'true' | ||
| uses: hyperpolymath/cicd-squabbler/.github/actions/squabble@14f853fde902ab2d7631070d87a9dc9122e01013 | ||
| with: | ||
| args: "" | ||
|
|
||
| # Propose-only: no --apply, ever, from CI. Exit 3 ("no gate on the | ||
| # base branch") is a reportable finding printed below, not a tool | ||
| # failure. Exit 2 is normally a tool failure and fails the job, | ||
| # because a silently broken advisor would be indistinguishable from | ||
| # "nothing to fight". The sole exception is the pinned upstream's | ||
| # exact hidden-classic-protection wording: that is positive evidence | ||
| # the supplied credential could not see the whole gate, so the | ||
| # advisory reports NOT EVALUATED instead of pretending to classify. | ||
| # The wording is version-specific; if a future pin changes it, the | ||
| # generic exit-2 branch below turns red until this contract is | ||
| # updated — failing closed, not silently downgrading real breakage. | ||
| - name: Classify this pull request's reds | ||
| if: steps.fight-credential.outputs.available == 'true' | ||
| env: | ||
| SQUABBLE_BIN: ${{ steps.squabble.outputs.path }} | ||
| REPO: ${{ github.repository }} | ||
| PR_NUMBER: ${{ inputs.pr-number }} | ||
| GH_TOKEN: ${{ secrets.ADMINISTRATION_READ_TOKEN }} | ||
| run: | | ||
| set -uo pipefail | ||
| test -x "$SQUABBLE_BIN" || { echo "::error::squabble did not install"; exit 1; } | ||
| case "$PR_NUMBER" in | ||
| ''|*[!0-9]*) echo "::error::pr-number must be a number, got '$PR_NUMBER'"; exit 1 ;; | ||
| esac | ||
| rc=0 | ||
| "$SQUABBLE_BIN" fight "$REPO" "$PR_NUMBER" --repo-root . > "$RUNNER_TEMP/fight.txt" 2>&1 || rc=$? | ||
| gate_hidden=false | ||
| if [ "$rc" -eq 2 ] && grep -Fq "classic branch protection on this branch is not visible to this token" "$RUNNER_TEMP/fight.txt"; then | ||
| gate_hidden=true | ||
| fi | ||
| { | ||
| if [ "$gate_hidden" = "true" ]; then | ||
| echo "### Squabble fight report — NOT EVALUATED" | ||
| else | ||
| echo "### Squabble fight report (propose-only)" | ||
| fi | ||
| echo | ||
| echo '```' | ||
| cat "$RUNNER_TEMP/fight.txt" | ||
| echo '```' | ||
| echo | ||
| if [ "$gate_hidden" = "true" ]; then | ||
| echo "Exit 2 — classic branch protection is not visible to the supplied credential, so squabble refused to reason about a partially visible gate. This advisory makes no claim about this pull request's reds, and no move was applied." | ||
| else | ||
| echo "Exit $rc. No move was applied; squabble in CI proposes, humans enact." | ||
| fi | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| cat "$RUNNER_TEMP/fight.txt" | ||
| if [ "$gate_hidden" = "true" ]; then | ||
| echo "::notice title=Squabble fight not evaluated::The supplied credential cannot read classic branch protection, so this advisory makes no claim about this pull request's reds. Broaden the Administration-read credential to enable classification. The hard chains gate still ran." | ||
| exit 0 | ||
| fi | ||
| if [ "$rc" -eq 2 ]; then | ||
| echo "::error::squabble fight failed to run — the report above is a failure, not an all-clear" | ||
| exit 1 | ||
| fi | ||
| if [ "$rc" -eq 3 ]; then | ||
| echo "::warning::squabble found no required_status_checks gate on the base branch — nothing to triage" | ||
| exit 0 | ||
| fi | ||
| if [ "$rc" -ne 0 ]; then | ||
| echo "::error::squabble fight exited $rc (unclassified) — treated as a tool failure" | ||
| exit 1 | ||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this reusable,
security-events: writeis on line 71, while line 70 iscontents: read. Because the dismissal matcher requires an exact path and line, the actual TokenPermissionsID alert will not match this entry; the newly added grounding check inscripts/tests/dismiss-code-scanning-alerts-test.shalso fails for this reason. Update this entry and its fixture to line 71.Useful? React with 👍 / 👎.