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
7 changes: 7 additions & 0 deletions .changes/pr-pipeline/perf-postmerge-idle-tick-5c2e81d4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Changed

- pr-merge's post-merge-dispatch job reads the post-merge report that the last scheduled or hand-started run kept. It reads only a run on main and finds it the way it finds the lane records (`.github/scripts/pr-merge/last-report.mjs`). On an idle tick it reuses that report and does not run "Check earlier lane merges". It keeps the report's `computedAt`, exit code and code commit, so a chain of reuses ends `SHARED_MAX_AGE` after the check that made the report. Any refusal or failed read runs the check as before. `last-report.mjs` is gate code on the risk floor.
9 changes: 9 additions & 0 deletions .changes/source-control/perf-postmerge-idle-tick-5c2e81d4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
bump: patch
---

### Changed

- `babysit_verify_evidence.py settle --idle-tick` lets pr-merge's post-merge-dispatch job reuse the post-merge report the last tick kept. The report must pass the lane room's reuse rule (`settle_shared`) and `babysit_postmerge.idle_refusal`. That rule takes a passing report only (exit 0 or 5) with no hold, no full run or re-run to start, no unattributed red run and no quarantine excuse, and at least 10 minutes left before `SHARED_MAX_AGE`. It also requires that since the report no verifying run completed, no quarantine issue changed, and every pin the check read reads the same again. The post-merge report now records those pins (`pins`) and when the earliest canary still running in a merge's window was created (`outstandingSince`), which can still turn red and fail a verified merge. Both `idle_refusal` and `pending_runs_unchanged` list verifying runs back to that canary, and refuse a report without the field.
- `pending_runs_unchanged` lists runs once for each verifying workflow instead of once for every workflow on the branch. On main, the all-workflow listing held 152 runs in under two hours, more than fit on one page.
- The reuse rule (`shared_refusal`) refuses a report once a quarantine excuse it carries has expired (`excuse-expired`).
72 changes: 72 additions & 0 deletions .github/scripts/pr-merge/last-report.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env node
// last-report.mjs: the post-merge report the last pr-merge tick kept (its
// post-merge-dispatch job's pr-merge-post-merge-report artifact), which this
// tick's post-merge-dispatch job reuses in place of its own check while
// nothing the check reads has moved (babysit_verify_evidence.py settle
// --idle-tick). Found as the lane records are (lane-records.mjs): the newest
// scheduled or hand-started run of main's pr-merge.yml, this run aside,
// holding the artifact unexpired (rotation.mjs `newestTrustedRun`), whose head
// commit is on main (`onMain`). Only a run's own steps upload its artifacts,
// so the files were written by main's pr-merge.yml. Any failure leaves no
// report and warns: the check then runs.
//
// Environment:
// ACTIONS_TOKEN the job's GITHUB_TOKEN (actions: read)
// GITHUB_REPOSITORY owner/repo
// GITHUB_RUN_ID this run, whose report is not written yet
// PR_MERGE_READS_PATH the job's shared GET answers (rotation.mjs sharedReads), if set
// GITHUB_API_URL
// LAST_REPORT_DIR where the report's files are written
import { execFileSync } from "node:child_process";
import { renameSync, rmSync } 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 { newestTrustedRun, sharedReads } from "./rotation.mjs";

export const REPORT_ARTIFACT = "pr-merge-post-merge-report";
const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;

const escapeCommand = (text) => String(text).replaceAll(/[\r\n%]/g, " ");

export async function main({ env = process.env, actions, download } = {}) {
const repo = env.GITHUB_REPOSITORY ?? "";
const dir = env.LAST_REPORT_DIR;
try {
if (!dir) throw new Error("LAST_REPORT_DIR is not set");
// A report left in its place before this step is never read as one.
rmSync(dir, { recursive: true, force: true });
if (!REPO.test(repo)) throw new Error("GITHUB_REPOSITORY is not owner/repo");
actions = sharedReads(
actions ?? createGitHub({ token: env.ACTIONS_TOKEN, apiUrl: env.GITHUB_API_URL || undefined }),
env.PR_MERGE_READS_PATH,
);
download ??= (id, target) =>
execFileSync("gh", ["run", "download", String(id), "-R", repo, "-n", REPORT_ARTIFACT, "-D", target], {
env: { ...process.env, GH_TOKEN: env.ACTIONS_TOKEN },
stdio: ["ignore", "ignore", "inherit"],
});
const accept = (run) => onMain({ repo, actions, sha: run.head_sha });
const id = await newestTrustedRun({ repo, runId: String(env.GITHUB_RUN_ID), actions, name: REPORT_ARTIFACT, accept });
if (id === undefined) {
console.log("no earlier post-merge report; the post-merge check runs");
return 0;
}
// Downloaded beside its place and moved in whole, so a failed download
// leaves no report.
const part = `${dir}.part`;
rmSync(part, { recursive: true, force: true });
download(id, part);
renameSync(part, dir);
console.log(`the post-merge report of run ${id}`);
} catch (error) {
console.log(`::warning title=last post-merge report::${escapeCommand(error?.message ?? error)}; the post-merge check runs`);
}
return 0;
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
process.exitCode = await main();
}
81 changes: 81 additions & 0 deletions .github/scripts/pr-merge/last-report.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Expected values come from the trust rule last-report.mjs shares with the
// lane records (lane-records.mjs): the report is read only from the newest
// scheduled or hand-started pr-merge.yml run on main other than this one,
// holding the artifact unexpired, whose head commit is on main; any failure
// leaves no report.
import assert from "node:assert/strict";
import { existsSync, mkdirSync, 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 { main } from "./last-report.mjs";
import { mainCall } from "./on-main.fixture.mjs";

const REPO = "melodic-software/claude-code-plugins";
const dir = mkdtempSync(join(tmpdir(), "last-report-"));
after(() => rmSync(dir, { recursive: true, force: true }));
const sha = (id) => String(id).padStart(40, "a");
const wf = (id, event, head_sha = sha(id)) => ({ id, event, head_branch: "main", path: ".github/workflows/pr-merge.yml", head_sha });

let n = 0;
// `runs` is the pr-merge.yml run listing, newest first; `held` lists the runs
// holding the report artifact; `offMain` the head commits not on main.
async function readLast({ runs = [], held = [], offMain = [], downloadFails = false, planted = false } = {}) {
forgetMainTip();
n += 1;
const out = join(dir, `last-${n}`);
if (planted) {
mkdirSync(out);
writeFileSync(join(out, "post-merge.json"), "planted");
}
const downloads = [];
const actions = async (method, url) => {
if (/\/actions\/workflows\/pr-merge\.yml\/runs\?branch=main&.*&page=1$/.test(url)) return { workflow_runs: runs };
const onMain = mainCall(url, { offMain });
if (onMain !== undefined) return onMain;
const id = Number(/\/actions\/runs\/(\d+)\/artifacts\?per_page=100$/.exec(url)?.[1]);
assert.ok(id, `unexpected call ${url}`);
const artifacts = held.includes(id) ? [{ name: "pr-merge-post-merge-report", expired: false }] : [];
return { total_count: artifacts.length, artifacts };
};
const download = (id, target) => {
downloads.push(id);
if (downloadFails) throw new Error("download 502");
mkdirSync(target);
writeFileSync(join(target, "post-merge.json"), `report of ${id}`);
};
const lines = [];
const log = console.log;
console.log = (line) => lines.push(String(line));
try {
await main({ env: { GITHUB_REPOSITORY: REPO, GITHUB_RUN_ID: "999", LAST_REPORT_DIR: out }, actions, download });
} finally {
console.log = log;
}
const file = join(out, "post-merge.json");
return { downloads, lines, report: existsSync(file) ? readFileSync(file, "utf8") : undefined };
}

test("the newest trusted run's report on main is read, this run and other events aside", async () => {
const result = await readLast({
runs: [wf(999, "schedule"), wf(8, "pull_request_target"), wf(7, "schedule"), wf(6, "workflow_dispatch")],
held: [999, 8, 7, 6],
offMain: [sha(7)],
});
// Run 7's head commit is off main, so run 6 (hand-started) is read.
assert.deepEqual(result.downloads, [6]);
assert.equal(result.report, "report of 6");
});

test("no earlier report, or one that fails to download, leaves none, even one planted", async () => {
const none = await readLast({ runs: [wf(7, "schedule")], held: [], planted: true });
assert.deepEqual(none.downloads, []);
assert.equal(none.report, undefined);
const failed = await readLast({ runs: [wf(7, "schedule")], held: [7], planted: true });
assert.equal(failed.report, "report of 7");
const broken = await readLast({ runs: [wf(7, "schedule")], held: [7], downloadFails: true, planted: true });
assert.equal(broken.report, undefined);
assert.match(broken.lines.join("\n"), /^::warning title=last post-merge report::download 502; the post-merge check runs$/m);
});
1 change: 1 addition & 0 deletions .github/scripts/pr-merge/merge-holds.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export const HOLD_PATH_CLASSES = Object.freeze([
[".github/scripts/pr-merge/gate-check.mjs", "gate"],
[".github/scripts/pr-merge/head-ref-map.mjs", "gate"],
[".github/scripts/pr-merge/lane-records.mjs", "gate"],
[".github/scripts/pr-merge/last-report.mjs", "gate"],
[".github/scripts/pr-merge/rate-stop.bash", "gate"],
[".github/scripts/pr-merge/refire-refused.mjs", "gate"],
[".github/scripts/pr-merge/rotation.mjs", "gate"],
Expand Down
99 changes: 99 additions & 0 deletions .github/scripts/pr-merge/merge-job.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ test("the re-dispatch for earlier lane merges waits on neither the listing nor t
"Check the kill switch",
"Read report-only",
"Read the lane records the last run kept",
"Read the post-merge report the last run kept",
"Check earlier lane merges",
"Keep the post-merge report for enable-auto-merge",
"Start the full CI run on main",
Expand Down Expand Up @@ -526,6 +527,104 @@ test("the early check fails red on a rate-limit stop, after its outputs and repo
assert.equal(run.keptCode, "75");
});

// post-merge-dispatch's check, run with the last run's report in
// post-merge-last (or none) and a fake python3: `settle` answers whether the
// report stands (exit `settle`), any other call is the check, which marks that
// it ran. The rule (pr-merge.yml): an idle tick reuses a report whose check
// passed (exit 0 or 5) and that `settle --idle-tick` keeps
// (babysit_verify_evidence.py, tested in test_babysit_lane_room.py
// IdleRefusal and SettleCommand), keeping its code and commit so the reuse
// ends SHARED_MAX_AGE after the check that made it; otherwise it runs the
// check.
const LAST_SHA = "d".repeat(40);
const lastReport = () => ({ readable: true, ok: true, needsDispatch: false, shared: { mainSha: "a".repeat(40) } });
const runIdleCheck = ({ last, code = "0", settle = 0, exit = 3 } = {}) => {
const dir = mkdtempSync(join(tmpdir(), "pr-merge-idle-"));
try {
const bin = join(dir, "bin");
mkdirSync(bin);
writeFileSync(
join(bin, "python3"),
`#!/usr/bin/env bash\nif [ "$2" = settle ]; then echo "$@" >"${dir}/settled"; exit ${settle}; fi\ntouch "${dir}/ran"\necho '{"ran": true}'\nexit ${exit}\n`,
);
chmodSync(join(bin, "python3"), 0o755);
symlinkSync(root, join(dir, ".base"));
writeFileSync(join(dir, "lane-records.json"), "read in");
if (last !== undefined) {
mkdirSync(join(dir, "post-merge-last"));
writeFileSync(join(dir, "post-merge-last/post-merge.json"), JSON.stringify(last));
writeFileSync(join(dir, "post-merge-last/post-merge-code"), `${code}\n`);
writeFileSync(join(dir, "post-merge-last/post-merge-code-sha"), `${LAST_SHA}\n`);
writeFileSync(join(dir, "post-merge-last/lane-records.json"), "the last run's");
}
const output = join(dir, "output");
const result = spawnSync("bash", ["-c", runScript("Check earlier lane merges", stepsOf(jobBlock("post-merge-dispatch")))], {
cwd: dir,
encoding: "utf8",
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, RUNNER_TEMP: dir, GITHUB_OUTPUT: output, REPO: "o/r" },
});
const kept = (file) => (existsSync(join(dir, "post-merge-report", file)) ? readFileSync(join(dir, "post-merge-report", file), "utf8").trim() : null);
return {
status: result.status,
stdout: result.stdout,
ran: existsSync(join(dir, "ran")),
settled: existsSync(join(dir, "settled")) ? readFileSync(join(dir, "settled"), "utf8").trim() : null,
output: existsSync(output) ? readFileSync(output, "utf8") : "",
report: kept("post-merge.json"),
keptCode: kept("post-merge-code"),
keptSha: kept("post-merge-code-sha"),
records: kept("lane-records.json"),
};
} finally {
rmSync(dir, { recursive: true, force: true });
}
};

test("an idle tick reuses the last run's report that settle --idle-tick keeps, with its code and commit", () => {
for (const code of ["0", "5"]) {
const run = runIdleCheck({ last: lastReport(), code });
assert.equal(run.status, 0, run.stdout);
assert.equal(run.ran, false, `exit ${code}: the check ran again`);
assert.match(run.settled, new RegExp(`babysit_verify_evidence\\.py settle --idle-tick --repo o/r --postmerge-result \\S+/post-merge-last/post-merge\\.json --code-sha ${LAST_SHA}$`));
assert.match(run.stdout, /::notice::the last run's post-merge check \(exit \d\) still stands: nothing it reads has moved; reused, not run again/);
assert.match(run.output, new RegExp(`^code=${code}$`, "m"));
assert.match(run.output, /^needs-dispatch=false$/m);
// Kept for this run's later jobs and the next tick as it was made, with
// the lane records this run read, not the last run's.
assert.deepEqual(JSON.parse(run.report), lastReport());
assert.equal(run.keptCode, code);
assert.equal(run.keptSha, LAST_SHA);
assert.equal(run.records, "read in");
}
});

test("a tick runs the check itself for no report, a refusal, a crash or a check that did not pass", () => {
const head = execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
for (const [why, options] of [
["none", {}],
["settle refuses", { last: lastReport(), settle: 1 }],
["settle crashes", { last: lastReport(), settle: 2 }],
...["1", "2", "3", "4", "75"].map((code) => [`exit ${code}`, { last: lastReport(), code }]),
]) {
const run = runIdleCheck(options);
assert.equal(run.ran, true, why);
assert.match(run.output, /^code=3$/m, why);
assert.deepEqual(JSON.parse(run.report), { ran: true }, why);
assert.equal(run.keptSha, head, why);
if (/^exit /.test(why)) assert.equal(run.settled, null, `${why} is not offered to settle`);
}
});

test("post-merge-dispatch reads the last run's report before its check, from a trusted run alone", () => {
const list = stepsOf(jobBlock("post-merge-dispatch"));
const names = list.map((s) => s.name);
const read = step("Read the post-merge report the last run kept", list);
assert.equal(names.indexOf("Read the post-merge report the last run kept") + 1, names.indexOf("Check earlier lane merges"));
assert.match(read, /\n {8}if: steps\.mode\.outputs\.live == 'true'\n {8}continue-on-error: true\n/);
assert.ok(read.includes("LAST_REPORT_DIR: ${{ runner.temp }}/post-merge-last\n"));
assert.match(read, /\n {8}run: node \.base\/\.github\/scripts\/pr-merge\/last-report\.mjs\n/);
});

// The merge job reuses the kept report only while the check's code at its
// own commit reads as it did here: the report keeps the commit it ran.
test("the early check keeps the commit whose code ran it beside its report", () => {
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/pr-merge/workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ 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"]],
["enable-auto-merge", ["head-ref-map.mjs read", "lane-records.mjs read", "verify-answers.mjs"]],
["post-merge-dispatch", ["lane-records.mjs read", "last-report.mjs"]],
]) {
for (const script of scripts) {
const step = new RegExp(`\\n {10}PR_MERGE_READS_PATH: (.*)\\n {8}run: node \\.base/\\.github/scripts/pr-merge/${script.replace(".", "\\.")}\\n`).exec(job(id));
Expand Down
Loading
Loading