Files
openclaw/scripts/validate-full-release-validation-evidence.mjs
Peter Steinberger fece8c9f54 fix(release): keep validation evidence immutable across reruns (#103906)
* fix(release): bind validation evidence to exact attempts

* test(release): cover exact validation attempts
2026-07-10 20:16:19 +01:00

197 lines
6.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Binds Full Release Validation run metadata to its v3 evidence manifest.
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const FULL_RELEASE_WORKFLOW = "Full Release Validation";
const FULL_RELEASE_WORKFLOW_PATH = ".github/workflows/full-release-validation.yml";
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
const PINNED_BRANCH_PATTERN = /^release-ci\/([a-f0-9]{12})-([1-9][0-9]*)$/u;
function normalizeWorkflowPathRef(ref) {
if (!ref || ref.startsWith("refs/")) {
return ref;
}
return `refs/heads/${ref}`;
}
export function normalizeFullReleaseValidationRun(run) {
const [workflowPath, workflowQualifiedRef] = String(run.path ?? run.workflowPath ?? "").split(
"@",
2,
);
return {
databaseId: String(run.id ?? run.databaseId ?? ""),
runAttempt: Number(run.run_attempt ?? run.runAttempt ?? run.attempt),
workflowName: run.name ?? run.workflowName,
workflowPath,
workflowQualifiedRef,
repository: run.repository?.full_name ?? run.repository,
headBranch: run.head_branch ?? run.headBranch,
headSha: run.head_sha ?? run.headSha,
event: run.event,
status: run.status,
conclusion: run.conclusion,
url: run.html_url ?? run.url,
};
}
export function isShaPinnedReleaseValidationBranch(branch) {
return PINNED_BRANCH_PATTERN.test(branch ?? "");
}
export function validateFullReleaseValidationEvidence({
run: rawRun,
manifest,
expectedRepository,
expectedRunId,
expectedTargetSha,
expectedWorkflowBranch,
isTrustedMainAncestor,
}) {
const run = normalizeFullReleaseValidationRun(rawRun);
const checks = [
["databaseId", String(expectedRunId)],
["workflowName", FULL_RELEASE_WORKFLOW],
["workflowPath", FULL_RELEASE_WORKFLOW_PATH],
["repository", expectedRepository],
["event", "workflow_dispatch"],
["status", "completed"],
["conclusion", "success"],
];
for (const [key, expected] of checks) {
if (run[key] !== expected) {
throw new Error(
`Referenced full release validation run ${expectedRunId} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`,
);
}
}
if (!Number.isInteger(run.runAttempt) || run.runAttempt < 1) {
throw new Error(`Referenced full release validation run ${expectedRunId} has invalid attempt.`);
}
if (!SHA_PATTERN.test(run.headSha ?? "")) {
throw new Error(
`Referenced full release validation run ${expectedRunId} has invalid head SHA.`,
);
}
const expectedQualifiedRef = `refs/heads/${run.headBranch}`;
const workflowQualifiedRef = normalizeWorkflowPathRef(run.workflowQualifiedRef);
if (workflowQualifiedRef && workflowQualifiedRef !== expectedQualifiedRef) {
throw new Error(
`Referenced full release validation run ${expectedRunId} has workflow path ref ${run.workflowQualifiedRef}, expected ${expectedQualifiedRef}.`,
);
}
if (manifest.version !== 3) {
throw new Error(
`Full release validation manifest must use version 3, got ${manifest.version}.`,
);
}
const manifestChecks = [
["workflowName", FULL_RELEASE_WORKFLOW],
["runId", String(expectedRunId)],
["runAttempt", String(run.runAttempt)],
["workflowRef", run.headBranch],
["workflowSha", run.headSha],
["workflowFullRef", expectedQualifiedRef],
["workflowRefType", "branch"],
["targetSha", expectedTargetSha],
];
for (const [key, expected] of manifestChecks) {
if (String(manifest[key] ?? "") !== expected) {
throw new Error(
`Full release validation manifest ${key} mismatch: expected ${expected}, got ${manifest[key] ?? "<missing>"}.`,
);
}
}
const pinnedMatch = PINNED_BRANCH_PATTERN.exec(run.headBranch ?? "");
if (!pinnedMatch) {
if (run.headBranch?.startsWith("release-ci/")) {
throw new Error(
`Referenced full release validation run ${expectedRunId} has untrusted head branch ${run.headBranch}.`,
);
}
const directBranches = new Set(["main", expectedWorkflowBranch].filter(Boolean));
if (directBranches.has(run.headBranch)) {
if (run.headBranch === "main" && !isTrustedMainAncestor?.(run.headSha)) {
throw new Error(
`Direct main validation workflow ${run.headSha} is not reachable from current main.`,
);
}
return { run, source: "direct" };
}
throw new Error(
`Referenced full release validation run ${expectedRunId} has untrusted head branch ${run.headBranch ?? "<missing>"}.`,
);
}
if (pinnedMatch[1] !== run.headSha.slice(0, 12)) {
throw new Error(
`SHA-pinned validation branch ${run.headBranch} does not match workflow SHA ${run.headSha}.`,
);
}
if (manifest.targetRef !== expectedTargetSha) {
throw new Error(
`SHA-pinned validation target ref mismatch: expected ${expectedTargetSha}, got ${manifest.targetRef ?? "<missing>"}.`,
);
}
if (Object.hasOwn(manifest, "evidenceReuse")) {
throw new Error("SHA-pinned validation evidence must not reuse another validation run.");
}
if (!isTrustedMainAncestor?.(run.headSha)) {
throw new Error(
`SHA-pinned validation workflow ${run.headSha} is not reachable from current main.`,
);
}
return { run, source: "sha-pinned-main" };
}
function gitIsAncestor(ancestor, target) {
const result = spawnSync(
"git",
["merge-base", "--is-ancestor", `${ancestor}^{commit}`, `${target}^{commit}`],
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
if (result.status === 0) {
return true;
}
if (result.status === 1) {
return false;
}
throw new Error(
`Could not validate trusted workflow ancestry: ${result.stderr?.trim() || result.signal || result.status}.`,
);
}
function main() {
const manifestPath = process.env.MANIFEST_FILE ?? "";
if (!manifestPath) {
throw new Error("MANIFEST_FILE is required.");
}
const run = JSON.parse(readFileSync(0, "utf8"));
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
const trustedMainRef = process.env.TRUSTED_MAIN_REF ?? "refs/remotes/origin/main";
const result = validateFullReleaseValidationEvidence({
run,
manifest,
expectedRepository: process.env.GITHUB_REPOSITORY,
expectedRunId: process.env.FULL_RELEASE_VALIDATION_RUN_ID,
expectedTargetSha: process.env.EXPECTED_SHA,
expectedWorkflowBranch: process.env.EXPECTED_WORKFLOW_BRANCH,
isTrustedMainAncestor: (sha) => gitIsAncestor(sha, trustedMainRef),
});
console.log(
`Using full release validation run ${result.run.databaseId} (${result.source}): ${result.run.url}`,
);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}