Skip to content

bug(gate): gate-ruleset --check finds the ruleset by name, so the live "main merge queue" reads as "main is unprotected" #2314

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: b8b0a4710cdf4d50a73973fcad984080
Branch: dev/3-ruleset-by-ref
Base: a11983c
At: 2026-11-03T15:03:19Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead

What is wrong

.github/scripts/gate-ruleset.mjs finds the gate ruleset by its display name: RULESET_NAME = 'main merge gate' (:28), findByRow() compares row.name only (:84-86), and main({ mode: 'check' }) calls findByRow(listRulesets(...)) (:111). The only active ruleset on this repository is named main merge queue (id 25173372), so --check takes the "not found" branch (:112) and reports the opposite of the truth:

gate-ruleset: no ruleset named "main merge gate" on refs/heads/main: main is unprotected

with ok = false, while main is gated.

Reproduction (read-only, live main a11983c5bf8cfbe8da391c201e520750ac2b48eb):

  • GET /repos/ThinkFlowLab/FlyBot/rulesets → [{"id":25173372,"name":"main merge queue","target":"branch","enforcement":"active"}]
  • GET /repos/ThinkFlowLab/FlyBot/rulesets/25173372 → active, target branch, conditions.ref_name.include = ["refs/heads/main"], bypass_actors = [], required contexts ["ubuntu-latest · Node 24 · DSH 0.2.0-rc.2", "tests-ok", "merge-gate"], plus a merge_queue rule (SQUASH, max_entries_to_merge 1).
  • Feeding those two real responses into main({ mode: 'check', get }) produces the line above.

Two same-root failures beyond --check

  1. --apply (:92-101) resolves through the same findByRow. No name match → it POSTs a new ruleset. Main would then carry two active rulesets ("main merge queue", which still requires merge-gate, and the new one), and --rollback (:103-108) deletes only the new one.
  2. .github/agent/merge-gate-ruleset.json is not a superset of the live gate: its name is main merge gate, its required contexts are tests-ok, enqueue-guard, policy-sign — no merge-gate, none of the Node 24 context — and it carries no merge_queue rule. So the moment the name matches, PUT-ing that file over 25173372 would drop the verdict gate from the required checks and delete the merge-queue rule. That is a downgrade, not convergence.

Expected

  • --check identifies the ruleset by what it governs, not by its display name: target === 'branch' and conditions.ref_name.include containing refs/heads/main; the name is used only in output.
  • problems() reports drift in both directions: a context the file wants that the live ruleset lacks, and a live-required context the file does not carry — that is how merge-gate and the Node 24 context become visible instead of being silently dropped.
  • --apply on a differently named ruleset governing the same ref is action: 'update' against that id (never a second ruleset), and it refuses to write a body that would remove a live-required context or the merge_queue rule: it fails with the exact drift list instead of applying a downgrade.
  • --rollback deletes the ruleset it updated, by the same ref-based match.

Scope (one PR)

Part 1, the defect:

  • .github/scripts/gate-ruleset.mjs: ref-based findByRow; two-way context drift in problems(); --apply updates by ref and refuses a downgrading body; --rollback by the same match.
  • .github/scripts/gate-ruleset.test.mjs: (a) a ruleset with a different name on refs/heads/main must not report main is unprotected and must name the live id; (b) a live ruleset that requires an extra context (merge-gate) yields a drift line, not ok; (c) --apply on a name mismatch returns action:'update' and sends no POST; (d) --apply whose body would drop a live-required context refuses and sends no request. Every existing assertion in the file stays green.
  • changelog.d/<N>-ruleset-by-ref.fixed.md; docs only if they name the ruleset.

Part 2, same slice, small (P-507): policy-sign never says which comment can sign the tip. judge() already receives comments (policy-sign.mjs:52), but the "body carries no Policy-sign: line" branch (:59) and the "does not name this tip" branch (:80) return one sentence with no remedy, while the three signability criteria are right there (:66-70, :78-80). Add signable: [{ id, login }] (at most 3; non-author, user.type === 'User', body naming the current tip — factor the existing predicate once instead of copying it) to those two refusals, and have main() print one line per candidate: policy-sign: comment <id> by <login> signs this tip — add `Policy-sign: <id>` to the body. Exit status and green/red do not change; existing assertions stay green, plus: candidates are listed when one exists, and the PR author's own comment and a comment naming an older tip are not listed.

Not in scope

Do not add enqueue-guard or policy-sign to the live ruleset 25173372 — that edit is the owner's, tracked on #2124. This issue only makes the tool read and write the right ruleset, and stops it from downgrading one.

Notes for the PR

  • policy-sign.mjs is a gate file (policy/gates.json), so the PR needs a Policy-sign: <comment id> from a non-author naming its exact tip; gate-ruleset.mjs is not a gate file, test.yml is not touched.
  • Tier is computed from the changed paths, not assumed: .github/scripts/** on this tree does not land T1, and the change alters the signature checker's own output, so plan on T2/T3 — a formal Verdict: PASS at the exact tip plus independent FlyBot Test advisory.
  • Keep the repo's .github/agent/merge-gate-ruleset.json untouched unless a workflow needs it; the body-vs-live drift this issue exposes is what the owner's bug(ci): deleting the signature step from test.yml still passes — the gate has no required context of its own #2124 edit will finally settle.

Dedupe

On live main a11983c5: "main merge gate" appears in exactly two files (.github/scripts/gate-ruleset.mjs, .github/agent/merge-gate-ruleset.json). Closed issues checked: #2124 (deleting the signature job still merges), #639 (merge-gate not required), #812 / #829 (building the gate), #960 / #2093 / #1317 (queue and signature coverage), #1015 / #1016 (docs point at the wrong repository), #2304 (policy/gates.json). None is this one: nothing compares the tool's expected ruleset name against the ruleset that is actually active.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions