📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
#!/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";
|
||||
const BASELINE_WORKFLOW_PATH = ".github/workflows/aas-v1-baseline-check.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 baselineRun = await ensurePullRequestChecksStarted(options, initialBaseSha, {}, BASELINE_WORKFLOW_PATH);
|
||||
await waitForChecks(options, baselineRun.check_suite_id, {}, ["aas-v1-baseline"]);
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user