Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/actions/resolve-config/dispatch-limits.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
8 changes: 4 additions & 4 deletions .github/scripts/pr-unblock/dispatch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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`);
Expand Down
10 changes: 10 additions & 0 deletions .github/scripts/pr-unblock/fix-ci-dispatch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -95,6 +96,15 @@ const HELD_SKIP = "Skipped: awaiting-human";

const MAX_PAGES = 20;

// 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));
}

// 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.
Expand Down
12 changes: 11 additions & 1 deletion .github/scripts/pr-unblock/fix-ci-dispatch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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));
});
Loading