You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rescoped 2026-09-21. This issue originally also asked for a distinct exit
code for the "no gate" outcome. That is #99, which was already open when this
was filed and implements it properly (FetchError::NoGate, exit 3, with
tests asserting that no-gate and failure do not share a code). Those criteria
are struck below. What remains is the defect #99 explicitly scopes out.
The defect
fetch.rs asks exactly one question:
repos/{owner}/{repo}/rules/branches/{branch}
That endpoint returns ruleset rules only. A repository protected the classic way — repos/{owner}/{repo}/branches/{branch}/protection, with required_status_checks.contexts — has a real, enforced gate, and squabble
reports that it has none.
#99 is honest about this; its own comment says classic protection
"is invisible to this query", and it has a test
(the_no_gate_message_does_not_claim_the_branch_is_unprotected) whose purpose is
to stop the message overclaiming. That is the right interim behaviour. It is not
a fix.
Today the failure mode is a red misdiagnosis: a classically-protected repo
exits 2, the job fails, and somebody investigates.
After #99 plus its consumer, the same repo exits 3, and the consumer
(MetaManifold-WebUI.github/workflows/ci.yml, branch fix/gate-triage-no-gate) writes a green job with:
This job is green because nothing was triaged, not because a gate passed.
Merges into that branch are gated by no required status check.
On a classically-protected branch that second sentence is false, stated
confidently, in a green check. A wrong answer nobody investigates is worse than
a wrong answer that stops the line — this is the vacuous-gate pattern, arrived at
from the other direction.
So this should land soon after #99, not long after it.
Classic branch protection is read. When rules/branches/{branch} yields
no required_status_checks rule, fall back to branches/{branch}/protection
and honour required_status_checks.contexts. A branch protected either
way is reported as gated, and its contexts populate the gate identically.
A fixture per protection mechanism. Ruleset-gated, classic-protected,
both-at-once (union the contexts — GitHub enforces both), and genuinely
unprotected. The classic-protected fixture must assert exit 0and the
resulting contexts; asserting merely "not 3" does not close this.
The NoGate message stops carrying the caveat. Once both APIs are read, no required_status_checks ruleset rule applies can become the unqualified no required status checks apply, and fix(fetch): give "no gate" its own exit code so callers can tell it apart #99's the_no_gate_message_does_not_claim_the_branch_is_unprotected test is
updated to assert the stronger claim rather than guard against it.
⚠ A 404 from branches/{branch}/protection means not protected; a 403
means not visible to this token and must not be read as "no gate" — that
would reintroduce, at the API layer, exactly the conflation #99 just removed.
Provenance
Found while diagnosing hyperpolymath/MetaManifold-WebUI PR #6, where Gate triage (cicd-squabbler) was the only failing job in run 35632633129.
The base repo there is genuinely unprotected by both mechanisms (0 rulesets,
404 on /protection), so that PR is not itself an instance of this defect — it
is what exposed the code path.
The defect
fetch.rsasks exactly one question:That endpoint returns ruleset rules only. A repository protected the
classic way —
repos/{owner}/{repo}/branches/{branch}/protection, withrequired_status_checks.contexts— has a real, enforced gate, and squabblereports that it has none.
#99 is honest about this; its own comment says classic protection
"is invisible to this query", and it has a test
(
the_no_gate_message_does_not_claim_the_branch_is_unprotected) whose purpose isto stop the message overclaiming. That is the right interim behaviour. It is not
a fix.
Why this gets worse after #99, not better
Today the failure mode is a red misdiagnosis: a classically-protected repo
exits 2, the job fails, and somebody investigates.
After #99 plus its consumer, the same repo exits 3, and the consumer
(
MetaManifold-WebUI.github/workflows/ci.yml, branchfix/gate-triage-no-gate) writes a green job with:On a classically-protected branch that second sentence is false, stated
confidently, in a green check. A wrong answer nobody investigates is worse than
a wrong answer that stops the line — this is the vacuous-gate pattern, arrived at
from the other direction.
So this should land soon after #99, not long after it.
Acceptance criteria
A distinct exit code for "nothing in my domain".fix(fetch): give "no gate" its own exit code so callers can tell it apart #99.A machine-readable form of the no-gate outcome.fix(fetch): give "no gate" its own exit code so callers can tell it apart #99.rules/branches/{branch}yieldsno
required_status_checksrule, fall back tobranches/{branch}/protectionand honour
required_status_checks.contexts. A branch protected eitherway is reported as gated, and its contexts populate the gate identically.
fix(fetch): give "no gate" its own exit code so callers can tell it apart #99.diagnoserefuses a vacuous input.both-at-once (union the contexts — GitHub enforces both), and genuinely
unprotected. The classic-protected fixture must assert exit
0and theresulting contexts; asserting merely "not 3" does not close this.
NoGatemessage stops carrying the caveat. Once both APIs are read,no required_status_checks ruleset rule appliescan become the unqualifiedno required status checks apply, and fix(fetch): give "no gate" its own exit code so callers can tell it apart #99'sthe_no_gate_message_does_not_claim_the_branch_is_unprotectedtest isupdated to assert the stronger claim rather than guard against it.
⚠ A 404 from
branches/{branch}/protectionmeans not protected; a 403means not visible to this token and must not be read as "no gate" — that
would reintroduce, at the API layer, exactly the conflation #99 just removed.
Provenance
Found while diagnosing
hyperpolymath/MetaManifold-WebUIPR #6, whereGate triage (cicd-squabbler)was the only failing job in run35632633129.The base repo there is genuinely unprotected by both mechanisms (0 rulesets,
404 on
/protection), so that PR is not itself an instance of this defect — itis what exposed the code path.