From 75e5498d7285d3a8793718fcce519a0be9696a31 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:00:02 -0500 Subject: [PATCH 1/3] perf(pr-pipeline): stop the dispatchers' FAIL-cap carry looking for a listing no gate saves The verdict reader reads the FAIL index by exact name once the legacy FAILs file loads, so pr-merge's gate never writes verify-listing.json and no pr-merge-verify-listing artifact has been uploaded since 03:22Z on 2026-11-03. verify-cap.mjs still ran listing-carry.mjs before the first FAIL-cap read of each address-feedback and pr-unblock dispatch job: one run listing and ten runs' artifact listings (MAX_RUNS), finding nothing, for a carried listing the index-mode reader never reads. Drop listing-carry.mjs from carrySteps and fetch the carry when the job names the run cache or head-ref map, and remove BABYSIT_VERIFY_LISTING_CARRY from both dispatch steps. Verdicts are unchanged: the reader reads the carry file only on its whole-listing path, and no such file has existed since the index turned on. Dispatch job 126277059871 (run 41739470521): listing-carry 11 calls, carry-run-cache 3, job total 45. Expected after: listing-carry 0, carry-run-cache about 5 (it now pays the run listing and the newest run's artifact listing the shared reads used to hold), about 9 fewer a job that reaches a FAIL-cap read. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MWmASQQUMmnivZUuzgYfbe --- ...rf-dispatch-verdict-exact-name-439816b1.md | 12 ++++ .github/scripts/pr-unblock/verify-cap.mjs | 22 ++++--- .../scripts/pr-unblock/verify-cap.test.mjs | 58 +++++++++++-------- .github/workflows/pr-feedback-dispatch.yml | 16 +++-- .github/workflows/pr-unblock-dispatch.yml | 16 +++-- 5 files changed, 74 insertions(+), 50 deletions(-) create mode 100644 .changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md diff --git a/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md b/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md new file mode 100644 index 00000000000..d6d86149ab8 --- /dev/null +++ b/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md @@ -0,0 +1,12 @@ +--- +bump: patch +--- + +### Changed + +- The address-feedback and pr-unblock dispatchers' FAIL-cap reads (`verify-cap.mjs`) no longer + look for the artifact listing pr-merge's last run saved. The verdict reader reads the FAIL + index by exact name, so no gate saves a listing to carry, and the search cost one run listing + and ten runs' artifact listings a job for nothing. The run cache and head-ref map are still + carried on the first FAIL-cap read; `BABYSIT_VERIFY_LISTING_CARRY` is gone from both dispatch + steps. diff --git a/.github/scripts/pr-unblock/verify-cap.mjs b/.github/scripts/pr-unblock/verify-cap.mjs index 03249c76060..9efc814e635 100644 --- a/.github/scripts/pr-unblock/verify-cap.mjs +++ b/.github/scripts/pr-unblock/verify-cap.mjs @@ -25,12 +25,15 @@ // record and a compare per FAIL and a download per untied FAIL: on 2026-10-31, // 458 FAILs were 1,252 reads, and every read was killed at // VERIFY_CAP_TIMEOUT_MS with its calls spent and no answer. The -// reads pr-merge carried answer them with no request: its listing -// (listing-carry.mjs), run cache (carry-run-cache.mjs) and head-ref map -// (head-ref-map.mjs), used as its gate uses them, only where they count a FAIL. -// They are fetched on the first read only, so a run that skips before any read -// spends none of their calls, and the run cache keeps what each read adds for -// the next. +// reads pr-merge carried answer them with no request: its run cache +// (carry-run-cache.mjs) and head-ref map (head-ref-map.mjs), used as its gate +// uses them, only where they count a FAIL. They are fetched on the first read +// only, so a run that skips before any read spends none of their calls, and +// the run cache keeps what each read adds for the next. pr-merge's artifact +// listing is not fetched: the reader reads the FAIL index by exact name +// (babysit_verify_evidence.py `index_mode`), so no gate saves a listing to +// carry, and looking for one cost one run listing and ten runs' artifact +// listings a job. // // A count miss on the carried listing takes the reader's waits, about 45 s a // read, and a read that ends in DEADLINE_UNREADABLE saves no listing, so the @@ -119,7 +122,7 @@ export const CARRY_TIMEOUT_MS = 2 * 60_000; export function carrySteps(env) { const shared = { PR_MERGE_READS_PATH: `${env.RUNNER_TEMP}/verify-carry-reads.json` }; const carriedFile = `${env.RUNNER_TEMP}/pr-merge-carried.json`; - const steps = [{ script: "listing-carry.mjs", args: [], env: { ...shared, API_BUDGET_SCRIPT: "listing-carry" } }]; + const steps = []; if (env.BABYSIT_RUN_CACHE) { steps.push( { script: "carry-run-cache.mjs", args: ["read"], env: { ...shared, API_BUDGET_SCRIPT: "carry-run-cache", CARRY_TO: carriedFile } }, @@ -191,7 +194,8 @@ function stderrSummary(stderr) { // merge.max-verify-fails (the lower of that commit's and main's). // `expected`: refusals the round is sent for, which warn nothing. // `carry`: fetches what pr-merge carried, before the first read only, when the -// job names BABYSIT_VERIFY_LISTING_CARRY; a failed fetch is no carry. +// job names BABYSIT_RUN_CACHE or BABYSIT_VERIFY_HEAD_REFS (`carrySteps`); a +// failed fetch is no carry. export function verifyCapReader(env, options) { const read = verifyReader(env, options); return async (request) => (await read(request)).cap; @@ -216,7 +220,7 @@ export function verifyReader(env, { spawn = spawnAsync, log = (line) => console. let carried = null; let deadlineMissed = false; return async ({ number, head, base, expected = [] }) => { - if (env.BABYSIT_VERIFY_LISTING_CARRY) { + if (carrySteps(env).length > 0) { carried ??= Promise.resolve() .then(() => carry(env)) .catch((error) => log(`::warning::pr-merge's carried reads were not all fetched (${oneLine(error?.message ?? error)}); the reader asks GitHub for the rest`)); diff --git a/.github/scripts/pr-unblock/verify-cap.test.mjs b/.github/scripts/pr-unblock/verify-cap.test.mjs index 88cc4478b7a..dc7e53da423 100644 --- a/.github/scripts/pr-unblock/verify-cap.test.mjs +++ b/.github/scripts/pr-unblock/verify-cap.test.mjs @@ -223,13 +223,12 @@ test("a refusal other than the caller limit keeps the waits for later reads", as assert.equal(calls[1].env[NO_WAITS], undefined); }); -// Both dispatchers' FAIL-cap reads take the listing pr-merge carried -// (listing-carry.mjs) as the first read's base, and share one saved listing -// across every pull request of the job, as pr-merge.yml's gate does; without -// them each read lists every artifact of the repository. The carry costs -// listing-carry.mjs's calls (5 or 6 a run, its api-calls notices on -// 2026-10-30), so it is fetched on the job's first FAIL-cap read, not by a -// step every run pays for. +// Both dispatchers' FAIL-cap reads take the run cache and head-ref map +// pr-merge carried, fetched on the job's first FAIL-cap read, not by a step +// every run pays for, and share one saved listing across every pull request +// of the job. They fetch no carried listing: the reader reads the FAIL index +// by exact name, so no gate saves one, and looking for it cost 11 calls a job +// (listing-carry's api-calls notice, dispatch job 126277059871 on 2026-11-03). const READER = readFileSync(new URL("../../../plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py", import.meta.url), "utf8"); const readerEnv = (name) => { const match = new RegExp(`^${name} = "([A-Z_]+)"$`, "m").exec(READER); @@ -259,10 +258,10 @@ for (const [workflow, dispatchName] of [ ["pr-feedback-dispatch.yml", "Dispatch address-feedback"], ["pr-unblock-dispatch.yml", "Dispatch pr-unblock for conflicting, review-tier Dependabot, failing lanes App and Markdown-changing PRs"], ]) { - test(`${workflow}: the carried listing is fetched on the first FAIL-cap read only, and every read shares one saved listing`, async () => { + test(`${workflow}: the carry is fetched on the first FAIL-cap read only, names no carried listing, and every read shares one saved listing`, async () => { const steps = dispatchSteps(workflow); assert.deepEqual( - steps.filter((s) => s.block.includes("run: node .base/.github/scripts/pr-merge/listing-carry.mjs")).map((s) => s.name), + steps.filter((s) => s.block.includes(".base/.github/scripts/pr-merge/")).map((s) => s.name), [], "no step fetches the carry before the dispatch knows it reads a FAIL cap", ); @@ -274,6 +273,7 @@ for (const [workflow, dispatchName] of [ const env = stepEnv(dispatch.block); assert.ok(env.VERIFY_EVIDENCE_SCRIPT, "the dispatch builds its FAIL-cap reader"); assert.equal(env.GITHUB_TOKEN, "${{ github.token }}", "the carry is fetched with the job's token"); + assert.equal(env[CARRY], undefined, "no gate saves a listing to carry"); const order = []; const read = verifyCapReader(env, { spawn: (command, args, options) => { @@ -290,9 +290,7 @@ for (const [workflow, dispatchName] of [ order.map(([kind]) => kind), ["carry", "read", "read"], ); - assert.equal(order[0][1][CARRY], "/runner/temp/verify-listing-carry.json"); for (const [, spawned] of order.slice(1)) { - assert.equal(spawned[CARRY], "/runner/temp/verify-listing-carry.json", "the reader takes the file listing-carry.mjs wrote"); assert.equal(spawned[LISTING], "/runner/temp/verify-listing.json", "one saved listing for the job's pull requests"); } }); @@ -311,7 +309,7 @@ for (const [workflow, dispatchName] of [ } assert.deepEqual( carrySteps({ ...env, RUNNER_TEMP: "/runner/temp" }).map(({ script, args }) => [script, ...args]), - [["listing-carry.mjs"], ["carry-run-cache.mjs", "read"], ["carry-run-cache.mjs"], ["head-ref-map.mjs", "read"]], + [["carry-run-cache.mjs", "read"], ["carry-run-cache.mjs"], ["head-ref-map.mjs", "read"]], ); }); } @@ -324,9 +322,9 @@ test("the carry starts the reader's run cache from pr-merge's file, shares its G const key = `${REPO}|${"a".repeat(40)}...main`; const env = { RUNNER_TEMP: dir, BABYSIT_RUN_CACHE: cache, BABYSIT_VERIFY_HEAD_REFS: join(dir, "head-refs-in.json") }; const steps = carrySteps(env); - const reads = steps.filter((step) => step.args[0] === "read" || step.script === "listing-carry.mjs"); + const reads = steps.filter((step) => step.args[0] === "read"); assert.equal(new Set(reads.map((step) => step.env.PR_MERGE_READS_PATH)).size, 1); - assert.deepEqual(reads.map((step) => step.env.API_BUDGET_SCRIPT), ["listing-carry", "carry-run-cache", "head-ref-map"]); + assert.deepEqual(reads.map((step) => step.env.API_BUDGET_SCRIPT), ["carry-run-cache", "head-ref-map"]); const start = steps.find((step) => step.script === "carry-run-cache.mjs" && step.args.length === 0); writeFileSync(start.env.CARRY_FROM, JSON.stringify({ "carried-on-branch": { [key]: { at: new Date().toISOString(), value: true } } })); @@ -337,8 +335,22 @@ test("the carry starts the reader's run cache from pr-merge's file, shares its G assert.equal(readFileSync(output, "utf8"), ""); }); -test("a job that names only the listing fetches only the listing", () => { - assert.deepEqual(carrySteps({ RUNNER_TEMP: "/t" }).map((step) => step.script), ["listing-carry.mjs"]); +test("the carry never fetches pr-merge's artifact listing, and a job that names only the head-ref map fetches only the map", () => { + assert.deepEqual(carrySteps({ RUNNER_TEMP: "/t", BABYSIT_VERIFY_LISTING_CARRY: "/t/verify-listing-carry.json" }), []); + assert.deepEqual( + carrySteps({ RUNNER_TEMP: "/t", BABYSIT_VERIFY_HEAD_REFS: "/t/head-refs-in.json" }).map(({ script, args }) => [script, ...args]), + [["head-ref-map.mjs", "read"]], + ); +}); + +test("a job that names only the listing carry fetches no carry", async () => { + let fetched = 0; + const read = verifyCapReader( + { GITHUB_REPOSITORY: REPO, BABYSIT_VERIFY_LISTING_CARRY: "/runner/temp/verify-listing-carry.json" }, + { spawn: () => answer({ refusal: null }), carry: () => (fetched += 1), log: () => {} }, + ); + assert.equal(await read({ number: 42, head: HEAD, base: BASE }), null); + assert.equal(fetched, 0); }); test("a carry that fails is fetched once, warns, and the reads go on without it", async () => { @@ -346,7 +358,7 @@ test("a carry that fails is fetched once, warns, and the reads go on without it" const lines = []; const reads = []; const read = verifyCapReader( - { GITHUB_REPOSITORY: REPO, BABYSIT_VERIFY_LISTING_CARRY: "/runner/temp/verify-listing-carry.json" }, + { GITHUB_REPOSITORY: REPO, BABYSIT_RUN_CACHE: "/runner/temp/babysit-run-cache.json" }, { spawn: (command, args) => { reads.push(args.at(-2)); @@ -367,20 +379,20 @@ test("a carry that fails is fetched once, warns, and the reads go on without it" assert.match(lines[0], /^::warning::pr-merge's carried reads were not all fetched \(HTTP 502\)/); }); -// The default carry runs listing-carry.mjs as a bounded child, as the step -// did: here it removes a stale file at the path and stops at the repository +// The default carry runs carry-run-cache.mjs as a bounded child, as the step +// did: here it removes a stale file at its path and stops at the repository // name, with no request. -test("the default carry runs listing-carry.mjs with the job's environment, then the read goes on", async () => { +test("the default carry runs carry-run-cache.mjs with the job's environment, then the read goes on", async () => { const dir = tempDir("verify-cap-"); - const path = join(dir, "verify-listing-carry.json"); + const path = join(dir, "pr-merge-carried.json"); writeFileSync(path, "stale"); const reads = []; const read = verifyCapReader( - { PATH: process.env.PATH, GITHUB_REPOSITORY: "not a repository", RUNNER_TEMP: dir, BABYSIT_VERIFY_LISTING_CARRY: path }, + { PATH: process.env.PATH, GITHUB_REPOSITORY: "not a repository", RUNNER_TEMP: dir, BABYSIT_RUN_CACHE: join(dir, "babysit-run-cache.json") }, { spawn: (command, args) => (reads.push(args.at(-2)), answer({ refusal: null })), log: () => {} }, ); assert.equal(await read({ number: 42, head: HEAD, base: BASE }), null); - assert.equal(existsSync(path), false, "listing-carry.mjs removed the stale file before reading"); + assert.equal(existsSync(path), false, "carry-run-cache.mjs removed the stale file before reading"); assert.deepEqual(reads, ["42"]); }); diff --git a/.github/workflows/pr-feedback-dispatch.yml b/.github/workflows/pr-feedback-dispatch.yml index 489cacc4ec9..9d8590ec996 100644 --- a/.github/workflows/pr-feedback-dispatch.yml +++ b/.github/workflows/pr-feedback-dispatch.yml @@ -414,17 +414,15 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} VERIFY_EVIDENCE_SCRIPT: .base/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py - # Each pull request's FAIL-cap read saves its listing here, and the - # next one reads it (or lists only the pages since it) instead of - # every page. The first starts from the listing pr-merge's last - # scheduled or hand-started run on main saved, which verify-cap.mjs - # fetches through listing-carry.mjs on that first read only, so a - # run that reads no FAIL cap spends none of its calls. + # The reader reads the FAIL index by exact name; a pull request with + # more index names than one whole listing costs reads that listing, + # saved here for the next such read instead of every page again. BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json # The FAIL run records, run heads on main and head refs pr-merge's - # last such run kept, fetched with the listing: the reader otherwise - # reads them for every FAIL in the repository, and each read shares + # last scheduled or hand-started run on main kept, which + # verify-cap.mjs fetches on the first FAIL-cap read only, so a run + # that reads no FAIL cap spends none of their calls: the reader + # otherwise reads them for every FAIL it counts, and each read shares # what it adds here. BABYSIT_RUN_CACHE: ${{ runner.temp }}/babysit-run-cache.json BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json diff --git a/.github/workflows/pr-unblock-dispatch.yml b/.github/workflows/pr-unblock-dispatch.yml index 4703e04de30..c3c9fc8ed28 100644 --- a/.github/workflows/pr-unblock-dispatch.yml +++ b/.github/workflows/pr-unblock-dispatch.yml @@ -235,17 +235,15 @@ jobs: API_BUDGET_TOKEN: github-token # fix-ci skips a PR whose pr-verify FAILs are at the cap (verify-cap.mjs). VERIFY_EVIDENCE_SCRIPT: .base/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py - # Each pull request's FAIL-cap read saves its listing here, and the - # next one reads it (or lists only the pages since it) instead of - # every page. The first starts from the listing pr-merge's last - # scheduled or hand-started run on main saved, which verify-cap.mjs - # fetches through listing-carry.mjs on that first read only, so a - # run that reads no FAIL cap spends none of its calls. + # The reader reads the FAIL index by exact name; a pull request with + # more index names than one whole listing costs reads that listing, + # saved here for the next such read instead of every page again. BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json # The FAIL run records, run heads on main and head refs pr-merge's - # last such run kept, fetched with the listing: the reader otherwise - # reads them for every FAIL in the repository, and each read shares + # last scheduled or hand-started run on main kept, which + # verify-cap.mjs fetches on the first FAIL-cap read only, so a run + # that reads no FAIL cap spends none of their calls: the reader + # otherwise reads them for every FAIL it counts, and each read shares # what it adds here. BABYSIT_RUN_CACHE: ${{ runner.temp }}/babysit-run-cache.json BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json From 54ad80020ef64a66e833eea2f1a64cc78bb08ac3 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:40:18 -0500 Subject: [PATCH 2/3] perf(pr-pipeline): remove every dead listing-carry lookup and the listing upload The verdict reader reads the FAIL index by exact name, so no gate saves an artifact listing and no pr-merge-verify-listing artifact exists to carry. Every remaining reader of that carry looked for one each run and found none: - pr-merge's merge job ran listing-carry.mjs after rotation, the head-ref map and the run-cache carry: about 9 run-artifact listings a run (runs 2 to 10 of MAX_RUNS; the run page and the newest run's artifacts were already in the job's shared reads); - enable-auto-merge ran it after the head-ref map: the same 9; - ci-status's merge-queue reader ran it in each lanes App merge_group run: 9, or 10 when the run page was not yet shared. Remove those steps and calls, every BABYSIT_VERIFY_LISTING_CARRY env line, and the merge job's pr-merge-verify-listing upload and its credential scan, which nothing reads now. With no caller left, listing-carry.mjs, its test, its floor row and retention entry go too; sync-hold-floor.py regenerates the hold-floor copies and the floor pins drop that one gate-class row. BABYSIT_VERIFY_LISTING stays wherever a reader can still save a listing (_targeted's whole-listing fallback, ci-status's pass-name file). Verdicts are unchanged: the reader reads a carried listing only on its whole-listing path, and no carry file has existed since index mode turned on. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MWmASQQUMmnivZUuzgYfbe --- ...rf-dispatch-verdict-exact-name-439816b1.md | 17 ++-- ...rf-dispatch-verdict-exact-name-5b2e91c4.md | 9 ++ .github/artifact-retention.json | 8 -- .../scripts/pr-merge/clear-escalations.mjs | 1 - .github/scripts/pr-merge/listing-carry.mjs | 88 ----------------- .../scripts/pr-merge/listing-carry.test.mjs | 98 ------------------- .github/scripts/pr-merge/merge-holds.mjs | 1 - .github/scripts/pr-merge/workflow.test.mjs | 15 ++- .github/workflows/pr-merge.yml | 60 ------------ .github/workflows/pr-require-checks.yml | 18 ++-- .github/workflows/pr-review-hosted.yml | 1 - .../workflows/pr-review-security-hosted.yml | 1 - docs/conventions/pr-pipeline.yaml | 2 - docs/conventions/pr-pipeline/README.md | 5 +- .../scripts/babysit_verify_evidence.py | 12 +-- .../scripts/tests/test_babysit_risk.py | 2 +- .../scripts/tests/test_floor_paths.py | 12 +-- .../scripts/tests/test_merge_holds_sync.py | 4 +- scripts/ci-lanes-pr-hold.test.sh | 20 ++-- 19 files changed, 64 insertions(+), 310 deletions(-) create mode 100644 .changes/source-control/perf-dispatch-verdict-exact-name-5b2e91c4.md delete mode 100755 .github/scripts/pr-merge/listing-carry.mjs delete mode 100644 .github/scripts/pr-merge/listing-carry.test.mjs diff --git a/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md b/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md index d6d86149ab8..d670bc98d37 100644 --- a/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md +++ b/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md @@ -4,9 +4,14 @@ bump: patch ### Changed -- The address-feedback and pr-unblock dispatchers' FAIL-cap reads (`verify-cap.mjs`) no longer - look for the artifact listing pr-merge's last run saved. The verdict reader reads the FAIL - index by exact name, so no gate saves a listing to carry, and the search cost one run listing - and ten runs' artifact listings a job for nothing. The run cache and head-ref map are still - carried on the first FAIL-cap read; `BABYSIT_VERIFY_LISTING_CARRY` is gone from both dispatch - steps. +- No workflow looks for the artifact listing pr-merge's last run saved any more. The verdict + reader reads the FAIL index by exact name, so no gate saves a listing, and each search cost + one run listing and up to ten runs' artifact listings for nothing. The address-feedback and + pr-unblock dispatchers' FAIL-cap reads (`verify-cap.mjs`), pr-merge's `merge` and + `enable-auto-merge` jobs, and ci-status's merge-queue reader no longer run it; the run cache + and head-ref map are still carried. `BABYSIT_VERIFY_LISTING_CARRY` is gone from every job. + +### Removed + +- `.github/scripts/pr-merge/listing-carry.mjs` and its floor row, and pr-merge's + `pr-merge-verify-listing` upload with its credential scan. diff --git a/.changes/source-control/perf-dispatch-verdict-exact-name-5b2e91c4.md b/.changes/source-control/perf-dispatch-verdict-exact-name-5b2e91c4.md new file mode 100644 index 00000000000..d212bff2acf --- /dev/null +++ b/.changes/source-control/perf-dispatch-verdict-exact-name-5b2e91c4.md @@ -0,0 +1,9 @@ +--- +bump: patch +--- + +### Changed + +- `babysit_verify_evidence.py` documents that no workflow names `BABYSIT_VERIFY_LISTING_CARRY` + now that index mode saves no listing to carry; the reader still takes one when named. The + floor-path pins drop the removed `listing-carry.mjs` row. diff --git a/.github/artifact-retention.json b/.github/artifact-retention.json index b47d6d80b49..fc0b918315b 100644 --- a/.github/artifact-retention.json +++ b/.github/artifact-retention.json @@ -117,14 +117,6 @@ "reason": "Read by the next run; 1 day is the minimum.", "scan": "upload" }, - "pr-merge-verify-listing": { - "retention-days": 1, - "readers": [ - ".github/scripts/pr-merge/listing-carry.mjs LISTING_ARTIFACT, from the newest trusted run" - ], - "reason": "Read by the next run as its listing base; 1 day is the minimum.", - "scan": "upload" - }, "pr-merge-head-refs": { "retention-days": 1, "readers": [ diff --git a/.github/scripts/pr-merge/clear-escalations.mjs b/.github/scripts/pr-merge/clear-escalations.mjs index 0525b937667..17a33c42691 100755 --- a/.github/scripts/pr-merge/clear-escalations.mjs +++ b/.github/scripts/pr-merge/clear-escalations.mjs @@ -166,7 +166,6 @@ // GITHUB_API_URL // GITHUB_RUN_NUMBER where the sweep starts (rotated) // BABYSIT_VERIFY_GH_TOKEN, BABYSIT_VERIFY_LISTING, BABYSIT_VERIFY_HEAD_REFS, -// BABYSIT_VERIFY_LISTING_CARRY, // BABYSIT_RUN_CACHE the gate reader's Actions token and the run's shared // reads, as enable-auto-merge passes them; the job // token also reads branch activity, check runs and diff --git a/.github/scripts/pr-merge/listing-carry.mjs b/.github/scripts/pr-merge/listing-carry.mjs deleted file mode 100755 index e2342f86d60..00000000000 --- a/.github/scripts/pr-merge/listing-carry.mjs +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env node -// listing-carry.mjs: hands the verdict reader the repository artifact listing -// the last run saved (babysit_verify_evidence.py BABYSIT_VERIFY_LISTING_CARRY), -// so it lists only the artifacts uploaded since (`Reader._incremental`) -// instead of every page. -// -// The file is the newest scheduled or hand-started run of main's pr-merge.yml -// (rotation.mjs `newestTrustedRun`), this run aside, holding the -// pr-merge-verify-listing artifact, and only when its head commit is on main -// (head-ref-map.mjs `onMain`), as the head-ref map and the carried run cache -// are read. Only a run's own steps can upload its artifacts, so the file was -// written by main's pr-merge.yml; it never goes through actions/cache, which -// any job in main's cache scope can write. Any file already at -// BABYSIT_VERIFY_LISTING_CARRY is removed first, so one left there is never -// read. The reader checks the file itself, and keeps a listing built on it -// only when it holds exactly the live artifact count GitHub reports; -// otherwise it lists every page. -// -// Any failure writes nothing and warns: the reader then lists every page. -// -// Environment: -// ACTIONS_TOKEN the job's GITHUB_TOKEN (actions: read) -// GITHUB_REPOSITORY owner/repo -// GITHUB_RUN_ID this run, which has no listing yet -// RUNNER_TEMP where the artifact is downloaded -// PR_MERGE_READS_PATH the job's shared GET answers -// (rotation.mjs sharedReads), if set -// PR_MERGE_MAIN_TIP main's tip (head-ref-map.mjs onMain), if set -// BABYSIT_VERIFY_LISTING_CARRY the file to write -// GITHUB_API_URL -import { renameSync, rmSync, writeFileSync } from "node:fs"; -import process from "node:process"; -import { pathToFileURL } from "node:url"; - -import { createGitHub } from "../../actions/check-trusted-trigger/check-trusted-trigger.mjs"; -import { onMain } from "./head-ref-map.mjs"; -import { ghDownload, newestTrustedRun, sharedReads } from "./rotation.mjs"; - -export const LISTING_ARTIFACT = "pr-merge-verify-listing"; -export const LISTING_FILE = "verify-listing.json"; -const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; - -const escapeCommand = (text) => String(text).replaceAll(/[\r\n%]/g, " "); -const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value); - -// The newest trusted run's saved listing and its run id, or undefined when no -// run has one; an error for a file of another shape. -export async function readListing({ repo, runId, actions: client, download, shared }) { - const actions = sharedReads(client, shared); - const accept = (run) => onMain({ repo, actions, sha: run.head_sha }); - const id = await newestTrustedRun({ repo, runId, actions, name: LISTING_ARTIFACT, accept }); - if (id === undefined) { - return undefined; - } - const text = download(id); - const data = JSON.parse(text); - if (!isObject(data) || typeof data.fetchedAt !== "string" || !Array.isArray(data.artifacts)) { - throw new Error(`the artifact listing of run ${id} has an unexpected shape`); - } - return { run: id, text, count: data.artifacts.length }; -} - -export async function main({ env = process.env, actions, download } = {}) { - const repo = env.GITHUB_REPOSITORY ?? ""; - const path = env.BABYSIT_VERIFY_LISTING_CARRY; - try { - if (!path) throw new Error("BABYSIT_VERIFY_LISTING_CARRY is not set"); - rmSync(path, { force: true }); - if (!REPO.test(repo)) throw new Error("GITHUB_REPOSITORY is not owner/repo"); - actions ??= createGitHub({ token: env.ACTIONS_TOKEN, apiUrl: env.GITHUB_API_URL || undefined }); - download ??= ghDownload({ repo, token: env.ACTIONS_TOKEN, dir: env.RUNNER_TEMP, name: LISTING_ARTIFACT, file: LISTING_FILE }); - const found = await readListing({ repo, runId: String(env.GITHUB_RUN_ID), actions, download, shared: env.PR_MERGE_READS_PATH }); - if (found === undefined) { - console.log("no earlier artifact listing; the verdict reader lists every page"); - return 0; - } - writeFileSync(`${path}.tmp`, found.text); - renameSync(`${path}.tmp`, path); - console.log(`artifact listing of run ${found.run}: ${found.count} artifacts`); - } catch (error) { - console.log(`::warning title=artifact listing::${escapeCommand(error?.message ?? error)}; the verdict reader lists every page`); - } - return 0; -} - -if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { - process.exitCode = await main(); -} diff --git a/.github/scripts/pr-merge/listing-carry.test.mjs b/.github/scripts/pr-merge/listing-carry.test.mjs deleted file mode 100644 index e6655663806..00000000000 --- a/.github/scripts/pr-merge/listing-carry.test.mjs +++ /dev/null @@ -1,98 +0,0 @@ -// Expected values come from the incremental listing design: the carried -// listing is read only from the newest scheduled or hand-started pr-merge.yml -// run on main other than this one (found as head-ref-map.mjs finds its map) -// whose head commit is on main, under the name pr-merge-verify-listing, never -// through actions/cache; a file already at the path is never read; any -// failure writes nothing and warns, so the reader lists every page. -import assert from "node:assert/strict"; -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { after, test } from "node:test"; - -import { forgetMainTip } from "./head-ref-map.mjs"; -import { LISTING_ARTIFACT, main } from "./listing-carry.mjs"; -import { mainCall } from "./on-main.fixture.mjs"; - -const REPO = "melodic-software/claude-code-plugins"; -const dir = mkdtempSync(join(tmpdir(), "listing-carry-")); -after(() => rmSync(dir, { recursive: true, force: true })); -const listingOf = (artifacts, extra = {}) => JSON.stringify({ fetchedAt: "2026-10-27T15:00:00+00:00", artifacts, ...extra }); -const sha = (id) => String(id).padStart(40, "a"); -const wf = (id, event, branch = "main") => ({ id, event, head_branch: branch, path: ".github/workflows/pr-merge.yml", head_sha: sha(id) }); - -let n = 0; -async function readRun({ pages = [[]], files = {}, offMain = [], fail, before } = {}) { - forgetMainTip(); - n += 1; - const path = join(dir, `carry-${n}.json`); - if (before !== undefined) writeFileSync(path, before); - const downloads = []; - const actions = async (method, url) => { - if (fail) throw fail; - const page = /\/actions\/workflows\/pr-merge\.yml\/runs\?branch=main&.*&page=(\d+)$/.exec(url)?.[1]; - if (page) return { workflow_runs: pages[Number(page) - 1] ?? [] }; - const onMain = mainCall(url, { offMain }); - if (onMain !== undefined) return onMain; - const id = /\/actions\/runs\/(\d+)\/artifacts\?per_page=100$/.exec(url)?.[1]; - assert.ok(id, `unexpected call ${url}`); - const artifacts = id in files ? [{ name: LISTING_ARTIFACT, expired: false }] : [{ name: "pr-merge-head-refs", expired: false }]; - return { total_count: artifacts.length, artifacts }; - }; - const download = (id) => { - downloads.push(id); - return files[id]; - }; - const lines = []; - const log = console.log; - console.log = (line) => lines.push(String(line)); - try { - assert.equal(await main({ env: { GITHUB_REPOSITORY: REPO, GITHUB_RUN_ID: "999", BABYSIT_VERIFY_LISTING_CARRY: path }, actions, download }), 0); - } finally { - console.log = log; - } - return { written: existsSync(path) ? readFileSync(path, "utf8") : undefined, downloads, lines }; -} - -test("the newest trusted run on main's listing is written as it was saved", async () => { - const kept = listingOf([{ id: 1 }, { id: 2 }]); - const result = await readRun({ - pages: [[wf(999, "schedule"), wf(9, "pull_request_target"), wf(8, "schedule"), wf(7, "workflow_dispatch"), wf(6, "schedule")]], - files: { 9: listingOf([{ id: 66 }]), 8: listingOf([]), 7: kept, 6: listingOf([]) }, - offMain: [sha(8)], - }); - // This run is skipped, a pull_request_target run is never asked, and run 8 - // is not on main. - assert.deepEqual(result.downloads, [7]); - assert.equal(result.written, kept); - assert.ok(result.lines.includes("artifact listing of run 7: 2 artifacts"), result.lines.join("\n")); -}); - -test("an untrusted run's listing alone writes nothing, so the reader lists every page", async () => { - const result = await readRun({ - pages: [[wf(9, "pull_request_target"), wf(8, "push"), wf(7, "schedule", "feature")]], - files: { 9: listingOf([]), 8: listingOf([]), 7: listingOf([]) }, - }); - assert.deepEqual(result.downloads, []); - assert.equal(result.written, undefined); -}); - -test("no earlier listing writes nothing, and removes a file left at the path", async () => { - const result = await readRun({ pages: [[wf(5, "schedule")]], before: listingOf([{ id: 1 }]) }); - assert.equal(result.written, undefined); - assert.deepEqual(result.lines, ["no earlier artifact listing; the verdict reader lists every page"]); -}); - -test("an unreadable or malformed listing writes nothing and warns", async () => { - for (const options of [ - { fail: new Error("HTTP 502") }, - { pages: [[wf(1, "schedule")]], files: { 1: "not json" } }, - { pages: [[wf(1, "schedule")]], files: { 1: JSON.stringify([]) } }, - { pages: [[wf(1, "schedule")]], files: { 1: listingOf({}) } }, - { pages: [[wf(1, "schedule")]], files: { 1: listingOf([], { fetchedAt: 1 }) } }, - ]) { - const result = await readRun({ ...options, before: listingOf([{ id: 1 }]) }); - assert.equal(result.written, undefined); - assert.ok(result.lines.some((line) => line.startsWith("::warning title=artifact listing::")), result.lines.join("\n")); - } -}); diff --git a/.github/scripts/pr-merge/merge-holds.mjs b/.github/scripts/pr-merge/merge-holds.mjs index 9def3ebf8e8..9b1160aa170 100755 --- a/.github/scripts/pr-merge/merge-holds.mjs +++ b/.github/scripts/pr-merge/merge-holds.mjs @@ -337,7 +337,6 @@ export const HOLD_PATH_CLASSES = Object.freeze([ [".github/scripts/pr-merge/who.mjs", "gate"], [".github/scripts/pr-merge/post-unarmed-status.mjs", "gate"], [".github/scripts/pr-merge/verify-answers.mjs", "gate"], - [".github/scripts/pr-merge/listing-carry.mjs", "gate"], [".github/actions/report-check-run/posting-run.mjs", "gate"], ["scripts/sync-hold-floor.py", "trust"], [".github/scripts/pr-unblock/address-feedback.mjs", "enforcement"], diff --git a/.github/scripts/pr-merge/workflow.test.mjs b/.github/scripts/pr-merge/workflow.test.mjs index 61b6d4f1b09..deb38bd69c2 100644 --- a/.github/scripts/pr-merge/workflow.test.mjs +++ b/.github/scripts/pr-merge/workflow.test.mjs @@ -808,8 +808,8 @@ test("enable-auto-merge skips its work when a waiting run's enable-auto-merge jo // answers in that job's RUNNER_TEMP; the readers' tests count what it saves. test("the merge and enable-auto-merge jobs' kept-file readers share their reads", () => { for (const [id, scripts] of [ - ["merge", ["rotation.mjs", "head-ref-map.mjs read", "lane-records.mjs read", "listing-carry.mjs"]], - ["enable-auto-merge", ["head-ref-map.mjs read", "lane-records.mjs read", "verify-answers.mjs", "listing-carry.mjs"]], + ["merge", ["rotation.mjs", "head-ref-map.mjs read", "carry-run-cache.mjs read", "lane-records.mjs read"]], + ["enable-auto-merge", ["head-ref-map.mjs read", "carry-run-cache.mjs read", "lane-records.mjs read", "verify-answers.mjs"]], ["post-merge-dispatch", ["lane-records.mjs read", "last-report.mjs"]], ]) { for (const script of scripts) { @@ -819,6 +819,15 @@ test("the merge and enable-auto-merge jobs' kept-file readers share their reads" } }); +// The verdict reader reads the FAIL index by exact name, so no run saves an +// artifact listing for the next: none is uploaded, looked for or named. +test("no job keeps, fetches or names a carried artifact listing", () => { + const text = read("pr-merge.yml"); + assert.doesNotMatch(text, /pr-merge-verify-listing/); + assert.doesNotMatch(text, /listing-carry/); + assert.doesNotMatch(text, /BABYSIT_VERIFY_LISTING_CARRY/); +}); + test("the last verifier read enable-auto-merge starts ends inside its step's timeout", () => { const arm = job("enable-auto-merge"); const step = /- name: Arm and disarm auto-merge\n {8}id: arm\n {8}if: .*\n {8}timeout-minutes: ([0-9]+)\n/.exec(arm); @@ -1511,7 +1520,7 @@ test("a dispatch that failed skips the record", () => { test("every credential scan has a timeout of at most 2 minutes, and the upload it guards needs its clean output", () => { const bestEffort = { "post-merge-dispatch": ["scan-upload", "scan-upload-5"], - merge: ["scan-upload-2", "scan-upload-3", "scan-upload-4", "scan-upload-5", "scan-upload-6", "scan-upload-7"], + merge: ["scan-upload-2", "scan-upload-3", "scan-upload-4", "scan-upload-5", "scan-upload-6"], "enable-auto-merge": ["scan-upload", "scan-upload-2"], "dispatch-full-run": [], }; diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml index 2e5be562edd..127ce89992d 100644 --- a/.github/workflows/pr-merge.yml +++ b/.github/workflows/pr-merge.yml @@ -1338,24 +1338,6 @@ jobs: CARRY_TO: ${{ runner.temp }}/babysit-run-cache.json run: node .base/.github/scripts/pr-merge/carry-run-cache.mjs - # The artifact listing the last run saved (the pr-merge-verify-listing - # artifact, found the way the head-ref map is): the verdict reader lists - # only the pages uploaded since and keeps the result only when it holds - # GitHub's live artifact count, else lists every page. Any error leaves - # no file, and the reader lists every page. Rules: - # .github/scripts/pr-merge/listing-carry.mjs and - # babysit_verify_evidence.py `Reader._incremental`. - - name: Read the artifact listing the last run kept - if: steps.candidates.outputs.listed == 'true' - continue-on-error: true - timeout-minutes: 2 - env: - API_BUDGET_TOKEN: github-token - ACTIONS_TOKEN: ${{ github.token }} - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json - PR_MERGE_READS_PATH: ${{ runner.temp }}/pr-merge-reads.json - run: node .base/.github/scripts/pr-merge/listing-carry.mjs - # The lane dispatch and re-run records earlier post-merge checks proved # (the pr-merge-lane-records artifact, found the way the cursor is): # each check below, the gate's included, answers those with no read and @@ -1580,7 +1562,6 @@ jobs: API_BUDGET_OVERRIDE_TOKEN: github-token BABYSIT_VERIFY_GH_TOKEN: ${{ github.token }} BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json REPO: ${{ github.repository }} RUNG_CAP: ${{ vars.CLAUDE_LANES_MERGE_RUNG_CAP }} MAX_MINUTES: ${{ steps.mode.outputs.skip-max-minutes }} @@ -1642,8 +1623,6 @@ jobs: # candidate the saved listing would allow lists again before it is # allowed. BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json - # The listing the last run kept, which a listing builds on. - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json # The head-ref map read above, if any. BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json # The lane records read and added to above, for a gate that checks @@ -2115,7 +2094,6 @@ jobs: CHECKS_TOKEN: ${{ github.token }} BABYSIT_VERIFY_GH_TOKEN: ${{ github.token }} BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json BABYSIT_RUN_CACHE: ${{ runner.temp }}/babysit-run-cache.json API_BUDGET_TOKEN: lanes-app @@ -2125,32 +2103,6 @@ jobs: TRUSTED_ACTORS_PATH: .base/.github/standards/trusted-actors/trusted-actors.json run: node .base/.github/scripts/pr-merge/clear-escalations.mjs - # The next run's base for its first listing: the run's last saved - # listing, written only by a listing that read whole (every page, or - # the pages since a base, holding GitHub's live count). Only a - # scheduled or hand-started run's file is read back, so only one - # uploads it. Rules: .github/scripts/pr-merge/listing-carry.mjs. - - name: Scan the upload for credentials - id: scan-upload-7 - if: >- - always() && steps.gate.outcome != 'skipped' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - continue-on-error: true - timeout-minutes: 2 - uses: ./.base/.github/actions/scan-upload # zizmor: ignore[self-repository] - with: - path: ${{ runner.temp }}/verify-listing.json - - - name: Keep the artifact listing for the next run - if: >- - (always() && steps.gate.outcome != 'skipped' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) && steps.scan-upload-7.outputs.clean == 'true' - continue-on-error: true - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: pr-merge-verify-listing - path: ${{ runner.temp }}/verify-listing.json - retention-days: 1 - if-no-files-found: ignore - # Under always(), not the lane token, so a run that stops before it mints # one (superseded, refused) still logs the job token's calls; a token # with no reading gets no line. After post-merge-dispatch's rate-limit @@ -2413,17 +2365,6 @@ jobs: PR_MERGE_READS_PATH: ${{ runner.temp }}/pr-merge-reads.json run: node .base/.github/scripts/pr-merge/head-ref-map.mjs read - - name: Read the artifact listing the last gate run kept - if: steps.lane-token.outputs.token != '' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - continue-on-error: true - env: - API_BUDGET_TOKEN: github-token - API_BUDGET_SCRIPT: listing-carry - ACTIONS_TOKEN: ${{ github.token }} - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json - PR_MERGE_READS_PATH: ${{ runner.temp }}/pr-merge-reads.json - run: node .base/.github/scripts/pr-merge/listing-carry.mjs - - name: Read the verdict reads the last run kept if: steps.lane-token.outputs.token != '' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') continue-on-error: true @@ -2622,7 +2563,6 @@ jobs: EVENT_PR: ${{ github.event.pull_request.number || inputs.pr }} BABYSIT_VERIFY_GH_TOKEN: ${{ github.token }} BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json BABYSIT_RUN_CACHE: ${{ runner.temp }}/babysit-run-cache.json # At most one whole-review request per run, as the merge job's gate diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index ea9026f7164..b3151660bd4 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -5099,7 +5099,6 @@ jobs: /plugins/source-control/lib/yaml-subset.awk /.github/scripts/pr-merge/head-ref-map.mjs /.github/scripts/pr-merge/carry-run-cache.mjs - /.github/scripts/pr-merge/listing-carry.mjs /.github/scripts/pr-merge/rotation.mjs /.github/actions/check-trusted-trigger/check-trusted-trigger.mjs /.github/actions/check-kill-switch/check-kill-switch.mjs @@ -6076,8 +6075,8 @@ jobs: echo "lanes=false" >>"$GITHUB_OUTPUT" echo "#$pr is not the lanes App's; the carry steps are skipped." fi - # MAIN'S TIP, LISTED ONCE FOR THE CARRY READERS. carry-run-cache.mjs, - # head-ref-map.mjs and listing-carry.mjs each run in their own process + # MAIN'S TIP, LISTED ONCE FOR THE CARRY READERS. carry-run-cache.mjs and + # head-ref-map.mjs each run in their own process # and take a run's carried reads only when its head commit is on main # (head-ref-map.mjs onMain), which otherwise reads the tag named main (a # 404 that no process shares) and main's tip over REST. One @@ -6212,9 +6211,9 @@ jobs: # AND NO FAIL PR-MERGE'S READER COUNTS: the step runs # babysit_verify_evidence.py `evidence --base-sha ` from # the .base checkout of main for the pull request at its head, the - # reader enable-auto-merge armed it on, on a listing of its own (every - # page, or the pages since pr-merge's last saved listing when they hold - # GitHub's live artifact count, saved to BABYSIT_VERIFY_LISTING) with + # reader enable-auto-merge armed it on, on reads of its own (the FAIL + # index by exact name, or with no legacy FAILs file every page of the + # listing, saved to BABYSIT_VERIFY_LISTING) with # pr-merge's head-ref map and carried run cache, so a FAIL uploaded # after enable-auto-merge's one listing of its run still holds. Its report goes to merge-holds.mjs # evidenceHold, which holds on every refusal but the three a pass decides @@ -6253,7 +6252,6 @@ jobs: PR_MERGE_SCRIPTS: .base/.github/scripts/pr-merge ACTIONS_TOKEN: ${{ github.token }} BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json - BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json HEAD_REFS_PATH: ${{ runner.temp }}/head-refs-in.json PR_MERGE_READS_PATH: ${{ runner.temp }}/pr-merge-reads.json @@ -6445,10 +6443,6 @@ jobs: # pr-merge's head-ref map: a read that fails leaves none, and the # reader downloads each untied FAIL instead. node "$PR_MERGE_SCRIPTS/head-ref-map.mjs" read || echo "::notice::no head-ref map from pr-merge's last run; the verdict reader reads each untied FAIL." - # pr-merge's last saved artifact listing: the reader lists only the - # pages since it and keeps that listing only when it holds GitHub's - # live artifact count, else lists every page, as with no file. - node "$PR_MERGE_SCRIPTS/listing-carry.mjs" || echo "::notice::no artifact listing from pr-merge's last run; the verdict reader lists every page." # A cold reader runs only with the headroom for it. With no carried # run cache, or one whose carried sections (carry-run-cache.mjs # carry) are all empty, the reader reads every FAIL: 860 @@ -6807,7 +6801,7 @@ jobs: held_paths() { # The trust-gate paths, as bash patterns (`*` crosses `/`). # BEGIN hold-floor hold_patterns: generated by scripts/sync-hold-floor.py - hold_patterns=('.github/standards/trusted-actors/*' 'CODEOWNERS' '*/CODEOWNERS' 'docs/conventions/pr-pipeline.yaml' '.gitmodules' '*/.gitmodules' 'plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_check_inputs.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_classify.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_dependabot.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_repo_config.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_trigger.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_run_cache.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_state.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_util.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_whole_review.py' '.claude/*' '*/.claude/*' 'docs/conventions/source-control.yaml' '.claude-lane/*' '*/.claude-lane/*' '.mcp.json' '*/.mcp.json' 'CLAUDE.md' '*/CLAUDE.md' 'CLAUDE.local.md' '*/CLAUDE.local.md' 'AGENTS.md' '*/AGENTS.md' 'REVIEW.md' '.github/actions/check-trusted-trigger/*' '.github/actions/resolve-config/*' '.github/actions/check-kill-switch/*' '.github/actions/request-lane-token/*' '.github/actions/log-api-budget/rate-guard.mjs' '.github/actions/log-api-budget/api-budget.bash' '.github/actions/log-api-budget/rate-limits.json' '.github/scripts/pr-merge/merge-holds.mjs' '.github/scripts/pr-merge/enable-auto-merge.mjs' '.github/scripts/pr-merge/superseded.mjs' '.github/scripts/pr-merge/clear-escalations.mjs' '.github/scripts/pr-merge/carry-run-cache.mjs' '.github/scripts/pr-merge/gate-check.mjs' '.github/scripts/pr-merge/head-ref-map.mjs' '.github/scripts/pr-merge/lane-records.mjs' '.github/scripts/pr-merge/last-report.mjs' '.github/scripts/pr-merge/rate-stop.bash' '.github/scripts/pr-merge/retry-call.bash' '.github/scripts/pr-merge/refire-refused.mjs' '.github/scripts/pr-merge/rotation.mjs' '.github/scripts/pr-merge/sweep-summary.mjs' '.github/scripts/pr-merge/who.mjs' '.github/scripts/pr-merge/post-unarmed-status.mjs' '.github/scripts/pr-merge/verify-answers.mjs' '.github/scripts/pr-merge/listing-carry.mjs' '.github/actions/report-check-run/posting-run.mjs' 'scripts/sync-hold-floor.py' '.github/scripts/pr-unblock/address-feedback.mjs' '.github/scripts/pr-unblock/escalation-routes.mjs' '.github/scripts/pr-unblock/escalation-routes.json' '.github/scripts/pr-unblock/gate-source.mjs' '.github/scripts/pr-unblock/thread-scope.mjs' '.github/scripts/pr-unblock/retry-fetch.mjs' '.github/scripts/work-resolve-issue/report-outcome.mjs' '.github/scripts/work-resolve-issue/limits.mjs' '.github/scripts/work-resolve-issue/retry-fetch.mjs' '.github/scripts/pr-unblock/report-escalation.sh' '.github/scripts/pr-unblock/adjudicate-feedback.mjs' '.github/scripts/pr-unblock/publish-feedback.mjs' '.github/scripts/pr-unblock/resolve-threads.mjs' '.github/scripts/pr-unblock/request-budget.mjs' '.github/scripts/pr-verify/resolve-pr.mjs' '.github/scripts/pr-verify/write-verdict.mjs' '.github/scripts/intake-triage/admit.mjs' '.github/actions/select-trusted-text/select-trusted-text.mjs' 'lib/publish-gate.mjs' '.github/recurring-schedule.json' 'plugins/work-items/scripts/charge-words.json' '.github/scripts/pr-unblock/dependency-classify.mjs' '.github/scripts/apply-patch/apply-patch.mjs' '.github/scripts/apply-patch/predicates.mjs' '.github/scripts/apply-patch/protected-paths.mjs' '.github/scripts/apply-patch/retry-fetch.mjs' '.github/scripts/pr-unblock/fix-ci-context.mjs' '.github/scripts/pr-unblock/feedback-dispatch.mjs' '.github/scripts/pr-unblock/verify-notes.mjs' '.github/scripts/pr-unblock/earlier-fail-notes.mjs' '.github/scripts/pr-unblock/limit-stop.mjs' '.github/scripts/lib/lane-outcome.mjs' '.github/scripts/pr-unblock/dispatch-record.mjs' '.github/scripts/pr-unblock/verify-cap.mjs' '.github/scripts/maintenance-reconcile-lost-runs/infra-failure.mjs' '.github/scripts/work-resolve-issue/stop-routes.json' '.github/scripts/work-resolve-issue/stop-routes.mjs' '.github/scripts/work-resolve-issue/known-names.json' '.github/scripts/lib/stop-reason.mjs' '.github/scripts/lib/admission-comment.mjs' '.github/scripts/lib/done-evidence.mjs' '.github/scripts/pr-review-main/*' 'docs/conventions/instruction-exception-register/*' 'docs/conventions/untrusted-content/*' 'docs/standards/*' 'docs/review/*' '.github/workflows/dependabot-fragments.yml' '.github/workflows/eval-browser-dispatch.yml' '.github/workflows/intake-reenter.yml' '.github/workflows/intake-triage.yml' '.github/workflows/maintenance-audit-lane-runs.yml' '.github/workflows/maintenance-reconcile-lost-runs.yml' '.github/workflows/maintenance-tidy-code-upkeep.yml' '.github/workflows/post-merge-demotion-alert.yml' '.github/workflows/post-merge-test-mutation.yml' '.github/workflows/pr-feedback-dispatch.yml' '.github/workflows/pr-merge.yml' '.github/workflows/pr-refine.yml' '.github/workflows/pr-require-checks.yml' '.github/workflows/pr-review-approved.yml' '.github/workflows/pr-review-comment-relay.yml' '.github/workflows/pr-review-hosted.yml' '.github/workflows/pr-review-main.yml' '.github/workflows/pr-review-security-hosted.yml' '.github/workflows/pr-run-activity-read.yml' '.github/workflows/pr-run-activity-write.yml' '.github/workflows/pr-unblock-dispatch.yml' '.github/workflows/pr-unblock.yml' '.github/workflows/pr-verify.yml' '.github/workflows/release-publish-plugins.yml' '.github/workflows/upstream-apply-changelog.yml' '.github/workflows/work-resolve-issue.yml' '.github/workflows/work-unblock.yml') + hold_patterns=('.github/standards/trusted-actors/*' 'CODEOWNERS' '*/CODEOWNERS' 'docs/conventions/pr-pipeline.yaml' '.gitmodules' '*/.gitmodules' 'plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_check_inputs.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_classify.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_dependabot.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_repo_config.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_review_trigger.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_run_cache.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_state.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_util.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py' 'plugins/source-control/skills/babysit-prs/scripts/babysit_whole_review.py' '.claude/*' '*/.claude/*' 'docs/conventions/source-control.yaml' '.claude-lane/*' '*/.claude-lane/*' '.mcp.json' '*/.mcp.json' 'CLAUDE.md' '*/CLAUDE.md' 'CLAUDE.local.md' '*/CLAUDE.local.md' 'AGENTS.md' '*/AGENTS.md' 'REVIEW.md' '.github/actions/check-trusted-trigger/*' '.github/actions/resolve-config/*' '.github/actions/check-kill-switch/*' '.github/actions/request-lane-token/*' '.github/actions/log-api-budget/rate-guard.mjs' '.github/actions/log-api-budget/api-budget.bash' '.github/actions/log-api-budget/rate-limits.json' '.github/scripts/pr-merge/merge-holds.mjs' '.github/scripts/pr-merge/enable-auto-merge.mjs' '.github/scripts/pr-merge/superseded.mjs' '.github/scripts/pr-merge/clear-escalations.mjs' '.github/scripts/pr-merge/carry-run-cache.mjs' '.github/scripts/pr-merge/gate-check.mjs' '.github/scripts/pr-merge/head-ref-map.mjs' '.github/scripts/pr-merge/lane-records.mjs' '.github/scripts/pr-merge/last-report.mjs' '.github/scripts/pr-merge/rate-stop.bash' '.github/scripts/pr-merge/retry-call.bash' '.github/scripts/pr-merge/refire-refused.mjs' '.github/scripts/pr-merge/rotation.mjs' '.github/scripts/pr-merge/sweep-summary.mjs' '.github/scripts/pr-merge/who.mjs' '.github/scripts/pr-merge/post-unarmed-status.mjs' '.github/scripts/pr-merge/verify-answers.mjs' '.github/actions/report-check-run/posting-run.mjs' 'scripts/sync-hold-floor.py' '.github/scripts/pr-unblock/address-feedback.mjs' '.github/scripts/pr-unblock/escalation-routes.mjs' '.github/scripts/pr-unblock/escalation-routes.json' '.github/scripts/pr-unblock/gate-source.mjs' '.github/scripts/pr-unblock/thread-scope.mjs' '.github/scripts/pr-unblock/retry-fetch.mjs' '.github/scripts/work-resolve-issue/report-outcome.mjs' '.github/scripts/work-resolve-issue/limits.mjs' '.github/scripts/work-resolve-issue/retry-fetch.mjs' '.github/scripts/pr-unblock/report-escalation.sh' '.github/scripts/pr-unblock/adjudicate-feedback.mjs' '.github/scripts/pr-unblock/publish-feedback.mjs' '.github/scripts/pr-unblock/resolve-threads.mjs' '.github/scripts/pr-unblock/request-budget.mjs' '.github/scripts/pr-verify/resolve-pr.mjs' '.github/scripts/pr-verify/write-verdict.mjs' '.github/scripts/intake-triage/admit.mjs' '.github/actions/select-trusted-text/select-trusted-text.mjs' 'lib/publish-gate.mjs' '.github/recurring-schedule.json' 'plugins/work-items/scripts/charge-words.json' '.github/scripts/pr-unblock/dependency-classify.mjs' '.github/scripts/apply-patch/apply-patch.mjs' '.github/scripts/apply-patch/predicates.mjs' '.github/scripts/apply-patch/protected-paths.mjs' '.github/scripts/apply-patch/retry-fetch.mjs' '.github/scripts/pr-unblock/fix-ci-context.mjs' '.github/scripts/pr-unblock/feedback-dispatch.mjs' '.github/scripts/pr-unblock/verify-notes.mjs' '.github/scripts/pr-unblock/earlier-fail-notes.mjs' '.github/scripts/pr-unblock/limit-stop.mjs' '.github/scripts/lib/lane-outcome.mjs' '.github/scripts/pr-unblock/dispatch-record.mjs' '.github/scripts/pr-unblock/verify-cap.mjs' '.github/scripts/maintenance-reconcile-lost-runs/infra-failure.mjs' '.github/scripts/work-resolve-issue/stop-routes.json' '.github/scripts/work-resolve-issue/stop-routes.mjs' '.github/scripts/work-resolve-issue/known-names.json' '.github/scripts/lib/stop-reason.mjs' '.github/scripts/lib/admission-comment.mjs' '.github/scripts/lib/done-evidence.mjs' '.github/scripts/pr-review-main/*' 'docs/conventions/instruction-exception-register/*' 'docs/conventions/untrusted-content/*' 'docs/standards/*' 'docs/review/*' '.github/workflows/dependabot-fragments.yml' '.github/workflows/eval-browser-dispatch.yml' '.github/workflows/intake-reenter.yml' '.github/workflows/intake-triage.yml' '.github/workflows/maintenance-audit-lane-runs.yml' '.github/workflows/maintenance-reconcile-lost-runs.yml' '.github/workflows/maintenance-tidy-code-upkeep.yml' '.github/workflows/post-merge-demotion-alert.yml' '.github/workflows/post-merge-test-mutation.yml' '.github/workflows/pr-feedback-dispatch.yml' '.github/workflows/pr-merge.yml' '.github/workflows/pr-refine.yml' '.github/workflows/pr-require-checks.yml' '.github/workflows/pr-review-approved.yml' '.github/workflows/pr-review-comment-relay.yml' '.github/workflows/pr-review-hosted.yml' '.github/workflows/pr-review-main.yml' '.github/workflows/pr-review-security-hosted.yml' '.github/workflows/pr-run-activity-read.yml' '.github/workflows/pr-run-activity-write.yml' '.github/workflows/pr-unblock-dispatch.yml' '.github/workflows/pr-unblock.yml' '.github/workflows/pr-verify.yml' '.github/workflows/release-publish-plugins.yml' '.github/workflows/upstream-apply-changelog.yml' '.github/workflows/work-resolve-issue.yml' '.github/workflows/work-unblock.yml') # merge.diff-check.path-holds is false: only Dependabot stays held. [ "$AUTHOR_ID" = 49699333 ] || hold_patterns=() # END hold-floor hold_patterns diff --git a/.github/workflows/pr-review-hosted.yml b/.github/workflows/pr-review-hosted.yml index e5aa70b66af..00bbb087380 100644 --- a/.github/workflows/pr-review-hosted.yml +++ b/.github/workflows/pr-review-hosted.yml @@ -311,7 +311,6 @@ jobs: .github/scripts/pr-merge/who.mjs .github/scripts/pr-merge/post-unarmed-status.mjs .github/scripts/pr-merge/verify-answers.mjs - .github/scripts/pr-merge/listing-carry.mjs .github/actions/report-check-run/posting-run.mjs scripts/sync-hold-floor.py .github/scripts/pr-unblock/address-feedback.mjs diff --git a/.github/workflows/pr-review-security-hosted.yml b/.github/workflows/pr-review-security-hosted.yml index 6ea1b0b094e..47d3611eb26 100644 --- a/.github/workflows/pr-review-security-hosted.yml +++ b/.github/workflows/pr-review-security-hosted.yml @@ -311,7 +311,6 @@ jobs: .github/scripts/pr-merge/who.mjs .github/scripts/pr-merge/post-unarmed-status.mjs .github/scripts/pr-merge/verify-answers.mjs - .github/scripts/pr-merge/listing-carry.mjs .github/actions/report-check-run/posting-run.mjs scripts/sync-hold-floor.py .github/scripts/pr-unblock/address-feedback.mjs diff --git a/docs/conventions/pr-pipeline.yaml b/docs/conventions/pr-pipeline.yaml index afed015085f..d57dbe626c8 100644 --- a/docs/conventions/pr-pipeline.yaml +++ b/docs/conventions/pr-pipeline.yaml @@ -1494,8 +1494,6 @@ merge: class: gate - glob: ".github/scripts/pr-merge/verify-answers.mjs" class: gate - - glob: ".github/scripts/pr-merge/listing-carry.mjs" - class: gate - glob: ".github/actions/report-check-run/posting-run.mjs" class: gate - glob: "scripts/sync-hold-floor.py" diff --git a/docs/conventions/pr-pipeline/README.md b/docs/conventions/pr-pipeline/README.md index 2125bff500b..1517950cf4f 100644 --- a/docs/conventions/pr-pipeline/README.md +++ b/docs/conventions/pr-pipeline/README.md @@ -2798,9 +2798,8 @@ head and commits, its head branch, its live issues and their other closing pull run they name and the pass names at each FAILed head; the file holds every FAIL from before the index. The module docstring's "Index mode" states the rule. Until then the listing below applies. A listing read from the API builds on the newest whole listing begun within three hours (the -reader's own, the run's saved one, or the `pr-merge-verify-listing` artifact the last scheduled or -hand-started run of main's pr-merge.yml uploaded, which `.github/scripts/pr-merge/listing-carry.mjs` -fetches as the head-ref map is fetched): it reads pages newest id first down past that listing's +reader's own, or the run's saved one; no run carries a listing to the next, since in index mode no +gate saves one): it reads pages newest id first down past that listing's highest id until an artifact created two hours before that listing was read, keeps that listing's entries below the lowest id those pages hold, less those expired by the first page's `Date`, and is kept only when it holds exactly that page's `total_count`. A count above it first reads again the pages diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py index 8f0bb2b4f3a..b29ae76b552 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py @@ -164,10 +164,9 @@ (a carried record counts the FAIL when it shows origin; any other answer from it could turn the FAIL away, so it is read again), with one exception: the artifact listing. A listing begun at most `LISTING_CARRY_MAX_AGE` ago (this -Reader's own, the run's saved one, or the one a scheduled or hand-started run -of main's pr-merge.yml saved and `LISTING_CARRY_ENV` names, which -.github/scripts/pr-merge/listing-carry.mjs fetches only from such a run whose -head is on main) is a base: `Reader._incremental` reads only the newest pages, +Reader's own, the run's saved one, or one a trusted earlier run saved that +`LISTING_CARRY_ENV` names, which no workflow does while index mode saves no +listing) is a base: `Reader._incremental` reads only the newest pages, newest id first, down past the base's highest id until an artifact created `LISTING_OVERLAP` before the base was read, and keeps the base's entries below the lowest id those pages hold, less those expired or past `expires_at` by the @@ -1288,8 +1287,9 @@ class Reader: listing_path: str | None = field( default_factory=lambda: os.environ.get(LISTING_ENV) or None ) - # The listing a trusted pr-merge run saved (`LISTING_CARRY_ENV`), which - # listing-carry.mjs fetched, as a base for `_incremental`. + # A listing an earlier trusted run saved (`LISTING_CARRY_ENV`), as a base + # for `_incremental`. No workflow names one: in index mode no gate saves + # a listing to carry. carry_path: str | None = field( default_factory=lambda: os.environ.get(LISTING_CARRY_ENV) or None ) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py index f45ef264d6f..6df15945856 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py @@ -294,7 +294,7 @@ def test_floor_paths_are_high_whatever_the_switch(self): # generated copies hold; here the count of each class. self.assertEqual( Counter(c for _, c in FLOOR.classes), - {"trust": 19, "gate": 56, "enforcement": 55}, + {"trust": 19, "gate": 55, "enforcement": 55}, ) self.assertEqual( FLOOR.high, diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_floor_paths.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_floor_paths.py index efc5c994527..3a5e8223e75 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_floor_paths.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_floor_paths.py @@ -90,21 +90,21 @@ class TheEffectiveFloorDidNotMove(unittest.TestCase): change updates these pins in the pull request that makes it.""" def test_each_glob_keeps_its_class(self): - self.assertEqual(len(FLOOR.classes), 130) + self.assertEqual(len(FLOOR.classes), 129) self.assertEqual( digest(f"{g}\t{c}" for g, c in FLOOR.classes), - "cdeefef08201b6670069794c9a5aca7eb8bbdd6d71e1b036f66d1bf86ee7d6f9", + "6c4c25f72ced0f83727f3c248bd4dc9ae57931576762bdfc08b524d6a61ab266", ) def test_each_clearing_route_keeps_its_globs(self): pins = { - FLOOR.high: "16c5e6c5b52d3411c15276ca410e19e0f7de907f2aa905d8f649e1d8d91bba51", + FLOOR.high: "0223d4c1863a0160fee263ec8ad773e0c002ed8dad02315f46bd024684f61544", FLOOR.review: "5264e103bf8c812a6ca19de2ca6b605d5f02bd7f92ab7af15189b688eb89662e", - FLOOR.author_review: "bef1d0d5a347fbacb2c09b5c35f610091605e05180e242c571354f52d940f275", + FLOOR.author_review: "52bceb8d569052c8d2e6ea56151f8b5bde9f6e28b316c04d532848e65b9a1f33", } self.assertEqual( (len(FLOOR.high), len(FLOOR.review), len(FLOOR.author_review)), - (75, 55, 16), + (74, 55, 15), ) for globs, pin in pins.items(): self.assertEqual(digest(globs), pin) @@ -170,7 +170,7 @@ def test_it_holds_only_what_every_consumer_shares(self): self.assertEqual(len(risk.FLOOR_CLASSES), 30) def test_every_other_row_sits_in_the_config(self): - self.assertEqual(len(ROWS), 130 - 30) + self.assertEqual(len(ROWS), 129 - 30) self.assertFalse(table(ROWS) & set(risk.FLOOR_CLASSES)) def test_the_config_every_consumer_reads_is_held_by_a_built_in_row(self): diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_merge_holds_sync.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_merge_holds_sync.py index 5fd967682cd..d0b54c89470 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_merge_holds_sync.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_merge_holds_sync.py @@ -420,7 +420,7 @@ def test_the_author_lift_is_the_gate_class_and_the_lanes_app_never_gets_it( if g not in surface and not g.startswith(".github/workflows/") ], ) - self.assertEqual(len(FLOOR.author_review), 16) + self.assertEqual(len(FLOOR.author_review), 15) samples = [g.removeprefix("**/").replace("**", "x.mjs") for g in gate] self.assertEqual(module(samples)["held"], [True] * len(samples)) step = (ROOT / ".github/workflows/pr-require-checks.yml").read_text( @@ -442,7 +442,7 @@ def test_the_person_floor_is_the_same_list(self) -> None: check=True, ) self.assertEqual(tuple(json.loads(proc.stdout)), FLOOR.high) - self.assertEqual(len(FLOOR.high), 75) + self.assertEqual(len(FLOOR.high), 74) def test_the_floor_holds_what_its_actions_load(self) -> None: files = import_closure(FLOOR_ENTRIES) diff --git a/scripts/ci-lanes-pr-hold.test.sh b/scripts/ci-lanes-pr-hold.test.sh index c22ba0eeb93..c9d730a918d 100755 --- a/scripts/ci-lanes-pr-hold.test.sh +++ b/scripts/ci-lanes-pr-hold.test.sh @@ -239,13 +239,6 @@ import process from "node:process"; appendFileSync(process.env.STUB_LOG, `head-ref-map ${process.argv.slice(2).join(" ")}\n`); process.exitCode = process.env.STUB_MAP_FAILS ? 1 : 0; EOF -# The listing carry stub logs `listing-carry`; it always exits 0, as the real -# one does after a warning. -cat >"$TMP_ROOT/pr-merge/listing-carry.mjs" <<'EOF' -import { appendFileSync } from "node:fs"; -import process from "node:process"; -appendFileSync(process.env.STUB_LOG, "listing-carry\n"); -EOF SHA=0123456789abcdef0123456789abcdef01234567 BASE=89abcdef0123456789abcdef0123456789abcdef @@ -418,7 +411,6 @@ for line in 'VERIFY_EVIDENCE_SCRIPT: .base/plugins/source-control/skills/babysit 'PR_MERGE_SCRIPTS: .base/.github/scripts/pr-merge' \ 'ACTIONS_TOKEN: ${{ github.token }}' \ 'BABYSIT_VERIFY_LISTING: ${{ runner.temp }}/verify-listing.json' \ - 'BABYSIT_VERIFY_LISTING_CARRY: ${{ runner.temp }}/verify-listing-carry.json' \ 'BABYSIT_VERIFY_HEAD_REFS: ${{ runner.temp }}/head-refs-in.json' \ 'HEAD_REFS_PATH: ${{ runner.temp }}/head-refs-in.json' \ 'BABYSIT_RUN_CACHE: ${{ runner.temp }}/babysit-run-cache.json' \ @@ -432,6 +424,13 @@ for line in 'VERIFY_EVIDENCE_SCRIPT: .base/plugins/source-control/skills/babysit fail "the step does not set $line" fi done +# No gate saves an artifact listing to carry: the reader reads the FAIL index +# by exact name, so the step names no carried listing and runs no carry for one. +if [[ "$step" != *BABYSIT_VERIFY_LISTING_CARRY* && "$step" != *listing-carry* ]]; then + ok "the step names no carried artifact listing" +else + fail "the step still names or fetches a carried artifact listing" +fi step_of() { awk -v name=" - name: $1" '$0 == name { on = 1; print; next } on && /^ - / { exit } on { print }' "$WORKFLOW"; } @@ -464,9 +463,8 @@ else fi sparse="$(awk '/sparse-checkout: \|/ { on = 1; next } on && /^ \// { print substr($0, 13); next } on { exit }' <<<"$base_step")" want_sparse=(/.node-version '/plugins/source-control/skills/babysit-prs/scripts/*.py' /plugins/source-control/skills/babysit-prs/scripts/verify-legacy-fails.json /plugins/source-control/lib/parse-concern-value.sh /plugins/source-control/lib/yaml-subset.awk) -# Every module head-ref-map.mjs, carry-run-cache.mjs and listing-carry.mjs -# import, transitively. -queue=(.github/scripts/pr-merge/head-ref-map.mjs .github/scripts/pr-merge/carry-run-cache.mjs .github/scripts/pr-merge/listing-carry.mjs) +# Every module head-ref-map.mjs and carry-run-cache.mjs import, transitively. +queue=(.github/scripts/pr-merge/head-ref-map.mjs .github/scripts/pr-merge/carry-run-cache.mjs) while ((${#queue[@]})); do file=${queue[0]} queue=("${queue[@]:1}") From 3eb156de782f0e0f213e6ec05a838a457b928bf0 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:09:32 -0500 Subject: [PATCH 3/3] docs(pr-pipeline): say index mode still saves a listing past the names cap The changelog fragment and the reader's carried-reads comment said no gate saves a listing in index mode. A pull request past INDEX_NAMES_CAP index names takes the whole-listing path, and that path saves one, so the carry was rarely live, not dead. Past that cap a dispatcher's FAIL-cap read now lists in full (independent security review, P5). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MWmASQQUMmnivZUuzgYfbe --- .../perf-dispatch-verdict-exact-name-439816b1.md | 6 ++++-- .../skills/babysit-prs/scripts/babysit_verify_evidence.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md b/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md index d670bc98d37..dadfd7a29d3 100644 --- a/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md +++ b/.changes/pr-pipeline/perf-dispatch-verdict-exact-name-439816b1.md @@ -5,8 +5,10 @@ bump: patch ### Changed - No workflow looks for the artifact listing pr-merge's last run saved any more. The verdict - reader reads the FAIL index by exact name, so no gate saves a listing, and each search cost - one run listing and up to ten runs' artifact listings for nothing. The address-feedback and + reader reads the FAIL index by exact name and saves a listing only for a pull request past + `INDEX_NAMES_CAP` index names, so each search cost one run listing and up to ten runs' + artifact listings and almost always found nothing. Past that cap a dispatcher's FAIL-cap + read now lists in full. The address-feedback and pr-unblock dispatchers' FAIL-cap reads (`verify-cap.mjs`), pr-merge's `merge` and `enable-auto-merge` jobs, and ci-status's merge-queue reader no longer run it; the run cache and head-ref map are still carried. `BABYSIT_VERIFY_LISTING_CARRY` is gone from every job. diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py index eaf53e80698..93d1fa8bd3b 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py @@ -165,8 +165,8 @@ it could turn the FAIL away, so it is read again), with one exception: the artifact listing. A listing begun at most `LISTING_CARRY_MAX_AGE` ago (this Reader's own, the run's saved one, or one a trusted earlier run saved that -`LISTING_CARRY_ENV` names, which no workflow does while index mode saves no -listing) is a base: `Reader._incremental` reads only the newest pages, +`LISTING_CARRY_ENV` names, which no workflow sets; index mode saves a listing +only past `INDEX_NAMES_CAP`) is a base: `Reader._incremental` reads only the newest pages, newest id first, down past the base's highest id until an artifact created `LISTING_OVERLAP` before the base was read, and keeps the base's entries below the lowest id those pages hold, less those expired or past `expires_at` by the