#!/usr/bin/env node /** * Release CI summary helper that prints parent and child workflow status for a * full release run. */ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { plainGhEnv, resolvePlainGhBin } from "./lib/plain-gh.mjs"; const DEFAULT_REPO = process.env.OPENCLAW_RELEASE_REPO || "openclaw/openclaw"; const RELEASE_EVIDENCE_SCHEMA = "openclaw.release-validation-evidence/v3"; const SHA_PINNED_BRANCH_PATTERN = /^release-ci\/[a-f0-9]{12}-[1-9][0-9]*$/u; const RELEASE_EVIDENCE_SCRIPT = "scripts/release-ci-summary.mjs"; const RELEASE_EVIDENCE_FILE = fileURLToPath(import.meta.url); const RELEASE_EVIDENCE_REPO_ROOT = resolve(dirname(RELEASE_EVIDENCE_FILE), ".."); const MANIFEST_ARTIFACT_ENTRY = "full-release-validation-manifest.json"; const MAX_MANIFEST_ARTIFACT_ZIP_BYTES = 256 * 1024; const MAX_MANIFEST_JSON_BYTES = 128 * 1024; const MAX_MANIFEST_ENTRY_LIST_BYTES = 8 * 1024; const CHILD_DISPATCHES = [ { manifestKey: "normalCi", name: "CI", parentJobName: "Run normal full CI", suffix: "-ci", trustedRef: "parent", workflow: "ci.yml", }, { manifestKey: "releaseChecks", name: "OpenClaw Release Checks", parentJobName: "Run release/live/Docker/QA validation", suffix: "-release-checks", trustedRef: "parent", workflow: "openclaw-release-checks.yml", }, { manifestKey: "pluginPrerelease", name: "Plugin Prerelease", parentJobName: "Run plugin prerelease validation", suffix: "-plugin-prerelease", trustedRef: "parent", workflow: "plugin-prerelease.yml", }, { manifestKey: "npmTelegram", name: "NPM Telegram Beta E2E", parentJobName: "Run package Telegram E2E", suffix: "-npm-telegram", trustedRef: "parent", workflow: "npm-telegram-beta-e2e.yml", }, { manifestKey: "productPerformance", name: "OpenClaw Performance", parentJobName: "Run product performance evidence", suffix: "", trustedRef: "parent", workflow: "openclaw-performance.yml", }, ]; const EVIDENCE_REUSE_POLICY = "exact-target-full-validation-v1"; const RERUN_GROUP_CHILD_KEYS = new Map([ ["all", ["normalCi", "releaseChecks", "pluginPrerelease", "productPerformance"]], ["ci", ["normalCi"]], ["plugin-prerelease", ["pluginPrerelease"]], ["release-checks", ["releaseChecks"]], ["install-smoke", ["releaseChecks"]], ["cross-os", ["releaseChecks"]], ["live-e2e", ["releaseChecks"]], ["package", ["releaseChecks"]], ["qa", ["releaseChecks"]], ["qa-parity", ["releaseChecks"]], ["qa-live", ["releaseChecks"]], ["npm-telegram", ["npmTelegram"]], ["performance", ["productPerformance"]], ]); function gh(args) { return execFileSync(resolvePlainGhBin(), args, { encoding: "utf8", env: plainGhEnv(), maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], }); } function jsonGh(args) { return JSON.parse(gh(args)); } function githubRestJson(pathSuffix, repository = DEFAULT_REPO) { const result = execFileSync( "bash", [ "-lc", [ "set -euo pipefail", 'token="$("$OPENCLAW_PLAIN_GH_BIN" auth token)"', 'curl -fsS -H "Authorization: Bearer ${token}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${OPENCLAW_GITHUB_REST_URL}"', ].join("\n"), ], { encoding: "utf8", env: { ...plainGhEnv(), OPENCLAW_PLAIN_GH_BIN: resolvePlainGhBin(), OPENCLAW_GITHUB_REST_URL: `https://api.github.com/repos/${repository}/${pathSuffix}`, }, maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], }, ); return JSON.parse(result); } function downloadArtifactZip(artifactId, destination, repository = DEFAULT_REPO) { execFileSync( "bash", [ "-lc", [ "set -euo pipefail", 'token="$("$OPENCLAW_PLAIN_GH_BIN" auth token)"', 'curl -fsSL -H "Authorization: Bearer ${token}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" --output "$OPENCLAW_GITHUB_ARTIFACT_DESTINATION" "$OPENCLAW_GITHUB_ARTIFACT_URL"', ].join("\n"), ], { env: { ...plainGhEnv(), OPENCLAW_GITHUB_ARTIFACT_DESTINATION: destination, OPENCLAW_GITHUB_ARTIFACT_URL: `https://api.github.com/repos/${repository}/actions/artifacts/${artifactId}/zip`, OPENCLAW_PLAIN_GH_BIN: resolvePlainGhBin(), }, stdio: ["ignore", "ignore", "pipe"], }, ); } function rate() { try { return jsonGh(["api", "rate_limit"]).resources.core; } catch { return undefined; } } export function validateParentRunBinding(parentView, parentRest, expectedRunId) { const boundWorkflowPath = String(parentRest.path ?? "").split("@", 1)[0]; if ( String(parentRest.id) !== String(expectedRunId) || parentRest.event !== "workflow_dispatch" || boundWorkflowPath !== ".github/workflows/full-release-validation.yml" || Number(parentRest.run_attempt) !== Number(parentView.attempt) || parentRest.head_branch !== parentView.headBranch || parentRest.head_sha !== parentView.headSha ) { throw new Error(`full release parent run binding mismatch: ${expectedRunId}`); } return parentRest; } export function expectedChildDispatches(parentRunId, parentRunAttempt, parentWorkflowRef) { if (!/^[1-9][0-9]*$/u.test(String(parentRunId))) { throw new Error("parent run ID must be a positive decimal"); } if (!Number.isSafeInteger(parentRunAttempt) || parentRunAttempt < 1) { throw new Error("parent run attempt must be a positive integer"); } if (typeof parentWorkflowRef !== "string" || parentWorkflowRef.length === 0) { throw new Error("parent workflow ref is required"); } const dispatchPrefix = `full-release-validation-${parentRunId}-${parentRunAttempt}`; return CHILD_DISPATCHES.map((child) => ({ ...child, displayTitle: `${child.name} ${dispatchPrefix}${child.suffix}`, headBranch: child.trustedRef === "main" ? "main" : parentWorkflowRef, })); } export function requiredChildKeysForRerunGroup(rerunGroup) { const childKeys = RERUN_GROUP_CHILD_KEYS.get(rerunGroup); if (!childKeys) { throw new Error(`release validation manifest rerun group is invalid: ${rerunGroup}`); } return new Set(childKeys); } export function expectedSelectedChildDispatches( parentRunId, parentRunAttempt, parentWorkflowRef, selectedKeys, ) { return expectedChildDispatches(parentRunId, parentRunAttempt, parentWorkflowRef).filter((child) => selectedKeys.has(child.manifestKey), ); } export function selectExactChildRun(runs, expectedDisplayTitle, expectedHeadBranch) { const matches = runs.filter( (run) => run.event === "workflow_dispatch" && run.display_title === expectedDisplayTitle && run.head_branch === expectedHeadBranch, ); if (matches.length > 1) { throw new Error( `multiple child runs have exact dispatch title and branch: ${expectedDisplayTitle} (${expectedHeadBranch})`, ); } return matches[0]; } export function selectExactChildRunFromPages(runPages, expectedDisplayTitle, expectedHeadBranch) { let exactMatch; for (const runs of runPages) { const match = selectExactChildRun(runs, expectedDisplayTitle, expectedHeadBranch); if (match) { if (exactMatch) { throw new Error( `multiple child runs have exact dispatch title and branch: ${expectedDisplayTitle} (${expectedHeadBranch})`, ); } exactMatch = match; } if (runs.length < 100) { break; } } return exactMatch; } function findExactChildRun(child, repository = DEFAULT_REPO) { const runPages = []; for (let page = 1; page <= 10; page += 1) { const query = new URLSearchParams({ event: "workflow_dispatch", branch: child.headBranch, page: String(page), per_page: "100", }); const runs = githubRestJson(`actions/workflows/${child.workflow}/runs?${query.toString()}`, repository) .workflow_runs ?? []; runPages.push(runs); if (runs.length < 100) { break; } } return selectExactChildRunFromPages(runPages, child.displayTitle, child.headBranch); } function findParentJobsAll(parentRunId, repository = DEFAULT_REPO) { const jobs = []; for (let page = 1; page <= 10; page += 1) { const query = new URLSearchParams({ filter: "all", page: String(page), per_page: "100", }); const pageJobs = githubRestJson(`actions/runs/${parentRunId}/jobs?${query.toString()}`, repository).jobs ?? []; jobs.push(...pageJobs); if (pageJobs.length < 100) { break; } } return jobs; } function parentJobLog(jobId, repository = DEFAULT_REPO) { return gh(["api", `repos/${repository}/actions/jobs/${jobId}/logs`]); } function normalizeOptionalRunId(value, label) { if (value === "") { return ""; } if (!/^[1-9][0-9]*$/u.test(String(value))) { throw new Error(`${label} must be empty or a positive decimal run ID`); } return String(value); } function normalizeRequiredRunId(value, label) { const runId = normalizeOptionalRunId(value, label); if (!runId) { throw new Error(`${label} is required`); } return runId; } function normalizeRepository(value) { const repository = String(value ?? ""); if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)) { throw new Error("repository must use the owner/name form"); } return repository; } function normalizeWorkflowRef(value, label) { const workflowRef = String(value ?? ""); const hasForbiddenCharacter = Array.from(workflowRef).some((character) => { const codePoint = character.codePointAt(0) ?? 0; return ( codePoint <= 0x1f || codePoint === 0x7f || character.trim() === "" || "~^:?*[\\".includes(character) ); }); if (workflowRef.length === 0 || workflowRef.length > 255 || hasForbiddenCharacter) { throw new Error(`${label} is invalid`); } return workflowRef; } function normalizeSha(value, label) { const sha = String(value ?? ""); if (!/^[a-f0-9]{40}$/u.test(sha)) { throw new Error(`${label} is invalid`); } return sha; } function normalizePositiveInteger(value, label) { const number = Number(value); if (!Number.isSafeInteger(number) || number < 1) { throw new Error(`${label} must be a positive integer`); } return number; } function normalizeJsonObject(value, label) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`${label} is invalid`); } return value; } function canonicalJson(value) { if (Array.isArray(value)) { return value.map(canonicalJson); } if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value) .toSorted(([left], [right]) => left.localeCompare(right)) .map(([key, entry]) => [key, canonicalJson(entry)]), ); } return value; } function manifestEvidenceIdentity(manifest) { return canonicalJson({ childRunIds: manifest.childRunIds, controls: manifest.controls, releaseProfile: manifest.releaseProfile, rerunGroup: manifest.rerunGroup, runReleaseSoak: manifest.runReleaseSoak, validationInputs: manifest.validationInputs, }); } export function validateParentManifest(value, expected) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error("release validation manifest must be an object"); } if (![2, 3].includes(value.version) || value.workflowName !== "Full Release Validation") { throw new Error("release validation manifest schema is unsupported"); } if (String(value.runId) !== String(expected.runId)) { throw new Error("release validation manifest run ID mismatch"); } if ( !/^[1-9][0-9]*$/u.test(String(value.runAttempt)) || (expected.runAttempt !== undefined && Number(value.runAttempt) !== Number(expected.runAttempt)) ) { throw new Error("release validation manifest run attempt mismatch"); } const targetSha = normalizeSha(value.targetSha, "release validation manifest target SHA"); if (typeof value.workflowRef !== "string" || value.workflowRef.length === 0) { throw new Error("release validation manifest workflow ref is invalid"); } if (expected.workflowRef !== undefined && value.workflowRef !== expected.workflowRef) { throw new Error("release validation manifest workflow ref mismatch"); } let workflowSha; let workflowFullRef; let workflowRefType; if (value.version === 3) { workflowSha = normalizeSha(value.workflowSha, "release validation manifest workflow SHA"); if (expected.workflowSha !== undefined && workflowSha !== expected.workflowSha) { throw new Error("release validation manifest workflow SHA mismatch"); } workflowFullRef = String(value.workflowFullRef ?? ""); workflowRefType = String(value.workflowRefType ?? ""); if ( !["branch", "tag"].includes(workflowRefType) || workflowFullRef !== `refs/${workflowRefType === "branch" ? "heads" : "tags"}/${value.workflowRef}` ) { throw new Error("release validation manifest workflow full ref is invalid"); } } else if (expected.workflowSha !== undefined) { workflowSha = normalizeSha(expected.workflowSha, "release validation workflow SHA"); } const rerunGroup = String(value.rerunGroup ?? ""); requiredChildKeysForRerunGroup(rerunGroup); const releaseProfile = String(value.releaseProfile ?? ""); if (!["beta", "stable", "full"].includes(releaseProfile)) { throw new Error("release validation manifest release profile is invalid"); } const runReleaseSoak = String(value.runReleaseSoak ?? ""); if (!["true", "false"].includes(runReleaseSoak)) { throw new Error("release validation manifest release soak value is invalid"); } const controls = normalizeJsonObject(value.controls, "release validation manifest controls"); if (value.version === 3 && controls.performanceReportPublication !== "artifact-only") { throw new Error("release validation manifest performance report publication mode is invalid"); } const validationInputs = value.validationInputs === undefined ? undefined : normalizeJsonObject( value.validationInputs, "release validation manifest validation inputs", ); const childRuns = value.childRuns; if (!childRuns || typeof childRuns !== "object" || Array.isArray(childRuns)) { throw new Error("release validation manifest childRuns is invalid"); } const childRunIds = { normalCi: normalizeOptionalRunId(childRuns.normalCi, "normal CI run ID"), npmTelegram: normalizeOptionalRunId(childRuns.npmTelegram, "npm Telegram run ID"), pluginPrerelease: normalizeOptionalRunId( childRuns.pluginPrerelease, "plugin prerelease run ID", ), productPerformance: normalizeOptionalRunId( childRuns.productPerformance?.runId ?? "", "performance run ID", ), releaseChecks: normalizeOptionalRunId(childRuns.releaseChecks, "release checks run ID"), }; let evidenceReuse; if (value.evidenceReuse !== undefined) { const reuse = normalizeJsonObject( value.evidenceReuse, "release validation manifest evidence reuse", ); if (reuse.policy !== EVIDENCE_REUSE_POLICY) { throw new Error("release validation manifest evidence reuse policy is invalid"); } if (!/^[a-f0-9]{40}$/u.test(String(reuse.evidenceSha))) { throw new Error("release validation manifest evidence SHA is invalid"); } if ( !Array.isArray(reuse.changedPaths) || reuse.changedPaths.some( (changedPath) => typeof changedPath !== "string" || changedPath.length === 0, ) || new Set(reuse.changedPaths).size !== reuse.changedPaths.length ) { throw new Error("release validation manifest evidence changed paths are invalid"); } evidenceReuse = { changedPaths: reuse.changedPaths, evidenceSha: String(reuse.evidenceSha), policy: reuse.policy, runId: normalizeRequiredRunId(reuse.runId, "evidence reuse root run ID"), selectedRunId: normalizeRequiredRunId(reuse.selectedRunId, "evidence reuse selected run ID"), }; } return { childRunIds, controls, evidenceReuse, releaseProfile, rerunGroup, runAttempt: Number(value.runAttempt), runId: String(value.runId), runReleaseSoak, targetRef: String(value.targetRef ?? ""), targetSha, validationInputs, version: value.version, workflowFullRef, workflowSha, workflowRef: value.workflowRef, workflowRefType, }; } export function validateEvidenceReuseChain(currentManifest, selectedManifest, rootManifest) { const reuse = currentManifest.evidenceReuse; if (!reuse) { throw new Error("release validation manifest does not authorize evidence reuse"); } if (reuse.changedPaths.length !== 0) { throw new Error("full release evidence reuse requires an exact target with no changed paths"); } if (rootManifest.evidenceReuse || selectedManifest.evidenceReuse) { throw new Error("evidence reuse must select a root execution manifest"); } if ( !currentManifest.validationInputs || !selectedManifest.validationInputs || !rootManifest.validationInputs ) { throw new Error("evidence reuse manifests must record validation inputs"); } if (rootManifest.runId !== reuse.runId) { throw new Error("evidence reuse root manifest run ID mismatch"); } if (selectedManifest.runId !== reuse.selectedRunId) { throw new Error("evidence reuse selected manifest run ID mismatch"); } if (selectedManifest.targetSha !== reuse.evidenceSha) { throw new Error("evidence reuse selected manifest SHA mismatch"); } if ( currentManifest.targetSha !== reuse.evidenceSha || rootManifest.targetSha !== reuse.evidenceSha ) { throw new Error("full release evidence reuse target SHA mismatch"); } if (selectedManifest.runId !== rootManifest.runId) { throw new Error("evidence reuse selected manifest is not the chain root"); } const rootIdentity = JSON.stringify(manifestEvidenceIdentity(rootManifest)); for (const [label, manifest] of [ ["selected", selectedManifest], ["current", currentManifest], ]) { if (JSON.stringify(manifestEvidenceIdentity(manifest)) !== rootIdentity) { throw new Error(`evidence reuse ${label} manifest policy differs from the chain root`); } } return rootManifest.targetSha; } export function selectedChildKeys(parentJobs) { return new Set( CHILD_DISPATCHES.filter((child) => { const parentJob = parentJobs.find((job) => job.name === child.parentJobName); return parentJob && parentJob.conclusion !== "skipped"; }).map((child) => child.manifestKey), ); } export function manifestChildEntries(manifest, children, selectedKeys) { return children.flatMap((child) => { const runId = manifest.childRunIds[child.manifestKey]; if (!runId) { if (selectedKeys.has(child.manifestKey)) { throw new Error(`selected child is missing from manifest: ${child.name}`); } return []; } return [{ child, runId }]; }); } function childDispatchAttempt(displayTitle, child, parentRunId, parentRunAttempt) { const prefix = `${child.name} full-release-validation-${parentRunId}-`; if (!displayTitle.startsWith(prefix) || !displayTitle.endsWith(child.suffix)) { return undefined; } const attemptEnd = child.suffix ? -child.suffix.length : undefined; const attemptText = displayTitle.slice(prefix.length, attemptEnd); if (!/^[1-9][0-9]*$/u.test(attemptText)) { return undefined; } const attempt = Number(attemptText); if (!Number.isSafeInteger(attempt) || attempt > parentRunAttempt) { return undefined; } return attempt; } function parentJobExecutionFingerprint(job) { return canonicalJson({ completedAt: job.completed_at, conclusion: job.conclusion, name: job.name, startedAt: job.started_at, status: job.status, steps: (job.steps ?? []).map((step) => ({ completedAt: step.completed_at, conclusion: step.conclusion, name: step.name, number: step.number, startedAt: step.started_at, status: step.status, })), }); } function selectedAttemptParentJob(parentJobs, child, parentManifest) { const slotJobs = parentJobs.filter((job) => job.name === child.parentJobName); if (slotJobs.length === 0) { throw new Error(`manifest parent job is missing: ${child.name}`); } const latestAttempt = Math.max(...slotJobs.map((job) => Number(job.run_attempt))); if (latestAttempt !== parentManifest.runAttempt) { throw new Error(`manifest parent job latest attempt mismatch: ${child.name}`); } const currentJobs = slotJobs.filter( (job) => Number(job.run_attempt) === parentManifest.runAttempt, ); if (currentJobs.length !== 1) { throw new Error(`manifest parent job is not unique at the selected attempt: ${child.name}`); } const currentJob = currentJobs[0]; if (currentJob.status !== "completed" || currentJob.conclusion !== "success") { throw new Error(`manifest parent job is not completed/success: ${child.name}`); } return { currentJob, slotJobs }; } export function resolveManifestChildOriginAttempt(run, child, parentManifest, parentJobs) { const correlatedAttempt = childDispatchAttempt( String(run.display_title ?? ""), child, parentManifest.runId, parentManifest.runAttempt, ); if (correlatedAttempt !== undefined) { return correlatedAttempt; } if (run.display_title !== child.name) { return undefined; } const { currentJob, slotJobs } = selectedAttemptParentJob(parentJobs, child, parentManifest); const currentFingerprint = JSON.stringify(parentJobExecutionFingerprint(currentJob)); const carriedOriginAttempts = slotJobs .filter( (job) => Number(job.run_attempt) < parentManifest.runAttempt && job.status === "completed" && job.conclusion === "success" && JSON.stringify(parentJobExecutionFingerprint(job)) === currentFingerprint, ) .map((job) => Number(job.run_attempt)); return carriedOriginAttempts.length > 0 ? Math.min(...carriedOriginAttempts) : parentManifest.runAttempt; } export function selectManifestParentJob(parentJobs, child, parentManifest, originAttempt) { const { currentJob, slotJobs } = selectedAttemptParentJob(parentJobs, child, parentManifest); if (originAttempt === parentManifest.runAttempt) { return currentJob; } const originJobs = slotJobs.filter((job) => Number(job.run_attempt) === originAttempt); if (originJobs.length !== 1) { throw new Error(`manifest parent job origin is not unique: ${child.name}`); } const originJob = originJobs[0]; if (originJob.status !== "completed" || originJob.conclusion !== "success") { throw new Error(`manifest parent job origin is not completed/success: ${child.name}`); } if ( JSON.stringify(parentJobExecutionFingerprint(currentJob)) !== JSON.stringify(parentJobExecutionFingerprint(originJob)) ) { throw new Error(`manifest parent job carry-forward fingerprint mismatch: ${child.name}`); } return currentJob; } function childRunIdsFromParentLog(log, repository = DEFAULT_REPO) { const escapedRepo = repository.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&"); const pattern = new RegExp( `https://github\\.com/${escapedRepo}/actions/runs/([1-9][0-9]*)`, "gu", ); return new Set(Array.from(log.matchAll(pattern), (match) => match[1])); } export function validateManifestChildRun( run, child, runId, parentManifest, parentJobs, selectedParentJobLog, repository = DEFAULT_REPO, ) { if (String(run.id) !== String(runId)) { throw new Error(`manifest child run ID mismatch: ${child.name}`); } const originAttempt = resolveManifestChildOriginAttempt(run, child, parentManifest, parentJobs); if ( run.event !== "workflow_dispatch" || run.head_branch !== child.headBranch || (child.trustedRef === "parent" && run.head_sha !== parentManifest.workflowSha) || !/^[a-f0-9]{40}$/u.test(String(run.head_sha)) || run.actor?.login !== "github-actions[bot]" || run.triggering_actor?.login !== "github-actions[bot]" || !Number.isSafeInteger(Number(run.run_attempt)) || Number(run.run_attempt) < 1 || originAttempt === undefined ) { throw new Error(`manifest child dispatch tuple mismatch: ${child.name}`); } const childWorkflowPath = String(run.path ?? "").split("@", 1)[0]; if (childWorkflowPath !== `.github/workflows/${child.workflow}`) { throw new Error(`manifest child workflow mismatch: ${child.name}`); } selectManifestParentJob(parentJobs, child, parentManifest, originAttempt); const emittedChildRunIds = childRunIdsFromParentLog(selectedParentJobLog, repository); if (emittedChildRunIds.size !== 1 || !emittedChildRunIds.has(String(runId))) { throw new Error(`manifest child run is not uniquely emitted by its parent job: ${child.name}`); } if ( child.manifestKey !== "npmTelegram" && !selectedParentJobLog.includes(`TARGET_SHA: ${parentManifest.targetSha}`) ) { throw new Error(`manifest parent job target SHA mismatch: ${child.name}`); } if ( child.manifestKey === "productPerformance" && !selectedParentJobLog.includes("-f publish_reports=false") ) { throw new Error("manifest performance child is not dispatched in artifact-only mode"); } return run; } export function validatePerformanceArtifactOnlyJobs(jobs, runAttempt) { const normalizedRunAttempt = normalizePositiveInteger(runAttempt, "performance run attempt"); const currentJobs = jobs.filter((job) => Number(job.run_attempt) === normalizedRunAttempt); const guards = currentJobs.filter((job) => job.name === "Verify artifact-only report mode"); if ( guards.length !== 1 || guards[0].status !== "completed" || guards[0].conclusion !== "success" ) { throw new Error("performance artifact-only guard is missing or unsuccessful"); } const unsafePublisher = currentJobs.find( (job) => String(job.name ?? "").startsWith("Publish ") && String(job.name ?? "").endsWith(" report") && job.conclusion !== "skipped", ); if (unsafePublisher) { throw new Error(`performance report publisher was not skipped: ${unsafePublisher.name}`); } return guards[0]; } function manifestArtifactName(runId, runAttempt) { const normalizedRunId = normalizeRequiredRunId(runId, "full release run ID"); const normalizedRunAttempt = normalizePositiveInteger(runAttempt, "full release run attempt"); return `full-release-validation-${normalizedRunId}-${normalizedRunAttempt}`; } function legacyManifestArtifactName(runId) { return `full-release-validation-${normalizeRequiredRunId(runId, "full release run ID")}`; } export function validateManifestArtifactIdentity( artifact, { artifactDigest, artifactId, runAttempt, runId }, ) { const normalizedArtifactId = normalizeRequiredRunId(artifactId, "manifest artifact ID"); const normalizedRunId = normalizeRequiredRunId(runId, "full release run ID"); const normalizedRunAttempt = normalizePositiveInteger(runAttempt, "full release run attempt"); const normalizedDigest = String(artifactDigest ?? ""); if (!/^sha256:[a-f0-9]{64}$/u.test(normalizedDigest)) { throw new Error(`release validation manifest artifact digest is invalid: ${normalizedRunId}`); } const canonicalName = manifestArtifactName(normalizedRunId, normalizedRunAttempt); const legacyName = legacyManifestArtifactName(normalizedRunId); const validName = artifact.name === canonicalName || (normalizedRunAttempt === 1 && artifact.name === legacyName); if ( String(artifact.id) !== normalizedArtifactId || !validName || artifact.digest !== normalizedDigest || artifact.expired !== false || String(artifact.workflow_run?.id) !== normalizedRunId || !Number.isSafeInteger(Number(artifact.size_in_bytes)) || Number(artifact.size_in_bytes) < 1 ) { throw new Error(`release validation manifest artifact identity mismatch: ${normalizedRunId}`); } return artifact; } export function selectManifestArtifact(artifacts, runId, runAttempt) { const expectedName = manifestArtifactName(runId, runAttempt); const canonicalMatches = artifacts.filter( (artifact) => artifact.name === expectedName && artifact.expired === false && String(artifact.workflow_run?.id) === String(runId), ); if (canonicalMatches.length > 1) { throw new Error(`multiple release validation manifest artifacts found: ${runId}`); } const canonicalArtifact = canonicalMatches[0]; if (canonicalArtifact) { return validateManifestArtifactIdentity(canonicalArtifact, { artifactDigest: canonicalArtifact.digest, artifactId: canonicalArtifact.id, runAttempt, runId, }); } const legacyName = legacyManifestArtifactName(runId); const legacyMatches = artifacts.filter( (artifact) => artifact.name === legacyName && artifact.expired === false && String(artifact.workflow_run?.id) === String(runId), ); if (legacyMatches.length > 1) { throw new Error(`multiple legacy release validation manifest artifacts found: ${runId}`); } const legacyArtifact = legacyMatches[0]; if (!legacyArtifact) { return undefined; } if (Number(runAttempt) !== 1) { throw new Error(`legacy release validation manifest requires run attempt 1: ${runId}`); } return validateManifestArtifactIdentity(legacyArtifact, { artifactDigest: legacyArtifact.digest, artifactId: legacyArtifact.id, runAttempt, runId, }); } export function validateManifestArtifactCompatibility(artifact, manifest, runId, runAttempt) { if (artifact.name === manifestArtifactName(runId, runAttempt)) { return artifact; } if ( Number(runAttempt) === 1 && artifact.name === legacyManifestArtifactName(runId) && manifest?.version === 2 ) { return artifact; } throw new Error(`legacy release validation manifest artifact is not compatible: ${runId}`); } export function readManifestArtifactArchive(archivePath, expectedDigest) { const archiveSize = statSync(archivePath).size; if ( !Number.isSafeInteger(archiveSize) || archiveSize < 1 || archiveSize > MAX_MANIFEST_ARTIFACT_ZIP_BYTES ) { throw new Error("release validation manifest artifact compressed size is invalid"); } const archiveBytes = readFileSync(archivePath); if (archiveBytes.byteLength !== archiveSize) { throw new Error("release validation manifest artifact changed while being verified"); } const actualDigest = `sha256:${createHash("sha256").update(archiveBytes).digest("hex")}`; if (actualDigest !== expectedDigest) { throw new Error("release validation manifest artifact digest mismatch"); } let entryList; try { entryList = execFileSync("unzip", ["-Z", "-1", archivePath], { encoding: "utf8", maxBuffer: MAX_MANIFEST_ENTRY_LIST_BYTES, stdio: ["ignore", "pipe", "pipe"], }); } catch { throw new Error("release validation manifest artifact entry list is invalid"); } const entries = entryList.split(/\r?\n/u).filter((entry) => entry.length > 0); if (entries.length !== 1 || entries[0] !== MANIFEST_ARTIFACT_ENTRY) { throw new Error( `release validation manifest artifact must contain only ${MANIFEST_ARTIFACT_ENTRY}`, ); } let manifestBytes; try { manifestBytes = execFileSync("unzip", ["-p", archivePath, MANIFEST_ARTIFACT_ENTRY], { maxBuffer: MAX_MANIFEST_JSON_BYTES + 1, stdio: ["ignore", "pipe", "pipe"], }); } catch { throw new Error("release validation manifest artifact entry could not be read safely"); } if (manifestBytes.byteLength < 1 || manifestBytes.byteLength > MAX_MANIFEST_JSON_BYTES) { throw new Error("release validation manifest artifact entry size is invalid"); } return JSON.parse(manifestBytes.toString("utf8")); } function downloadParentManifestEvidence(runId, runAttempt, repository, manifestPath) { const targetRepository = repository ?? DEFAULT_REPO; const artifacts = []; for (let page = 1; page <= 10; page += 1) { const pageArtifacts = githubRestJson(`actions/runs/${runId}/artifacts?per_page=100&page=${page}`, targetRepository) .artifacts ?? []; artifacts.push(...pageArtifacts); if (pageArtifacts.length < 100) { break; } } const listedArtifact = selectManifestArtifact(artifacts, runId, runAttempt); if (!listedArtifact) { return undefined; } const artifact = validateManifestArtifactIdentity( githubRestJson(`actions/artifacts/${listedArtifact.id}`, targetRepository), { artifactDigest: listedArtifact.digest, artifactId: listedArtifact.id, runAttempt, runId, }, ); const downloadDir = mkdtempSync(join(tmpdir(), "openclaw-release-ci-summary-")); try { const archivePath = join(downloadDir, "manifest.zip"); downloadArtifactZip(String(artifact.id), archivePath, targetRepository); const manifest = readManifestArtifactArchive(archivePath, artifact.digest); validateManifestArtifactCompatibility(artifact, manifest, runId, runAttempt); if (manifestPath) { const providedManifest = JSON.parse(readFileSync(resolve(manifestPath), "utf8")); if ( JSON.stringify(canonicalJson(providedManifest)) !== JSON.stringify(canonicalJson(manifest)) ) { throw new Error("provided release validation manifest differs from the run artifact"); } } return { artifact, manifest }; } finally { rmSync(downloadDir, { force: true, recursive: true }); } } function tryDownloadParentManifest(runId, runAttempt, repository = DEFAULT_REPO) { return downloadParentManifestEvidence(runId, runAttempt, repository)?.manifest; } function workflowPath(run) { return String(run.path ?? "").split("@", 1)[0]; } function normalizedManifestArtifact(artifact, runAttempt) { return { digest: artifact.digest, id: String(artifact.id), name: artifact.name, runAttempt, sizeInBytes: Number(artifact.size_in_bytes), }; } function validateManifestArtifactBinding(artifact, manifest, parentRun, runId) { validateManifestArtifactCompatibility(artifact, manifest, runId, parentRun.run_attempt); if ( String(artifact.workflow_run?.id) !== String(runId) || artifact.workflow_run?.head_branch !== parentRun.head_branch || artifact.workflow_run?.head_sha !== parentRun.head_sha ) { throw new Error(`release validation manifest artifact binding mismatch: ${runId}`); } } function validateCompletedParentRun(parentView, parentRest, repository, runId) { validateParentRunBinding(parentView, parentRest, runId); if ( parentView.status !== "completed" || parentView.conclusion !== "success" || parentRest.status !== "completed" || parentRest.conclusion !== "success" || parentRest.repository?.full_name !== repository ) { throw new Error(`full release parent run is not completed/success: ${runId}`); } } export function createReleaseEvidenceClient(repository = DEFAULT_REPO) { const normalizedRepository = normalizeRepository(repository); return { compareCommits(base, head) { return githubRestJson(`compare/${base}...${head}`, normalizedRepository); }, getJobLog(jobId) { return parentJobLog(jobId, normalizedRepository); }, getParentJobs(runId) { return findParentJobsAll(runId, normalizedRepository); }, getRun(runId) { return githubRestJson(`actions/runs/${runId}`, normalizedRepository); }, getRunView(runId) { return jsonGh([ "run", "view", String(runId), "--repo", normalizedRepository, "--json", "status,conclusion,attempt,headBranch,headSha,url,jobs", ]); }, loadManifest(runId, runAttempt, manifestPath) { return downloadParentManifestEvidence(runId, runAttempt, normalizedRepository, manifestPath); }, }; } function loadValidatedParentEvidence({ client, manifestPath, repository, runId }) { const parentView = client.getRunView(runId); const parentRun = client.getRun(runId); validateCompletedParentRun(parentView, parentRun, repository, runId); const manifestEvidence = client.loadManifest(runId, parentRun.run_attempt, manifestPath); if (!manifestEvidence) { throw new Error(`successful parent run is missing its release validation manifest: ${runId}`); } const manifest = validateParentManifest(manifestEvidence.manifest, { runAttempt: parentRun.run_attempt, runId, workflowRef: parentRun.head_branch, workflowSha: parentRun.head_sha, }); validateManifestArtifactBinding(manifestEvidence.artifact, manifest, parentRun, runId); return { artifact: manifestEvidence.artifact, manifest, manifestJson: canonicalJson(manifestEvidence.manifest), parentRun, parentView, }; } function trustedWorkflowFullRef(workflowRef) { return `refs/heads/${workflowRef}`; } function normalizeWorkflowPathRef(ref) { if (!ref || ref.startsWith("refs/")) { return ref; } return `refs/heads/${ref}`; } export function validateTrustedProducerIdentity(evidence, client, verifier, trustedWorkflowRef) { const { manifest, parentRun } = evidence; // Keep this predicate local: verifier source identity covers this file only. const shaPinned = SHA_PINNED_BRANCH_PATTERN.test(manifest.workflowRef ?? ""); if (manifest.workflowRef !== trustedWorkflowRef && !shaPinned) { throw new Error( `release evidence producer must run from trusted workflow ref: ${trustedWorkflowRef}`, ); } if (shaPinned) { if (manifest.version !== 3) { throw new Error("SHA-pinned release evidence requires a v3 manifest"); } if (!manifest.workflowRef.startsWith(`release-ci/${manifest.workflowSha.slice(0, 12)}-`)) { throw new Error("SHA-pinned release evidence branch does not match its workflow SHA"); } if (manifest.targetRef !== manifest.targetSha) { throw new Error("SHA-pinned release evidence target ref must equal its target SHA"); } } const expectedFullRef = trustedWorkflowFullRef(manifest.workflowRef); const runPath = String(parentRun.path ?? ""); const [runWorkflowPath, runWorkflowFullRef] = runPath.split("@", 2); if (runWorkflowPath !== ".github/workflows/full-release-validation.yml") { throw new Error("release evidence producer workflow path is not trusted"); } if (runWorkflowFullRef && normalizeWorkflowPathRef(runWorkflowFullRef) !== expectedFullRef) { throw new Error("release evidence producer workflow full ref is not trusted"); } let workflowRefProof = "legacy-v2-main-ancestry"; if (manifest.version === 3) { if (manifest.workflowRefType !== "branch" || manifest.workflowFullRef !== expectedFullRef) { throw new Error("release evidence producer workflow full ref is not trusted"); } workflowRefProof = shaPinned ? "manifest-v3-sha-pinned-main-ancestry" : "manifest-v3-branch"; } const comparison = client.compareCommits(manifest.workflowSha, verifier.sourceSha); if ( !["ahead", "identical"].includes(String(comparison.status)) || comparison.merge_base_commit?.sha !== manifest.workflowSha ) { throw new Error("release evidence producer is not on the trusted main verifier lineage"); } return { producerOnTrustedMainLineage: true, workflowFullRef: expectedFullRef, workflowQualifiedPath: `${runWorkflowPath}@${expectedFullRef}`, workflowRefProof, workflowRefType: "branch", workflowRunPath: runPath, }; } function normalizedParentTuple(evidence, identity) { const { manifest, parentRun } = evidence; return { artifact: normalizedManifestArtifact(evidence.artifact, manifest.runAttempt), conclusion: parentRun.conclusion, manifest: evidence.manifestJson, manifestVersion: manifest.version, runAttempt: manifest.runAttempt, runId: manifest.runId, status: parentRun.status, targetSha: manifest.targetSha, url: parentRun.html_url ?? evidence.parentView.url, ...identity, workflowPath: workflowPath(parentRun), workflowRef: manifest.workflowRef, workflowSha: manifest.workflowSha, }; } export function resolveVerifierIdentity( sourceSha, verifierSourceContent, repositoryRoot = RELEASE_EVIDENCE_REPO_ROOT, ) { let normalizedSourceSha = sourceSha ?? process.env.GITHUB_SHA; if (!/^[a-f0-9]{40}$/u.test(String(normalizedSourceSha ?? ""))) { try { normalizedSourceSha = execFileSync("git", ["-C", repositoryRoot, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); } catch { normalizedSourceSha = null; } } if (!/^[a-f0-9]{40}$/u.test(String(normalizedSourceSha ?? ""))) { throw new Error("release evidence verifier source SHA is unavailable"); } const script = readFileSync(RELEASE_EVIDENCE_FILE); const scriptSha256 = createHash("sha256").update(script).digest("hex"); let sourceScript; if (verifierSourceContent !== undefined) { sourceScript = Buffer.from(verifierSourceContent); } else { try { sourceScript = execFileSync( "git", ["-C", repositoryRoot, "show", `${normalizedSourceSha}:${RELEASE_EVIDENCE_SCRIPT}`], { maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], }, ); } catch { throw new Error("release evidence verifier source blob is unavailable"); } } const sourceScriptSha256 = createHash("sha256").update(sourceScript).digest("hex"); if (scriptSha256 !== sourceScriptSha256) { throw new Error("release evidence verifier script differs from its source SHA"); } return { schemaVersion: 3, script: RELEASE_EVIDENCE_SCRIPT, scriptSha256, sourceSha: normalizedSourceSha, }; } function validateStrictChildRun({ child, client, parentEvidence, parentJobs, repository, runId }) { const run = client.getRun(runId); const originAttempt = resolveManifestChildOriginAttempt( run, child, parentEvidence.manifest, parentJobs, ); if (originAttempt === undefined) { throw new Error(`manifest child dispatch tuple mismatch: ${child.name}`); } const parentJob = selectManifestParentJob( parentJobs, child, parentEvidence.manifest, originAttempt, ); validateManifestChildRun( run, child, runId, parentEvidence.manifest, parentJobs, client.getJobLog(parentJob.id), repository, ); if ( run.repository?.full_name !== repository || run.status !== "completed" || run.conclusion !== "success" || run.head_sha !== parentEvidence.manifest.workflowSha ) { throw new Error(`manifest child run is not exact completed/success evidence: ${child.name}`); } if (child.manifestKey === "productPerformance") { validatePerformanceArtifactOnlyJobs(client.getParentJobs(runId), run.run_attempt); } return { conclusion: run.conclusion, dispatchNonce: `full-release-validation-${parentEvidence.manifest.runId}-${originAttempt}${child.suffix}`, displayTitle: run.display_title, event: run.event, headBranch: run.head_branch, parentJobId: String(parentJob.id), path: workflowPath(run), role: child.manifestKey, runAttempt: normalizePositiveInteger(run.run_attempt, `${child.name} run attempt`), runId: String(run.id), sourceParentAttempt: originAttempt, sourceParentRunId: parentEvidence.manifest.runId, status: run.status, url: run.html_url, workflowSha: run.head_sha, ...(child.manifestKey === "productPerformance" ? { reportPublication: "artifact-only" } : {}), }; } export function validateReleaseRunEvidence( { manifestPath, repository = DEFAULT_REPO, runId, trustedWorkflowRef = "main", verifierSourceContent, verifierSourceSha, }, client, ) { const normalizedRepository = normalizeRepository(repository); const normalizedRunId = normalizeRequiredRunId(runId, "full release run ID"); const normalizedTrustedWorkflowRef = normalizeWorkflowRef( trustedWorkflowRef, "trusted workflow ref", ); const evidenceClient = client ?? createReleaseEvidenceClient(normalizedRepository); const verifier = resolveVerifierIdentity(verifierSourceSha, verifierSourceContent); const currentEvidence = loadValidatedParentEvidence({ client: evidenceClient, manifestPath, repository: normalizedRepository, runId: normalizedRunId, }); const producerIdentities = new Map([ [ currentEvidence.manifest.runId, validateTrustedProducerIdentity( currentEvidence, evidenceClient, verifier, normalizedTrustedWorkflowRef, ), ], ]); let rootEvidence = currentEvidence; let selectedEvidence = currentEvidence; const reuse = currentEvidence.manifest.evidenceReuse; if (reuse) { rootEvidence = loadValidatedParentEvidence({ client: evidenceClient, repository: normalizedRepository, runId: reuse.runId, }); selectedEvidence = reuse.selectedRunId === reuse.runId ? rootEvidence : loadValidatedParentEvidence({ client: evidenceClient, repository: normalizedRepository, runId: reuse.selectedRunId, }); validateEvidenceReuseChain( currentEvidence.manifest, selectedEvidence.manifest, rootEvidence.manifest, ); } for (const evidence of [currentEvidence, selectedEvidence, rootEvidence]) { if (!producerIdentities.has(evidence.manifest.runId)) { producerIdentities.set( evidence.manifest.runId, validateTrustedProducerIdentity( evidence, evidenceClient, verifier, normalizedTrustedWorkflowRef, ), ); } } const selectedKeys = requiredChildKeysForRerunGroup(rootEvidence.manifest.rerunGroup); const expectedChildren = expectedSelectedChildDispatches( rootEvidence.manifest.runId, rootEvidence.manifest.runAttempt, rootEvidence.manifest.workflowRef, selectedKeys, ); const parentJobs = evidenceClient.getParentJobs(rootEvidence.manifest.runId); const children = manifestChildEntries(rootEvidence.manifest, expectedChildren, selectedKeys).map( ({ child, runId: childRunId }) => validateStrictChildRun({ child, client: evidenceClient, parentEvidence: rootEvidence, parentJobs, repository: normalizedRepository, runId: childRunId, }), ); const current = normalizedParentTuple( currentEvidence, producerIdentities.get(currentEvidence.manifest.runId), ); const root = normalizedParentTuple( rootEvidence, producerIdentities.get(rootEvidence.manifest.runId), ); const childConclusions = Object.fromEntries( children.map((child) => [child.role, child.conclusion]), ); return canonicalJson({ children, conclusions: { allRequiredSucceeded: children.every((child) => child.conclusion === "success"), children: childConclusions, current: current.conclusion, root: root.conclusion, }, controls: rootEvidence.manifest.controls, current, directRoot: !reuse, evidenceReuse: reuse ? { changedPaths: reuse.changedPaths, evidenceSha: reuse.evidenceSha, policy: reuse.policy, rootRunId: reuse.runId, selectedRunId: reuse.selectedRunId, } : null, manifest: rootEvidence.manifestJson, releaseProfile: rootEvidence.manifest.releaseProfile, repository: normalizedRepository, rerunGroup: rootEvidence.manifest.rerunGroup, root, runReleaseSoak: rootEvidence.manifest.runReleaseSoak === "true", schema: RELEASE_EVIDENCE_SCHEMA, producerOnTrustedMainLineage: true, trustedWorkflowFullRef: trustedWorkflowFullRef(normalizedTrustedWorkflowRef), trustedWorkflowRef: normalizedTrustedWorkflowRef, valid: true, validationInputs: rootEvidence.manifest.validationInputs ?? null, verifier, }); } export function parseReleaseCiSummaryArgs(argv) { const options = { intervalMs: 30_000, json: false, manifestPath: undefined, repository: DEFAULT_REPO, runId: undefined, trustedWorkflowRef: "main", validate: false, verifierSourceFile: undefined, verifierSourceSha: undefined, watch: false, }; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--validate-run") { options.validate = true; options.runId = argv[++index]; } else if (argument === "--repo") { options.repository = argv[++index]; } else if (argument === "--manifest") { options.manifestPath = argv[++index]; } else if (argument === "--trusted-workflow-ref") { options.trustedWorkflowRef = argv[++index]; } else if (argument === "--verifier-source-sha") { options.verifierSourceSha = argv[++index]; } else if (argument === "--verifier-source-file") { options.verifierSourceFile = argv[++index]; } else if (argument === "--json") { options.json = true; } else if (argument === "--watch") { options.watch = true; } else if (argument === "--interval") { const seconds = argv[++index]; if (!/^[1-9][0-9]*$/u.test(seconds ?? "")) { throw new Error("--interval requires a positive number of seconds"); } options.intervalMs = Number(seconds) * 1000; } else if (!argument.startsWith("-") && !options.runId && !options.validate) { options.runId = argument; } else { throw new Error(`unknown or incomplete argument: ${argument}`); } } if (!options.validate && options.manifestPath) { throw new Error("--manifest requires --validate-run"); } if (options.validate && options.watch) { throw new Error("--watch cannot be combined with --validate-run"); } if (options.verifierSourceFile && !options.verifierSourceSha) { throw new Error("--verifier-source-file requires --verifier-source-sha"); } if (!options.runId) { throw new Error("full release run ID is required"); } return options; } function printUsage() { console.error( [ "usage: release-ci-summary.mjs ", " release-ci-summary.mjs --watch [--interval seconds]", " release-ci-summary.mjs --validate-run [--repo owner/name] [--trusted-workflow-ref main] [--manifest path] [--verifier-source-sha sha --verifier-source-file path] --json", ].join("\n"), ); } export function releaseCiWatchFingerprint(parent) { return JSON.stringify({ attempt: parent.attempt, conclusion: parent.conclusion ?? "", jobs: (parent.jobs ?? []) .map((job) => ({ conclusion: job.conclusion ?? "", name: job.name, status: job.status, })) .toSorted((left, right) => left.name.localeCompare(right.name)), status: parent.status, }); } function summarizeReleaseCiRun(options) { execFileSync( process.execPath, [ RELEASE_EVIDENCE_FILE, options.runId, "--repo", options.repository, "--trusted-workflow-ref", options.trustedWorkflowRef, ], { stdio: "inherit" }, ); } export async function watchReleaseCiRun(options, overrides = {}) { const fetchParent = overrides.fetchParent ?? (() => jsonGh([ "run", "view", options.runId, "--repo", options.repository, "--json", "status,conclusion,attempt,jobs", ])); const summarize = overrides.summarize ?? (() => summarizeReleaseCiRun(options)); const sleep = overrides.sleep ?? ((milliseconds) => new Promise((complete) => { setTimeout(complete, milliseconds); })); let previousFingerprint; while (true) { const parent = fetchParent(); const fingerprint = releaseCiWatchFingerprint(parent); if (fingerprint !== previousFingerprint) { summarize(); previousFingerprint = fingerprint; } if (parent.status === "completed") { if (parent.conclusion !== "success") { throw new Error( `full release run ${options.runId} completed with ${parent.conclusion || "no conclusion"}`, ); } return; } await sleep(options.intervalMs); } } async function main() { let options; try { options = parseReleaseCiSummaryArgs(process.argv.slice(2)); } catch (error) { printUsage(); console.error(error instanceof Error ? error.message : String(error)); process.exit(2); } const { repository, runId } = options; if (options.validate) { try { const evidence = validateReleaseRunEvidence({ manifestPath: options.manifestPath, repository, runId, trustedWorkflowRef: options.trustedWorkflowRef, verifierSourceContent: options.verifierSourceFile ? readFileSync(options.verifierSourceFile) : undefined, verifierSourceSha: options.verifierSourceSha, }); console.log(JSON.stringify(evidence, null, options.json ? 2 : 0)); } catch (error) { const failure = { error: error instanceof Error ? error.message : String(error), schema: RELEASE_EVIDENCE_SCHEMA, valid: false, }; if (options.json) { console.log(JSON.stringify(failure, null, 2)); } else { console.error(failure.error); } process.exit(1); } return; } if (options.watch) { await watchReleaseCiRun(options); return; } const core = rate(); if (core) { const reset = new Date(core.reset * 1000).toISOString(); console.log(`rate: remaining=${core.remaining}/${core.limit} reset=${reset}`); if (core.remaining < 20) { console.error("rate too low for CI summary; wait for reset before polling"); process.exit(3); } } const parent = jsonGh([ "run", "view", runId, "--repo", repository, "--json", "status,conclusion,attempt,headBranch,headSha,url,jobs", ]); validateParentRunBinding(parent, githubRestJson(`actions/runs/${runId}`, repository), runId); console.log(`parent: ${runId} ${parent.status}/${parent.conclusion || "none"}`); console.log(`workflow-ref: ${parent.headBranch}`); console.log(`workflow-sha: ${parent.headSha}`); console.log(`url: ${parent.url}`); for (const job of parent.jobs ?? []) { const marker = job.conclusion || job.status; console.log(`parent-job: ${marker} ${job.name}`); } const currentManifestRaw = tryDownloadParentManifest(runId, parent.attempt, repository); let children; if (currentManifestRaw) { const currentManifest = validateParentManifest(currentManifestRaw, { runAttempt: parent.attempt, runId, workflowRef: parent.headBranch, workflowSha: parent.headSha, }); console.log(`candidate-sha: ${currentManifest.targetSha}`); console.log(`manifest-run: ${currentManifest.runId}/${currentManifest.runAttempt}`); let sourceManifest = currentManifest; let sourceParent = parent; if (currentManifest.evidenceReuse) { const rootRunId = currentManifest.evidenceReuse.runId; const rootParent = jsonGh([ "run", "view", rootRunId, "--repo", repository, "--json", "status,conclusion,attempt,headBranch,headSha,url,jobs", ]); validateParentRunBinding( rootParent, githubRestJson(`actions/runs/${rootRunId}`, repository), rootRunId, ); if (rootParent.status !== "completed" || rootParent.conclusion !== "success") { throw new Error(`evidence root run is not completed/success: ${rootRunId}`); } const rootManifestRaw = tryDownloadParentManifest(rootRunId, rootParent.attempt, repository); if (!rootManifestRaw) { throw new Error(`evidence root manifest is unavailable: ${rootRunId}`); } const rootManifest = validateParentManifest(rootManifestRaw, { runAttempt: rootParent.attempt, runId: rootRunId, workflowRef: rootParent.headBranch, workflowSha: rootParent.headSha, }); const selectedRunId = currentManifest.evidenceReuse.selectedRunId; let selectedManifest = rootManifest; if (selectedRunId !== rootRunId) { const selectedParent = jsonGh([ "run", "view", selectedRunId, "--repo", repository, "--json", "status,conclusion,attempt,headBranch,headSha,url,jobs", ]); validateParentRunBinding( selectedParent, githubRestJson(`actions/runs/${selectedRunId}`, repository), selectedRunId, ); if (selectedParent.status !== "completed" || selectedParent.conclusion !== "success") { throw new Error(`selected evidence run is not completed/success: ${selectedRunId}`); } const selectedManifestRaw = tryDownloadParentManifest( selectedRunId, selectedParent.attempt, repository, ); if (!selectedManifestRaw) { throw new Error(`selected evidence manifest is unavailable: ${selectedRunId}`); } selectedManifest = validateParentManifest(selectedManifestRaw, { runAttempt: selectedParent.attempt, runId: selectedRunId, workflowRef: selectedParent.headBranch, workflowSha: selectedParent.headSha, }); } const evidenceSha = validateEvidenceReuseChain( currentManifest, selectedManifest, rootManifest, ); sourceManifest = rootManifest; sourceParent = rootParent; console.log(`evidence-selected-run: ${selectedRunId}`); console.log(`evidence-root-run: ${rootRunId}`); console.log(`evidence-sha: ${evidenceSha}`); } const expectedChildren = expectedSelectedChildDispatches( sourceManifest.runId, sourceManifest.runAttempt, sourceManifest.workflowRef, requiredChildKeysForRerunGroup(sourceManifest.rerunGroup), ); const sourceParentJobs = findParentJobsAll(sourceManifest.runId, repository); children = manifestChildEntries( sourceManifest, expectedChildren, requiredChildKeysForRerunGroup(sourceManifest.rerunGroup), ).map(({ child, runId: childRunId }) => { const run = githubRestJson(`actions/runs/${childRunId}`, repository); const originAttempt = resolveManifestChildOriginAttempt( run, child, sourceManifest, sourceParentJobs, ); if (originAttempt === undefined) { throw new Error(`manifest child dispatch tuple mismatch: ${child.name}`); } const parentJob = selectManifestParentJob( sourceParentJobs, child, sourceManifest, originAttempt, ); const validatedRun = validateManifestChildRun( run, child, childRunId, { ...sourceManifest, workflowSha: sourceParent.headSha }, sourceParentJobs, parentJobLog(parentJob.id, repository), repository, ); if (child.manifestKey === "productPerformance") { validatePerformanceArtifactOnlyJobs( findParentJobsAll(childRunId, repository), run.run_attempt, ); } return { child, run: validatedRun }; }); } else { console.log("candidate-sha: unavailable (release validation manifest not uploaded)"); if (parent.status === "completed" && parent.conclusion === "success") { throw new Error("successful parent run is missing its release validation manifest"); } const selectedKeys = selectedChildKeys(parent.jobs ?? []); children = expectedSelectedChildDispatches( runId, parent.attempt, parent.headBranch, selectedKeys, ) .map((child) => { const run = findExactChildRun(child, repository); if (!run) { console.log( `child-missing: ${child.name} title=${child.displayTitle} branch=${child.headBranch}`, ); } return { child, run }; }) .filter((entry) => entry.run); } if (children.length === 0) { console.log("children: none found yet"); return; } console.log("children:"); for (const { child, run } of children) { console.log( `child: ${run.id} ${child.name} ${run.status}/${run.conclusion || "none"} branch=${run.head_branch} workflow_sha=${run.head_sha}`, ); console.log(`child-url: ${run.html_url}`); } } if (process.argv[1]?.endsWith("release-ci-summary.mjs")) { await main().catch( /** @param {unknown} error */ (error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }, ); }