Repository navigation
Description
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 amerge_queuerule (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
--apply(:92-101) resolves through the samefindByRow. No name match → itPOSTs a new ruleset. Main would then carry two active rulesets ("main merge queue", which still requiresmerge-gate, and the new one), and--rollback(:103-108) deletes only the new one..github/agent/merge-gate-ruleset.jsonis not a superset of the live gate: its name ismain merge gate, its required contexts aretests-ok,enqueue-guard,policy-sign— nomerge-gate, none of theNode 24context — and it carries nomerge_queuerule. So the moment the name matches,PUT-ing that file over25173372would drop the verdict gate from the required checks and delete the merge-queue rule. That is a downgrade, not convergence.
Expected
--checkidentifies the ruleset by what it governs, not by its display name:target === 'branch'andconditions.ref_name.includecontainingrefs/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 howmerge-gateand theNode 24context become visible instead of being silently dropped.--applyon a differently named ruleset governing the same ref isaction: 'update'against that id (never a second ruleset), and it refuses to write a body that would remove a live-required context or themerge_queuerule: it fails with the exact drift list instead of applying a downgrade.--rollbackdeletes the ruleset it updated, by the same ref-based match.
Scope (one PR)
Part 1, the defect:
.github/scripts/gate-ruleset.mjs: ref-basedfindByRow; two-way context drift inproblems();--applyupdates by ref and refuses a downgrading body;--rollbackby the same match..github/scripts/gate-ruleset.test.mjs: (a) a ruleset with a different name onrefs/heads/mainmust not reportmain is unprotectedand must name the live id; (b) a live ruleset that requires an extra context (merge-gate) yields a drift line, notok; (c)--applyon a name mismatch returnsaction:'update'and sends noPOST; (d)--applywhose 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.mjsis a gate file (policy/gates.json), so the PR needs aPolicy-sign: <comment id>from a non-author naming its exact tip;gate-ruleset.mjsis not a gate file,test.ymlis 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 formalVerdict: PASSat the exact tip plus independent FlyBot Test advisory. - Keep the repo's
.github/agent/merge-gate-ruleset.jsonuntouched 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