From 0f5d030cb10c6033f1624bd668d1d60d65552ac2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 21:37:53 +0100 Subject: [PATCH] =?UTF-8?q?fix(ci):=20sweep=20PRs=20with=20unknown=20merge?= =?UTF-8?q?ability=20=E2=80=94=20stuck=20merge-ref=20is=20the=20target=20p?= =?UTF-8?q?athology=20(#110945)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live verification showed dropped-CI PRs report mergeable=null and mergeable_state=unknown indefinitely: the stuck merge-ref computation is the same failure that dropped their CI, and close/reopen is what un-sticks it. The pending-mergeability skip therefore made the primary repair population permanently unsweepable (three real dropped PRs skipped across three consecutive sweeps). Keep skipping computed conflicts; a not-yet-computed conflict costs at most one budgeted re-fire. --- scripts/github/pr-ci-sweeper.mjs | 13 ++++++------- test/scripts/pr-ci-sweeper.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/github/pr-ci-sweeper.mjs b/scripts/github/pr-ci-sweeper.mjs index 30175755386f..4c414a28002f 100644 --- a/scripts/github/pr-ci-sweeper.mjs +++ b/scripts/github/pr-ci-sweeper.mjs @@ -37,15 +37,14 @@ export function classifyPrForSweep({ pr, ciRuns, botCloseCount, now }) { return { action: "skip", reason: "recently-updated" }; } // A conflicted PR legitimately has no merge ref; CI cannot attach until the - // author resolves, so re-firing would loop forever. + // author resolves, so re-firing would loop forever. Null/unknown mergeability + // is NOT skipped: live testing showed dropped-CI PRs stay mergeable=null + // indefinitely (the stuck merge-ref computation IS the pathology), and + // close/reopen is what un-sticks it. A not-yet-computed conflict costs at + // most one budgeted re-fire before the recomputed false skips it. if (pr.mergeable === false) { return { action: "skip", reason: "merge-conflict" }; } - // Pending computation resolves by the next hourly sweep; do not close/reopen - // on an unknown merge state. - if (pr.mergeable === null || pr.mergeable === undefined) { - return { action: "skip", reason: "mergeability-pending" }; - } // Closing a PR silently cancels enabled auto-merge and reopening does not // restore it; leave those PRs (e.g. generated locale refreshes) to a human. if (pr.auto_merge) { @@ -203,7 +202,7 @@ export async function runPrCiSweeper({ github, context, core, dryRun = false, ap fresh.state !== "open" || fresh.head.sha !== pr.head.sha || fresh.auto_merge || - fresh.mergeable !== true + fresh.mergeable === false ) { core.info(`pr-ci-sweeper: #${pr.number} changed during sweep; leaving it alone`); continue; diff --git a/test/scripts/pr-ci-sweeper.test.ts b/test/scripts/pr-ci-sweeper.test.ts index 5175f92c4145..777b8fe4d76d 100644 --- a/test/scripts/pr-ci-sweeper.test.ts +++ b/test/scripts/pr-ci-sweeper.test.ts @@ -113,9 +113,9 @@ describe("classifyPrForSweep", () => { expected: { action: "skip", reason: "refire-budget-exhausted" }, }, { - name: "skips while mergeability is still computing", + name: "re-fires on unknown mergeability (stuck merge-ref IS the pathology)", input: { pr: pr({ mergeable: null }), ciRuns: [], botCloseCount: 0, now: NOW }, - expected: { action: "skip", reason: "mergeability-pending" }, + expected: { action: "refire", reason: "ci-run-missing" }, }, ];