From fd9e40a8e373366861899bff2787b3420c7496ad Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:18:13 -0500 Subject: [PATCH 1/3] fix(pr-unblock): keep fix-ci off standards-sync and release PRs fix-ci adopted every trusted account but Dependabot, and treats every bot-authored PR as idle, so a red standards-sync PR got a lane commit (#15429, 80d79d1) that check-managed-files then refuses. Adoption now leaves out the same authors fix-docs already skips: Dependabot, the release App and the standards-sync App, from one shared list. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7 --- .github/scripts/pr-unblock/dispatch.mjs | 8 ++++---- .github/scripts/pr-unblock/fix-ci-dispatch.mjs | 10 ++++++++++ .github/scripts/pr-unblock/fix-ci-dispatch.test.mjs | 12 +++++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/scripts/pr-unblock/dispatch.mjs b/.github/scripts/pr-unblock/dispatch.mjs index bffcfe29fbc..d42ee4245b7 100644 --- a/.github/scripts/pr-unblock/dispatch.mjs +++ b/.github/scripts/pr-unblock/dispatch.mjs @@ -55,7 +55,7 @@ import { emptyRecord, isHandled, markHandled, pruneRecord, readRecord, RUN_ID, s import { laneReader, withLaneBinding } from "./address-feedback.mjs"; import { prHold, reviewReader } from "./gate-source.mjs"; import { runTitle, shareWorkflowRuns } from "./feedback-dispatch.mjs"; -import { DEFAULTS as CI_FIX_DEFAULTS, dispatchCiFixes, humanGatedLabel } from "./fix-ci-dispatch.mjs"; +import { adoptableOwnerIds, DEFAULTS as CI_FIX_DEFAULTS, dispatchCiFixes, humanGatedLabel } from "./fix-ci-dispatch.mjs"; import { TRACKER_BINDING_PATH, trackerBindingText } from "../pr-merge/merge-holds.mjs"; import { HISTORY_QUERY as DOCS_FIX_HISTORY_QUERY } from "./fix-docs-dispatch.mjs"; import { anyHandles, cappedAt, headLagStop, laneStop, limitStop, pendingStop, permanentlyHandles, releasedBy, stopRelease } from "./limit-stop.mjs"; @@ -884,15 +884,15 @@ async function dispatchAll(env, { client, sleep, log, loadLimits, loadAdoption, // human-gated label is read only when owners are adopted; an unreadable // tracker config holds the adopted owner PRs alone, as a failure. // With `adoption.idle-hours` set (as pr-merge requires), every account on - // the base's trusted-actors list is adoptable, by id, but Dependabot: - // review-dependency owns its red PRs. + // the base's trusted-actors list is adoptable, by id, but the bot authors + // fix-ci-dispatch.mjs adoptableOwnerIds leaves out. let fixes = { dispatched: [], failed: 1 }; let ownerIds = []; let holdLabels = CI_FIX_DEFAULTS.holdLabels; let ownersHeld = ""; if (adoption["idle-hours"] !== undefined) { try { - ownerIds = [...loadTrustedList(`${basePath(env)}/${TRUSTED_ACTORS_PATH}`).ids].filter((id) => id !== DEPENDABOT_ID); + ownerIds = adoptableOwnerIds(loadTrustedList(`${basePath(env)}/${TRUSTED_ACTORS_PATH}`).ids); holdLabels = [...new Set([...holdLabels, baseHumanGatedLabel(env)])]; } catch (error) { log(`::error::cannot read the base trusted-actors list or the human-gated label from the base tracker binding (${error.message}); no adopted PR gets fix-ci`); diff --git a/.github/scripts/pr-unblock/fix-ci-dispatch.mjs b/.github/scripts/pr-unblock/fix-ci-dispatch.mjs index 8121d49adfa..49ae4792c69 100644 --- a/.github/scripts/pr-unblock/fix-ci-dispatch.mjs +++ b/.github/scripts/pr-unblock/fix-ci-dispatch.mjs @@ -57,6 +57,7 @@ import { postingAttempt } from "../../actions/report-check-run/posting-run.mjs"; import { emptyRecord, isHandled, markHandled, sendDispatch } from "./dispatch-record.mjs"; import { STOP_RESOLVED } from "./escalation-routes.mjs"; import { HOLD_LINE, runTitle } from "./feedback-dispatch.mjs"; +import { DEFAULTS as DOCS_FIX_DEFAULTS } from "./fix-docs-dispatch.mjs"; import { failingChecks } from "./fix-ci-context.mjs"; import { prHold, reviewReader } from "./gate-source.mjs"; import { HEAD_LAG_STOP, KILL_SWITCH_STOP, killSwitchStop, laneStop, releasesHead } from "./limit-stop.mjs"; @@ -98,6 +99,15 @@ const MAX_PAGES = 20; // The tracker's human-gated label from the binding's JSON text, as // pr-merge.yml resolves it: config.role_labels["human-gated"] trimmed, or // needs-human when the file is absent (null) or maps none. Invalid JSON throws. +// The trusted accounts whose idle PRs fix-ci adopts: every one but the authors +// fix-docs also leaves alone (Dependabot, whose red PRs review-dependency owns; +// the release and standards-sync Apps, whose PRs carry generated or synced +// files that a lane commit breaks: check-managed-files refuses a sync PR +// commit by any other author). +export function adoptableOwnerIds(trustedIds, excludedAuthorIds = DOCS_FIX_DEFAULTS.excludedAuthorIds) { + return [...trustedIds].filter((id) => !excludedAuthorIds.includes(id)); +} + export function humanGatedLabel(text) { if (text === null) { return "needs-human"; diff --git a/.github/scripts/pr-unblock/fix-ci-dispatch.test.mjs b/.github/scripts/pr-unblock/fix-ci-dispatch.test.mjs index 3b66000f48c..4a1d4fd0130 100644 --- a/.github/scripts/pr-unblock/fix-ci-dispatch.test.mjs +++ b/.github/scripts/pr-unblock/fix-ci-dispatch.test.mjs @@ -6,7 +6,7 @@ import { createGitHubApi, LIST_QUERY } from "./dispatch.mjs"; import { failingChecks } from "./gate-source.mjs"; import { emptyRecord, isHandled } from "./dispatch-record.mjs"; import { RERUN_DUE_MINUTES } from "../maintenance-reconcile-lost-runs/infra-failure.mjs"; -import { DEFAULTS, dispatchCiFixes, humanGatedLabel, skipReason, staleStop } from "./fix-ci-dispatch.mjs"; +import { adoptableOwnerIds, DEFAULTS, dispatchCiFixes, humanGatedLabel, skipReason, staleStop } from "./fix-ci-dispatch.mjs"; // The lanes App bot's user id and the GitHub Actions app id // (.github/standards/trusted-actors/trusted-actors.json; dispatch.mjs). @@ -1552,3 +1552,13 @@ test("a fix-ci stop is left on a head with a check failing or going, a run since const older = await run({ pages: [[settled(green)]], workflowRuns: [{ id: 2, status: "completed", display_title: "pr-unblock fix-ci #9", created_at: "2026-10-22T10:00:00Z" }] }); assert.equal(older.api.calls.dispatch.length, 1); }); + +test("adoption leaves out Dependabot and the release and standards-sync Apps, whose PRs a lane commit breaks", () => { + const trusted = JSON.parse(readFileSync(new URL("../../standards/trusted-actors/trusted-actors.json", import.meta.url), "utf8")); + const ids = trusted.actors.map((actor) => actor.id); + const adopted = adoptableOwnerIds(new Set(ids)); + const excluded = [49699333, 337859524, 300666570]; + assert.ok(excluded.every((id) => ids.includes(id))); + assert.deepEqual(adopted, ids.filter((id) => !excluded.includes(id))); + assert.ok(adopted.includes(APP)); +}); From 4e340f758e284b716dd2aa9948e7ad13f7cb706a Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:23:45 -0500 Subject: [PATCH 2/3] fix(pr-unblock): put humanGatedLabel's comment back on it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7 --- .github/scripts/pr-unblock/fix-ci-dispatch.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/pr-unblock/fix-ci-dispatch.mjs b/.github/scripts/pr-unblock/fix-ci-dispatch.mjs index 49ae4792c69..9aa17f9bb13 100644 --- a/.github/scripts/pr-unblock/fix-ci-dispatch.mjs +++ b/.github/scripts/pr-unblock/fix-ci-dispatch.mjs @@ -96,9 +96,6 @@ const HELD_SKIP = "Skipped: awaiting-human"; const MAX_PAGES = 20; -// The tracker's human-gated label from the binding's JSON text, as -// pr-merge.yml resolves it: config.role_labels["human-gated"] trimmed, or -// needs-human when the file is absent (null) or maps none. Invalid JSON throws. // The trusted accounts whose idle PRs fix-ci adopts: every one but the authors // fix-docs also leaves alone (Dependabot, whose red PRs review-dependency owns; // the release and standards-sync Apps, whose PRs carry generated or synced @@ -108,6 +105,9 @@ export function adoptableOwnerIds(trustedIds, excludedAuthorIds = DOCS_FIX_DEFAU return [...trustedIds].filter((id) => !excludedAuthorIds.includes(id)); } +// The tracker's human-gated label from the binding's JSON text, as +// pr-merge.yml resolves it: config.role_labels["human-gated"] trimmed, or +// needs-human when the file is absent (null) or maps none. Invalid JSON throws. export function humanGatedLabel(text) { if (text === null) { return "needs-human"; From 798829c2fb1ed910ea123be98d5a6ecc67a5671a Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:44:46 -0500 Subject: [PATCH 3/3] test(resolve-config): pin that fix-ci never adopts release or sync PRs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7 --- .../actions/resolve-config/dispatch-limits.test.mjs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/actions/resolve-config/dispatch-limits.test.mjs b/.github/actions/resolve-config/dispatch-limits.test.mjs index 22b3ef90d3f..04f6e3fa283 100644 --- a/.github/actions/resolve-config/dispatch-limits.test.mjs +++ b/.github/actions/resolve-config/dispatch-limits.test.mjs @@ -155,11 +155,19 @@ test("the base config's adoption section lets fix-ci adopt an idle owner PR", as owner.commits.nodes[0].commit.committedDate = idle; assert.equal(parse(LIVE).adoption["idle-hours"], 2, "the live config adopts a trusted person after 2 hours"); assert.deepEqual(await run(LIVE, [owner]), { code: 0, dispatched: ["40", "61"] }); - // Every trusted-actors.json entry is adoptable: the release App, a Bot, at once. + // Another trusted Bot is adoptable at once (a Bot never waits out idle-hours). + const bot = ciFixPull(64); + bot.author = { __typename: "Bot", databaseId: 209825114 }; + bot.updatedAt = "2026-10-17T11:59:00Z"; + assert.deepEqual(await run(LIVE, [bot]), { code: 0, dispatched: ["40", "64"] }); + // The release and standards-sync Apps' PRs carry generated or synced files a lane commit breaks: never adopted. const release = ciFixPull(65); release.author = { __typename: "Bot", databaseId: 337859524 }; release.updatedAt = "2026-10-17T11:59:00Z"; - assert.deepEqual(await run(LIVE, [release]), { code: 0, dispatched: ["40", "65"] }); + assert.deepEqual(await run(LIVE, [release]), { code: 0, dispatched: ["40"] }); + const sync = ciFixPull(68); + sync.author = { __typename: "Bot", databaseId: 300666570 }; + assert.deepEqual(await run(LIVE, [sync]), { code: 0, dispatched: ["40"] }); // Dependabot's red PR is review-dependency's, never fix-ci's. const dependabot = ciFixPull(67); dependabot.author = { __typename: "Bot", databaseId: 49699333 };