Files
playbook/antigravity-awesome-skills/tools/scripts/merge_canonical_sync_pr.cjs
T
2026-07-20 00:03:02 +00:00

258 lines
11 KiB
JavaScript

#!/usr/bin/env node
const { spawnSync } = require("child_process");
const REQUIRED_CHECKS = ["pr-policy", "pr-evidence", "source-validation", "artifact-preview"];
const GITHUB_ACTIONS_APP_ID = 15368;
const BOT_BRANCH = "automation/canonical-repo-state";
const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
function parseArgs(argv) {
const options = { pollSeconds: 10, maxAttempts: 180, skipPages: false };
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--skip-pages") {
options.skipPages = true;
continue;
}
if (["--repo", "--pr", "--head", "--poll-seconds", "--max-attempts"].includes(argument)) {
const value = argv[index + 1];
if (!value || value.startsWith("--")) throw new Error(`${argument} requires a value.`);
const key = argument.slice(2).replace(/-([a-z])/gu, (_match, letter) => letter.toUpperCase());
options[key] = ["pollSeconds", "maxAttempts"].includes(key) ? Number(value) : value;
index += 1;
continue;
}
throw new Error(`Unknown argument: ${argument}`);
}
if (!/^[^/]+\/[^/]+$/u.test(String(options.repo || ""))) throw new Error("--repo must be owner/name.");
if (!/^\d+$/u.test(String(options.pr || ""))) throw new Error("--pr must be a positive integer.");
if (!/^[0-9a-f]{40}$/u.test(String(options.head || ""))) throw new Error("--head must be a full SHA-1.");
if (!Number.isInteger(options.pollSeconds) || options.pollSeconds <= 0) throw new Error("--poll-seconds must be positive.");
if (!Number.isInteger(options.maxAttempts) || options.maxAttempts <= 0) throw new Error("--max-attempts must be positive.");
return options;
}
function verificationWorkflows(options = {}) {
return options.skipPages ? ["ci.yml", "codeql.yml"] : ["ci.yml", "pages.yml", "codeql.yml"];
}
function runGh(args, options = {}) {
const result = spawnSync("gh", args, {
encoding: "utf8",
input: options.input,
maxBuffer: 8 * 1024 * 1024,
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(result.stderr.trim() || `gh ${args.join(" ")} failed.`);
return result.stdout.trim();
}
function latestRequiredChecks(checkRuns, expectedCheckSuiteId, requiredChecks = REQUIRED_CHECKS) {
const result = new Map();
for (const run of checkRuns || []) {
if (
Number(run?.app?.id) !== GITHUB_ACTIONS_APP_ID ||
Number(run?.check_suite?.id) !== Number(expectedCheckSuiteId) ||
!requiredChecks.includes(String(run?.name || ""))
) {
continue;
}
const prior = result.get(run.name);
const currentKey = run.completed_at || run.started_at || run.created_at || "";
const priorKey = prior?.completed_at || prior?.started_at || prior?.created_at || "";
if (!prior || currentKey > priorKey || (currentKey === priorKey && Number(run.id) > Number(prior.id))) {
result.set(run.name, run);
}
}
return result;
}
function summarizeChecks(checkRuns, expectedCheckSuiteId, requiredChecks = REQUIRED_CHECKS) {
const latest = latestRequiredChecks(checkRuns, expectedCheckSuiteId, requiredChecks);
return requiredChecks.map((name) => {
const run = latest.get(name);
if (!run) return { name, state: "pending", conclusion: "missing" };
if (String(run.status).toLowerCase() !== "completed") return { name, state: "pending", conclusion: "in_progress" };
const conclusion = String(run.conclusion || "").toLowerCase();
return { name, state: conclusion === "success" ? "success" : "failed", conclusion };
});
}
function validatePullRequest(pr, options, expectedBaseSha) {
if (
Number(pr?.number) !== Number(options.pr) ||
pr?.state !== "open" ||
pr?.base?.ref !== "main" ||
pr?.base?.sha !== expectedBaseSha ||
pr?.head?.ref !== BOT_BRANCH ||
pr?.head?.repo?.full_name !== options.repo ||
pr?.head?.sha !== options.head ||
pr?.auto_merge
) {
throw new Error("Canonical-sync PR identity, head, base, state, or merge mode changed.");
}
return true;
}
function validateProtectedMain(branch) {
if (branch?.name !== "main" || branch?.protected !== true) {
throw new Error("Canonical-sync merge requires GitHub to report main as protected.");
}
return true;
}
function selectCanonicalPullRequestRun(runs, options, expectedBaseSha, workflowPath = CI_WORKFLOW_PATH) {
const matches = (runs || []).filter((run) => (
run?.path === workflowPath &&
run?.event === "pull_request" &&
run?.head_branch === BOT_BRANCH &&
run?.head_sha === options.head &&
run?.head_repository?.full_name === options.repo &&
run?.actor?.login === "github-actions[bot]" &&
Number.isInteger(Number(run?.check_suite_id)) &&
Number(run.check_suite_id) > 0 &&
Array.isArray(run?.pull_requests) &&
run.pull_requests.length === 1 &&
Number(run.pull_requests[0]?.number) === Number(options.pr) &&
run.pull_requests[0]?.base?.ref === "main" &&
run.pull_requests[0]?.base?.sha === expectedBaseSha &&
run.pull_requests[0]?.head?.ref === BOT_BRANCH &&
run.pull_requests[0]?.head?.sha === options.head &&
Number(run.pull_requests[0]?.base?.repo?.id) === Number(run.head_repository?.id) &&
Number(run.pull_requests[0]?.head?.repo?.id) === Number(run.head_repository?.id)
));
if (matches.length > 1) {
throw new Error("Multiple canonical-sync pull-request CI runs matched the exact trusted identity.");
}
return matches[0] || null;
}
function wait(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function waitForChecks(options, expectedCheckSuiteId, dependencies = {}, requiredChecks = REQUIRED_CHECKS) {
if (!Number.isInteger(Number(expectedCheckSuiteId)) || Number(expectedCheckSuiteId) <= 0) {
throw new Error("Canonical-sync PR CI did not expose a valid check-suite ID.");
}
const load = dependencies.loadCheckRuns || (() => {
const payload = JSON.parse(runGh([
"api",
`repos/${options.repo}/commits/${options.head}/check-runs?per_page=100`,
]) || "{}");
return payload.check_runs || [];
});
const pause = dependencies.wait || wait;
for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
const summaries = summarizeChecks(load(), expectedCheckSuiteId, requiredChecks);
process.stdout.write(`[canonical-sync] ${summaries.map((item) => `${item.name}:${item.conclusion}`).join(" ")}\n`);
const failed = summaries.filter((item) => item.state === "failed");
if (failed.length) throw new Error(`Canonical-sync required checks failed: ${failed.map((item) => item.name).join(", ")}`);
if (summaries.every((item) => item.state === "success")) return summaries;
await pause(options.pollSeconds * 1000);
}
throw new Error("Timed out waiting for canonical-sync required checks.");
}
async function ensurePullRequestChecksStarted(options, expectedBaseSha, dependencies = {}, workflowPath = CI_WORKFLOW_PATH) {
const load = dependencies.loadWorkflowRuns || (() => {
const payload = JSON.parse(runGh([
"api",
`repos/${options.repo}/actions/runs?head_sha=${options.head}&per_page=100`,
]) || "{}");
return payload.workflow_runs || [];
});
const rerun = dependencies.rerunWorkflow || ((run) => runGh([
"api",
`repos/${options.repo}/actions/runs/${run.id}/rerun`,
"-X",
"POST",
]));
const pause = dependencies.wait || wait;
for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
const run = selectCanonicalPullRequestRun(load(), options, expectedBaseSha, workflowPath);
if (!run) {
await pause(options.pollSeconds * 1000);
continue;
}
const status = String(run.status || "").toLowerCase();
const conclusion = String(run.conclusion || "").toLowerCase();
if (status === "completed" && conclusion === "action_required") {
rerun(run);
process.stdout.write(`[canonical-sync] restarted PR-associated CI run ${run.id}.\n`);
return run;
}
if (["queued", "in_progress", "waiting", "requested", "pending"].includes(status)) return run;
if (status === "completed" && conclusion === "success") return run;
throw new Error(`Canonical-sync PR CI cannot start from ${status || "unknown"}/${conclusion || "none"}.`);
}
throw new Error("Timed out waiting for the canonical-sync pull-request CI run.");
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const initialBranch = JSON.parse(runGh(["api", `repos/${options.repo}/branches/main`]));
validateProtectedMain(initialBranch);
const initialBaseSha = String(initialBranch?.commit?.sha || "");
if (!/^[0-9a-f]{40}$/u.test(initialBaseSha)) throw new Error("Protected main did not expose a full base SHA.");
const initialPr = JSON.parse(runGh(["api", `repos/${options.repo}/pulls/${options.pr}`]));
validatePullRequest(initialPr, options, initialBaseSha);
const pullRequestRun = await ensurePullRequestChecksStarted(options, initialBaseSha);
await waitForChecks(options, pullRequestRun.check_suite_id);
const pr = JSON.parse(runGh(["api", `repos/${options.repo}/pulls/${options.pr}`]));
const finalBranch = JSON.parse(runGh(["api", `repos/${options.repo}/branches/main`]));
validateProtectedMain(finalBranch);
if (finalBranch?.commit?.sha !== initialBaseSha) {
throw new Error("Protected main changed while canonical-sync checks were running.");
}
validatePullRequest(pr, options, initialBaseSha);
const payload = JSON.stringify({
merge_method: "squash",
sha: options.head,
commit_title: options.skipPages
? "[skip pages] chore: synchronize canonical repository state"
: "chore: synchronize canonical repository state",
commit_message: "Generated artifacts reproduced and merged through protected required checks.",
});
const merged = JSON.parse(runGh(
["api", `repos/${options.repo}/pulls/${options.pr}/merge`, "-X", "PUT", "--input", "-"],
{ input: payload },
));
if (merged?.merged !== true) throw new Error(`Canonical-sync merge failed: ${merged?.message || "merged=false"}`);
for (const workflow of verificationWorkflows(options)) {
runGh([
"api",
`repos/${options.repo}/actions/workflows/${workflow}/dispatches`,
"-X",
"POST",
"-f",
"ref=main",
]);
}
const pages = options.skipPages ? " without Pages publication" : " including Pages verification";
process.stdout.write(`[canonical-sync] merged PR #${options.pr} at ${options.head} and dispatched main verification${pages}.\n`);
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
});
}
module.exports = {
ensurePullRequestChecksStarted,
latestRequiredChecks,
parseArgs,
selectCanonicalPullRequestRun,
summarizeChecks,
validateProtectedMain,
validatePullRequest,
verificationWorkflows,
waitForChecks,
};