mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 13:28:10 +00:00
One-time maintainer-authorized bootstrap merge for the release-gate verifier policy. Exact hosted CI and all supporting workflow gates passed on 66133de419.
245 lines
7.4 KiB
JavaScript
245 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
|
|
|
export const SCHEDULED_HOSTED_WORKFLOWS = [
|
|
"Blacksmith Testbox",
|
|
"Blacksmith ARM Testbox",
|
|
"Blacksmith Build Artifacts Testbox",
|
|
"Workflow Sanity",
|
|
];
|
|
const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
|
|
const BUILD_ARTIFACTS_WORKFLOW = "Blacksmith Build Artifacts Testbox";
|
|
const ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS = [
|
|
"Blacksmith Testbox",
|
|
"Blacksmith ARM Testbox",
|
|
"Workflow Sanity",
|
|
];
|
|
|
|
function readOptionValue(argv, index, optionName) {
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error(`Expected ${optionName} <value>.`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function parseArgs(argv) {
|
|
const args = { repo: "", sha: "", output: "", changelogOnly: false };
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
switch (arg) {
|
|
case "--repo":
|
|
args.repo = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case "--sha":
|
|
args.sha = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case "--output":
|
|
args.output = readOptionValue(argv, index, arg);
|
|
index += 1;
|
|
break;
|
|
case "--changelog-only":
|
|
args.changelogOnly = true;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown option: ${arg}`);
|
|
}
|
|
}
|
|
if (!args.repo || !args.sha || !args.output) {
|
|
throw new Error(
|
|
"Usage: node scripts/verify-pr-hosted-gates.mjs --repo <owner/repo> --sha <sha> --output <path>",
|
|
);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function formatObservedRuns(runs) {
|
|
if (runs.length === 0) {
|
|
return "none";
|
|
}
|
|
return runs
|
|
.map(
|
|
(run) => `${run.id ?? "unknown"}:${run.status ?? "unknown"}/${run.conclusion ?? "unknown"}`,
|
|
)
|
|
.join(", ");
|
|
}
|
|
|
|
function isReleaseGateCiRun(run, sha) {
|
|
return (
|
|
run?.event === "workflow_dispatch" &&
|
|
run?.head_sha === sha &&
|
|
String(run?.path ?? "").split("@", 1)[0] === CI_WORKFLOW_PATH &&
|
|
run?.display_title === `CI release gate ${sha}`
|
|
);
|
|
}
|
|
|
|
function matchingAuthoritativeRuns(runs, workflowName, sha) {
|
|
return runs.filter((run) => {
|
|
if (run?.head_sha !== sha) {
|
|
return false;
|
|
}
|
|
if (run?.event === "pull_request") {
|
|
return run.name === workflowName;
|
|
}
|
|
return workflowName === "CI" && isReleaseGateCiRun(run, sha);
|
|
});
|
|
}
|
|
|
|
function latestRun(runs) {
|
|
return runs.toSorted((left, right) =>
|
|
String(right.updated_at ?? "").localeCompare(String(left.updated_at ?? "")),
|
|
)[0];
|
|
}
|
|
|
|
function preferredCiRun(runs) {
|
|
const scheduledRuns = runs.filter((run) => run.event === "pull_request");
|
|
const failedScheduledRun = scheduledRuns.find(
|
|
(run) => run.status === "completed" && run.conclusion !== "success",
|
|
);
|
|
if (failedScheduledRun) {
|
|
return failedScheduledRun;
|
|
}
|
|
const latestScheduledRun = latestRun(scheduledRuns);
|
|
if (latestScheduledRun?.status === "completed") {
|
|
return latestScheduledRun;
|
|
}
|
|
return latestRun(runs.filter((run) => run.event === "workflow_dispatch")) ?? latestScheduledRun;
|
|
}
|
|
|
|
function successfulRunOrThrow(runs, workflowName, sha) {
|
|
const matchingRuns = matchingAuthoritativeRuns(runs, workflowName, sha);
|
|
const run = workflowName === "CI" ? preferredCiRun(matchingRuns) : latestRun(matchingRuns);
|
|
if (!run || run.status !== "completed" || run.conclusion !== "success") {
|
|
throw new Error(
|
|
`Missing successful exact-head ${workflowName} workflow for ${sha}. Observed: ${formatObservedRuns(matchingRuns)}`,
|
|
);
|
|
}
|
|
return run;
|
|
}
|
|
|
|
function successfulReleaseGateFallback(workflowRuns, sha) {
|
|
const fallback = latestRun(workflowRuns.filter((run) => isReleaseGateCiRun(run, sha)));
|
|
if (fallback?.status !== "completed" || fallback.conclusion !== "success") {
|
|
return null;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function canCoverQueuedBuildArtifacts(workflowRuns, sha) {
|
|
if (!successfulReleaseGateFallback(workflowRuns, sha)) {
|
|
return false;
|
|
}
|
|
const supportingGatesPassed = ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS.every((workflowName) => {
|
|
const run = latestRun(matchingAuthoritativeRuns(workflowRuns, workflowName, sha));
|
|
return run?.status === "completed" && run.conclusion === "success";
|
|
});
|
|
if (!supportingGatesPassed) {
|
|
return false;
|
|
}
|
|
const buildArtifactRuns = matchingAuthoritativeRuns(workflowRuns, BUILD_ARTIFACTS_WORKFLOW, sha);
|
|
const latestBuildArtifactRun = latestRun(buildArtifactRuns);
|
|
return (
|
|
latestBuildArtifactRun?.status === "queued" &&
|
|
buildArtifactRuns.every(
|
|
(run) =>
|
|
run.status === "queued" || (run.status === "completed" && run.conclusion === "success"),
|
|
)
|
|
);
|
|
}
|
|
|
|
function stripAnsi(raw) {
|
|
const escape = String.fromCharCode(27);
|
|
return raw.replace(new RegExp(`${escape}\\[[0-?]*[ -/]*[@-~]`, "gu"), "");
|
|
}
|
|
|
|
export function parseWorkflowRunPages(raw) {
|
|
return JSON.parse(stripAnsi(raw)).flatMap((page) => page.workflow_runs ?? []);
|
|
}
|
|
|
|
export function collectHostedGateEvidence({ sha, workflowRuns, changelogOnly = false }) {
|
|
if (!Array.isArray(workflowRuns)) {
|
|
throw new Error("workflowRuns must be an array.");
|
|
}
|
|
const workflows = [];
|
|
const fallbackCoveredWorkflows = [];
|
|
let ciRun;
|
|
if (!changelogOnly) {
|
|
ciRun = successfulRunOrThrow(workflowRuns, "CI", sha);
|
|
workflows.push(ciRun);
|
|
}
|
|
for (const workflowName of SCHEDULED_HOSTED_WORKFLOWS) {
|
|
const matchingRuns = matchingAuthoritativeRuns(workflowRuns, workflowName, sha);
|
|
if (matchingRuns.length > 0) {
|
|
if (
|
|
workflowName === BUILD_ARTIFACTS_WORKFLOW &&
|
|
canCoverQueuedBuildArtifacts(workflowRuns, sha)
|
|
) {
|
|
fallbackCoveredWorkflows.push({
|
|
name: workflowName,
|
|
coveredBy: "CI release gate",
|
|
reason: "scheduled workflow is queued",
|
|
});
|
|
continue;
|
|
}
|
|
workflows.push(successfulRunOrThrow(workflowRuns, workflowName, sha));
|
|
}
|
|
}
|
|
const evidence = {
|
|
headSha: sha,
|
|
workflows: workflows.map((run) => ({
|
|
id: run.id,
|
|
name: run.name,
|
|
event: run.event,
|
|
status: run.status,
|
|
conclusion: run.conclusion,
|
|
createdAt: run.created_at,
|
|
updatedAt: run.updated_at,
|
|
url: run.html_url,
|
|
})),
|
|
};
|
|
if (fallbackCoveredWorkflows.length > 0) {
|
|
evidence.fallbackCoveredWorkflows = fallbackCoveredWorkflows;
|
|
}
|
|
return evidence;
|
|
}
|
|
|
|
function loadWorkflowRuns(repo, sha) {
|
|
const raw = execFileSync(
|
|
"gh",
|
|
["api", `repos/${repo}/actions/runs?head_sha=${sha}&per_page=100`, "--paginate", "--slurp"],
|
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
);
|
|
return parseWorkflowRunPages(raw);
|
|
}
|
|
|
|
export function main(argv = process.argv.slice(2)) {
|
|
const args = parseArgs(argv);
|
|
const evidence = collectHostedGateEvidence({
|
|
sha: args.sha,
|
|
workflowRuns: loadWorkflowRuns(args.repo, args.sha),
|
|
changelogOnly: args.changelogOnly,
|
|
});
|
|
const manifest = {
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
repo: args.repo,
|
|
...evidence,
|
|
};
|
|
mkdirSync(path.dirname(args.output), { recursive: true });
|
|
writeFileSync(args.output, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
console.log(
|
|
`Exact-head hosted gates passed for ${args.sha}: ${manifest.workflows
|
|
.map((workflow) => `${workflow.name}#${workflow.id}`)
|
|
.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
|
main();
|
|
}
|