From f7a3e51f77b88195c1687b208bffe6d15efc8d09 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:15:30 +0100 Subject: [PATCH 1/2] ci(triage): handle squabble's no-gate exit code instead of failing on it --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c35ce6..bfafe0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -567,12 +567,62 @@ jobs: # `squabble fetch` and `squabble fight` both take / # ; the job-level gate above guarantees a PR is present. + # + # Exit codes, fixed by SHA pin so their meaning cannot drift underneath: + # 0 a gate exists and was fetched -> triage it + # 3 the base branch has no `required_status_checks` ruleset rule + # -> there is nothing to triage. A finding, not a breakage. + # * anything else is a real failure and still fails this job. + # + # Before hyperpolymath/cicd-squabbler#99 every one of those was exit 2, so + # this step could not tell "nothing to triage" from "squabble is broken". + # It went red on a legitimate non-finding, and the only alternative was to + # swallow rc=2 -- which would have muted genuine breakage along with it. + # Discriminating on the message, or inferring the state from the error + # code, would both be guesses; the producer answers it instead. - name: Fetch the live gate for this PR + id: fetch run: | - "$SQUABBLE" fetch "${{ github.repository }}" "${{ github.event.pull_request.number }}" > gate.json - echo "--- gate.json ---"; cat gate.json + set +e + "$SQUABBLE" fetch "${{ github.repository }}" \ + "${{ github.event.pull_request.number }}" > gate.json 2> fetch.err + rc=$? + set -e + cat fetch.err >&2 + case "$rc" in + 0) + echo "has_gate=true" >> "$GITHUB_OUTPUT" + echo "--- gate.json ---"; cat gate.json + ;; + 3) + echo "has_gate=false" >> "$GITHUB_OUTPUT" + rm -f gate.json + { + echo "## Gate triage: no gate to triage" + echo + echo "\`squabble fetch\` exited 3. Base branch \`${{ github.event.pull_request.base.ref }}\`" + echo "of \`${{ github.repository }}\` carries no \`required_status_checks\` ruleset rule," + echo "so there is no gate to squabble over and triage was skipped." + echo + echo "**This job is green because nothing was triaged, not because a gate passed.**" + echo "Merges into that branch are gated by no required status check." + echo + echo "Classic branch protection is a separate API and is not visible to this query," + echo "so this says nothing about it." + } | tee triage-outcome.md >> "$GITHUB_STEP_SUMMARY" + echo "::warning title=No gate to triage::base branch has no required_status_checks ruleset rule -- triage skipped, nothing was verified" + ;; + *) + # The redirect leaves a 0-byte gate.json even when the fetch + # failed; uploading it would look like an empty gate was fetched. + rm -f gate.json + echo "::error title=squabble fetch failed::exit $rc -- this is a real failure, not a missing gate" + exit "$rc" + ;; + esac - name: Diagnose the gate + if: steps.fetch.outputs.has_gate == 'true' run: | "$SQUABBLE" diagnose gate.json | tee squabble-diagnose.txt @@ -580,6 +630,7 @@ jobs: # cannot legitimately land — that is a finding to report, not a build break, # and failing here would mask the very deadlock we are trying to surface. - name: Fight (propose only — never commits, pushes or re-runs CI) + if: steps.fetch.outputs.has_gate == 'true' run: | "$SQUABBLE" fight "${{ github.repository }}" "${{ github.event.pull_request.number }}" \ --repo-root . --json > squabble-fight.json || true @@ -594,4 +645,5 @@ jobs: gate.json squabble-diagnose.txt squabble-fight.json + triage-outcome.md if-no-files-found: ignore From 4af3d1d3af54e1bbf6c972c795a59f0cab06490c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:00:18 +0100 Subject: [PATCH 2/2] ci(triage): re-pin squabble to the revision that emits exit 3 The preceding commit branches on `squabble fetch` exit 3 for "the base branch has no required_status_checks rule". That code did not exist at the pinned revision, so the fix was inert: rc=2 fell into the `*` arm and hard-failed exactly as before. An ordering trap, and one that looks identical to a working fix from the diff alone. hyperpolymath/cicd-squabbler#99 merged at 2026-09-21T18:55Z and is what introduced `FetchError::NoGate` and `ExitCode::from(e.exit_code())`. Re-pinning onto its merge commit is what makes the preceding commit live. The window also carries #94..#98 -- CI permissions indentation, two DEED grammar gate fixes, an actions.lock reconcile, and a harden-runner bump. None touches squabble's CLI behaviour. The pin comment now records the FLOOR rather than just the value, because moving this pin backwards would silently revert the exit-code handling below without touching a line of it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfafe0b..f2d595e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -546,7 +546,13 @@ jobs: env: GH_TOKEN: ${{ github.token }} # Pinned, matching this file's convention for every other external ref. - SQUABBLER_SHA: 6be52e3489783669fef29ef9228022a6439819bd + # FLOOR: this pin must be at or after hyperpolymath/cicd-squabbler#99 + # (merged 2026-09-21T18:55Z as 9846169c), which is what introduced the + # distinct exit 3 the Fetch step below branches on. At any earlier + # revision "no gate" is exit 2, falls into the `*` arm, and hard-fails -- + # i.e. moving this pin backwards silently reverts the fix below without + # touching it. Bump it forwards freely; never behind 9846169c. + SQUABBLER_SHA: 9846169c1dc8e549edd72fa20efc6680d324d484 SQUABBLE: /tmp/squabbler/target/release/squabble steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4