Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
255 lines
7.7 KiB
JavaScript
255 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
|
import { execPlainGh } from "./lib/plain-gh.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 };
|
|
const seen = new Set();
|
|
const setOnce = (flag, key, value) => {
|
|
if (seen.has(flag)) {
|
|
throw new Error(`${flag} was provided more than once.`);
|
|
}
|
|
seen.add(flag);
|
|
args[key] = value;
|
|
};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
switch (arg) {
|
|
case "--repo":
|
|
setOnce(arg, "repo", readOptionValue(argv, index, arg));
|
|
index += 1;
|
|
break;
|
|
case "--sha":
|
|
setOnce(arg, "sha", readOptionValue(argv, index, arg));
|
|
index += 1;
|
|
break;
|
|
case "--output":
|
|
setOnce(arg, "output", readOptionValue(argv, index, arg));
|
|
index += 1;
|
|
break;
|
|
case "--changelog-only":
|
|
setOnce(arg, "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 latestScheduledRun = latestRun(scheduledRuns);
|
|
const failedScheduledRun = latestRun(
|
|
scheduledRuns.filter(
|
|
(run) =>
|
|
run.status === "completed" && !["success", "cancelled", "skipped"].includes(run.conclusion),
|
|
),
|
|
);
|
|
if (failedScheduledRun && latestScheduledRun?.status !== "completed") {
|
|
return failedScheduledRun;
|
|
}
|
|
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 = execPlainGh(
|
|
["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();
|
|
}
|