openclaw/scripts/full-release-validation-at-sha.mts
Peter Steinberger 72cca0df98
test(agents, memory, ui, tooling): remove low-value tests (batch d112) (#161835)
* test(transcripts): deslop t0348 tests

* test(memory-core): deslop t0340 tests

* test(agents): deslop t0334 tests

* test(line): deslop t0361 tests

* test(docker-e2e): deslop t0362 tests

* test(scripts): deslop t0338 tests

* test(release): deslop t0359 tests

* test(markdown): deslop t0357 tests

* test(ui): deslop t0360 tests

* test(vitest): deslop t0363 tests

* test(ci): align config owner expectation after test cleanup
2026-09-30 11:59:03 +00:00

2098 lines
75 KiB
TypeScript

#!/usr/bin/env node
// Dispatches full release validation against a temporary SHA-pinned branch.
import {
execFileSync,
spawnSync,
type ExecFileSyncOptionsWithBufferEncoding,
type ExecFileSyncOptionsWithStringEncoding,
} from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
closeSync,
existsSync,
fsyncSync,
linkSync,
lstatSync,
mkdirSync,
mkdtempSync,
openSync,
readFileSync,
renameSync,
rmSync,
statSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { isDeepStrictEqual } from "node:util";
import { parse as parseYaml } from "yaml";
import { isRecord as isJsonRecord } from "../packages/normalization-core/src/record-coerce.ts";
import {
decodePublicationDispatchEnvelope,
normalizePublicationIntent,
normalizePublicationLaneInputs,
publicationDispatchEnvelope,
publicationIntentInputs,
} from "./full-release-publication-contract.mjs";
import {
classifyReleaseGhTransportError,
formatReleaseStateOutcome,
isReleaseGhArtifactMissingError,
MAX_RELEASE_ARTIFACT_BYTES,
validateReleaseStateArtifact,
} from "./full-release-validation-policy.mjs";
import {
inspectActionsArtifactZipWithPolicy,
readBoundedRegularFile,
} from "./lib/actions-artifact-archive.mjs";
import { requireOptionArgument } from "./lib/arg-utils.mts";
import { execPlainGh } from "./lib/plain-gh.mjs";
import { parseReleaseContextRef, resolveReleaseContextIdentity } from "./lib/release-context.mjs";
import { validatePackageSourceRef } from "./package-source-preflight.mjs";
const REPOSITORY = "openclaw/openclaw";
const WORKFLOW = "full-release-validation.yml";
const TRUSTED_WORKFLOW_PATH = `.github/workflows/${WORKFLOW}`;
const RELEASE_ISOLATION_TOOLING_CONTRACT = "2";
const RELEASE_ISOLATION_TOOLING_CONTRACT_ENV = "RELEASE_ISOLATION_TOOLING_CONTRACT";
const RELEASE_EVIDENCE_VERIFIER_PATHS = [
"scripts/release-ci-summary.mjs",
".agents/skills/release-openclaw-ci/scripts/release-ci-summary.mjs",
];
const GH_READ_TIMEOUT_MS = 60_000;
export const FULL_RELEASE_WAIT_TIMEOUT_MINUTES = 720;
const FULL_RELEASE_GITHUB_POLL_INTERVAL_MS = 2 * 60_000;
const FULL_RELEASE_PROGRESS_INTERVAL_MS = 15 * 60_000;
const FULL_RELEASE_RUN_DISCOVERY_DELAYS_MS = [30_000, 60_000, 120_000];
// A run can wait in the runner queue before its first job uploads the witness.
const FULL_RELEASE_WITNESS_QUEUE_WAIT_MS = 3 * 60 * 60_000;
const ACTIVE_RUN_STATUSES = new Set(["requested", "queued", "pending", "waiting", "in_progress"]);
const RELEASE_DECISION_FILE = "full-release-decision.json";
const GH_NO_CACHE_HEADER = "Cache-Control: max-age=0";
const REQUEST_KIND = "openclaw.full-release-dispatch/v1";
const WITNESS_KIND = "openclaw.full-release-dispatch-inputs/v1";
const WITNESS_FILE = "dispatch-inputs.json";
const MAX_REQUEST_BYTES = 128 * 1024;
const MAX_WITNESS_ARCHIVE_BYTES = 256 * 1024;
const RUN_PAGE_SIZE = 20;
const MAX_RUN_PAGES = 5;
const GH_READ_OPTIONS = {
encoding: "utf8",
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "inherit"],
timeout: GH_READ_TIMEOUT_MS,
} satisfies ExecFileSyncOptionsWithStringEncoding;
const TRUSTED_WORKFLOW_TAG_PATTERN = /^release-publish\/([a-f0-9]{12})-[1-9][0-9]*$/u;
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
const RERUN_GROUPS = new Set([
"all",
"ci",
"plugin-prerelease",
"install-smoke",
"cross-os",
"live-e2e",
"package",
"qa-parity",
"qa-live",
"npm-telegram",
"performance",
]);
const DEFAULT_INPUTS = {
provider: "openai",
mode: "both",
rerun_group: "all",
reuse_evidence: "true",
fail_fast: "false",
};
type ReleaseInputs = Record<string, string> &
typeof DEFAULT_INPUTS &
Partial<Record<"release_profile" | "allow_unreleased_changelog", string>>;
type CommandOptions = {
dryRun?: boolean;
stdio?: "inherit" | ["ignore", "pipe" | "ignore", "pipe" | "inherit" | "ignore"];
timeoutMs?: number;
};
type CommandStatus = {
error?: Error;
signal?: unknown;
status: number | null;
stderr: unknown;
stdout: unknown;
};
type TemporaryRefParams = {
keepBranch: boolean;
dryRun: boolean;
parentConclusion: string;
evidenceVerified: boolean;
};
type TrustedWorkflowHarness = {
contract: "1" | "2";
verifierPath: string;
};
type DispatchInputs = Record<string, string | boolean | number>;
type DispatchRun = { id: number; attempt: number };
type DispatchRequest = {
id: string;
host: "github.com";
repository: typeof REPOSITORY;
workflowId: number;
workflowPath: typeof TRUSTED_WORKFLOW_PATH;
event: "workflow_dispatch";
workflowSha: string;
trustedWorkflowRef: string;
targetSha: string;
targetVersion: string;
targetContextRef: string;
workflowRef: string;
wireInputs: Record<string, string>;
inputs: DispatchInputs;
effectiveSoak: boolean;
};
type DispatchRecord = {
kind: typeof REQUEST_KIND;
request: DispatchRequest;
phase: "prepared" | "attempted" | "observed" | "rejected";
refs: { workflow: "intended" | "uncertain" | "created" };
error: "none" | "transport" | "unclassified" | "http-rejection";
run: DispatchRun | null;
};
function stringValue(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function displayValue(value: unknown): string {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return value === null ? "null" : (JSON.stringify(value) ?? "<undefined>");
}
function requiredPositiveInteger(value: unknown, label: string): number {
const normalized = Number(value);
if (!Number.isSafeInteger(normalized) || normalized < 1) {
throw new Error(`${label} must be a positive integer`);
}
return normalized;
}
function usage() {
console.error(`Usage: node scripts/full-release-validation-at-sha.mjs [--sha <target-sha>] [--target-ref <canonical-release-branch-or-tag>] [--workflow-sha <trusted-tooling-sha>] [--trusted-workflow-ref <main-or-release-publish-tag>] [--request-file <path>] [--keep-branch] [--dry-run] [-- -f key=value ...]
node scripts/full-release-validation-at-sha.mjs --reconcile-request <path>
Retains a private request artifact before remote mutations. An existing --request-file
always performs read-only reconciliation. --reconcile-request refuses a missing file.
Retain the artifact until operator cleanup; its loss never proves non-execution.
Frozen tooling must declare FULL_RELEASE_DISPATCH_WITNESS_CONTRACT=1 before a new request.
Preflights the Validation SHA with a bare-SHA fetch into a fresh temporary repository.
Creates one immutable release-ci/* workflow ref pinned to the exact Tooling SHA,
dispatches Full Release Validation with the full Validation SHA as its ref input
and expected_sha as its immutable identity,
watches the parent run, verifies all child workflow head SHAs match the trusted
workflow lineage through the release evidence manifest, then deletes the temporary
workflow ref by default. --keep-branch retains that ref. Exact-target and changelog-only Release SHA
evidence reuse stay enabled; pass -f reuse_evidence=false to force a fresh
run. Child workflows collect independent failures by default; pass
-f fail_fast=true to cancel only an exact still-active child after Release
Decision identifies a blocking failure for that child. The release
branch accepts its final package version or a matching beta prerelease.
A numeric correction branch also accepts the base package only when its
published base tag resolves to the exact Validation SHA.
The release profile defaults to beta for beta candidates and stable otherwise; pass
-f release_profile=full for the broad advisory sweep. Focused retries must use
one controller rerun_group; the removed release-checks aggregate and the direct
child's manual qa aggregate are not accepted.`);
}
function run(command: string, args: string[], options: CommandOptions = {}) {
if (options.dryRun) {
console.log(["+", command, ...args].join(" "));
return "";
}
const output = execFileSync(command, args, {
encoding: "utf8",
stdio: options.stdio ?? ["ignore", "pipe", "inherit"],
});
return typeof output === "string" ? output.trim() : "";
}
function runStatus(command: string, args: string[], options: CommandOptions = {}): CommandStatus {
if (options.dryRun) {
console.log(["+", command, ...args].join(" "));
return { status: 0, stderr: "", stdout: "" };
}
return spawnSync(command, args, {
encoding: "utf8",
killSignal: "SIGKILL",
stdio: options.stdio ?? ["ignore", "pipe", "inherit"],
timeout: options.timeoutMs ?? GH_READ_TIMEOUT_MS,
});
}
function runGh(inputArgs: string[], options: CommandOptions = {}) {
const args =
inputArgs[0] === "api" && !inputArgs.includes("--hostname")
? [...inputArgs, "--hostname", "github.com"]
: inputArgs;
if (options.dryRun) {
console.log(["+", "gh", ...args].join(" "));
return "";
}
const output = execPlainGh(args, {
encoding: "utf8",
stdio: options.stdio ?? ["ignore", "pipe", "inherit"],
...(options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs }),
});
return typeof output === "string" ? (args.includes("--include") ? output : output.trim()) : "";
}
function runGhStatus(args: string[], options: CommandOptions = {}): CommandStatus {
try {
return {
signal: null,
status: 0,
stderr: "",
stdout: execPlainGh(args, {
encoding: "utf8",
killSignal: "SIGKILL",
stdio: options.stdio ?? ["ignore", "pipe", "inherit"],
timeout: options.timeoutMs ?? GH_READ_TIMEOUT_MS,
}),
};
} catch (error) {
const failure = error instanceof Error ? error : new Error(String(error));
const details = failure as Error & {
signal?: unknown;
status?: number | null;
stderr?: unknown;
stdout?: unknown;
};
return {
error: failure,
signal: details.signal,
status: details.status ?? 1,
stderr: details.stderr ?? "",
stdout: details.stdout ?? "",
};
}
}
function readGhApi(
endpoint: string,
fields: string[] = [],
options: ExecFileSyncOptionsWithStringEncoding = GH_READ_OPTIONS,
) {
return execPlainGh(
[
"api",
"--method",
"GET",
endpoint,
...fields,
"--hostname",
"github.com",
"-H",
GH_NO_CACHE_HEADER,
],
options,
);
}
function commandFailureMessage(error: unknown): string {
if (error === undefined || error === null) {
return "";
}
if (!(error instanceof Error)) {
return displayValue(error);
}
const details = error as Error & {
cause?: unknown;
stderr?: unknown;
stdout?: unknown;
};
const outputText = (value: unknown) => {
if (typeof value === "string") {
return value.trim();
}
return Buffer.isBuffer(value) ? value.toString("utf8").trim() : "";
};
return [
outputText(details.stderr),
outputText(details.stdout),
error.message,
details.cause === error ? "" : commandFailureMessage(details.cause),
]
.filter(Boolean)
.join("\n");
}
function isUnsupportedAllowEscapeSequencesFlag(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
const stderr = (error as Error & { stderr?: unknown }).stderr;
const text =
typeof stderr === "string" ? stderr : Buffer.isBuffer(stderr) ? stderr.toString("utf8") : "";
return text
.replaceAll("\r\n", "\n")
.split("\n")
.some((line) => line.trim() === "unknown flag: --allow-escape-sequences");
}
function createTemporaryRef(ref: string, sha: string, dryRun: boolean) {
try {
runGh(
[
"api",
"--method",
"POST",
`repos/${REPOSITORY}/git/refs`,
"-f",
`ref=${ref}`,
"-f",
`sha=${sha}`,
],
{ dryRun, stdio: ["ignore", "pipe", "pipe"] },
);
} catch (error) {
throw new Error(commandFailureMessage(error), { cause: error });
}
}
function deleteTemporaryRef(ref: string, dryRun: boolean) {
try {
runGh(
["api", "--method", "DELETE", `repos/${REPOSITORY}/git/refs/${ref.slice("refs/".length)}`],
{ dryRun },
);
} catch (error) {
throw new Error(`Failed to delete temporary ref ${ref}: ${commandFailureMessage(error)}`, {
cause: error,
});
}
}
export function parseArgs(argv: string[]) {
const inputs: ReleaseInputs = { ...DEFAULT_INPUTS };
const args = {
sha: "",
targetRef: "",
trustedWorkflowRef: "main",
workflowSha: "",
requestFile: "",
reconcileRequest: "",
specifiedInputs: [] as string[],
keepBranch: false,
dryRun: false,
inputs,
};
const valueOptions = [
["--sha", "sha"],
["--request-file", "requestFile"],
["--reconcile-request", "reconcileRequest"],
["--workflow-sha", "workflowSha"],
["--trusted-workflow-ref", "trustedWorkflowRef"],
["--target-ref", "targetRef"],
] as const;
const assignInput = (assignment: string, errorMessage: string) => {
const [key, ...valueParts] = assignment.split("=");
if (!key || valueParts.length === 0) {
throw new Error(errorMessage);
}
args.inputs[key] = valueParts.join("=");
args.specifiedInputs.push(key);
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]!;
if (arg === "--help" || arg === "-h") {
usage();
process.exit(0);
}
const valueKey = valueOptions.find(([flag]) => flag === arg)?.[1];
if (valueKey) {
args[valueKey] = requireOptionArgument(argv, i, arg);
i += 1;
continue;
}
if (arg === "--keep-branch") {
args.keepBranch = true;
continue;
}
if (arg === "--dry-run") {
args.dryRun = true;
continue;
}
if (arg === "--") {
const extras = argv.slice(i + 1);
for (let extraIndex = 0; extraIndex < extras.length; extraIndex += 1) {
const extra = extras[extraIndex]!;
let assignment;
if (extra === "-f") {
assignment = requireOptionArgument(extras, extraIndex, extra);
extraIndex += 1;
} else {
assignment = extra.startsWith("-f") ? extra.slice(2).trim() : extra;
}
assignInput(assignment, `Unsupported extra argument after --: ${extra}`);
}
break;
}
if (arg === "-f") {
const assignment = requireOptionArgument(argv, i, arg);
i += 1;
assignInput(assignment, `Invalid -f assignment: ${assignment}`);
continue;
}
if (arg.startsWith("-f") && arg.includes("=")) {
const assignment = arg.slice(2).trim();
assignInput(assignment, `Invalid -f assignment: ${arg}`);
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
if (args.reconcileRequest) {
if (argv.length !== 2 || argv[0] !== "--reconcile-request") {
throw new Error("--reconcile-request accepts only the retained request path");
}
return args;
}
if (!["true", "false"].includes(args.inputs.reuse_evidence)) {
throw new Error("reuse_evidence must be true or false");
}
if (!["true", "false"].includes(args.inputs.fail_fast)) {
throw new Error("fail_fast must be true or false");
}
if (
Object.hasOwn(args.inputs, "allow_unreleased_changelog") &&
!["true", "false"].includes(args.inputs.allow_unreleased_changelog ?? "")
) {
throw new Error("allow_unreleased_changelog must be true or false");
}
if (
args.inputs.release_profile &&
!["beta", "stable", "full"].includes(args.inputs.release_profile)
) {
throw new Error("release_profile must be beta, stable, or full");
}
if (!RERUN_GROUPS.has(args.inputs.rerun_group)) {
throw new Error(`rerun_group must be one of: ${[...RERUN_GROUPS].join(", ")}`);
}
if (Object.hasOwn(args.inputs, "ref")) {
throw new Error("SHA-pinned release validation reserves the ref input for --sha");
}
if (Object.hasOwn(args.inputs, "expected_sha")) {
throw new Error("SHA-pinned release validation reserves expected_sha for the resolved --sha");
}
if (Object.hasOwn(args.inputs, "trusted_workflow_json")) {
throw new Error("SHA-pinned release validation reserves trusted_workflow_json");
}
if (
args.targetRef.includes("-alpha.") ||
args.targetRef.includes("tideclaw/alpha/") ||
args.trustedWorkflowRef.includes("tideclaw/alpha/")
) {
throw new Error("Alpha releases are retired; use a beta prerelease instead.");
}
const targetContext = parseReleaseContextRef(args.targetRef);
if (args.targetRef && !targetContext) {
throw new Error("--target-ref must be a canonical OpenClaw release branch or tag");
}
args.targetRef = targetContext?.ref ?? args.targetRef;
if (
args.trustedWorkflowRef !== "main" &&
!TRUSTED_WORKFLOW_TAG_PATTERN.test(args.trustedWorkflowRef)
) {
throw new Error(
"--trusted-workflow-ref must be main or a protected release-publish/<12hex>-<decimal> tag",
);
}
if (args.trustedWorkflowRef !== "main" && !SHA_PATTERN.test(args.workflowSha.toLowerCase())) {
throw new Error(
"protected release-publish workflow refs require --workflow-sha with an explicit full Tooling SHA",
);
}
if (
targetContext &&
targetContext.kind !== "release tag" &&
!SHA_PATTERN.test(args.workflowSha.toLowerCase())
) {
throw new Error(
"release-branch validation requires --workflow-sha with an explicit full Tooling SHA",
);
}
return args;
}
export function resolveRemoteTargetRefSha(
targetRef: string,
executeGit: (args: string[]) => string = (args) => run("git", args),
) {
const context = parseReleaseContextRef(targetRef);
if (!context) {
throw new Error("Target ref must be a canonical OpenClaw release branch or tag");
}
if (context.kind !== "release tag") {
return (
executeGit(["ls-remote", "--heads", "origin", `refs/heads/${context.ref}`]).split(
/\s+/u,
)[0] ?? ""
);
}
const tagRef = `refs/tags/${context.ref}`;
const peeledSha = executeGit(["ls-remote", "--tags", "origin", `${tagRef}^{}`]).split(/\s+/u)[0];
if (peeledSha) {
return peeledSha;
}
return executeGit(["ls-remote", "--tags", "origin", tagRef]).split(/\s+/u)[0] ?? "";
}
export function verifyTargetRef(
targetRef: string,
targetSha: string,
targetVersion: string,
resolveRemoteSha: (ref: string) => string = resolveRemoteTargetRefSha,
isAncestor: (ancestor: string, descendant: string) => boolean = (ancestor, descendant) =>
runStatus("git", ["merge-base", "--is-ancestor", ancestor, descendant], {
stdio: ["ignore", "ignore", "ignore"],
}).status === 0,
) {
if (!targetRef) {
return targetSha;
}
const identity = resolveReleaseContextIdentity(targetRef, targetVersion);
if (!identity) {
throw new Error("Target ref must be a canonical OpenClaw release branch or tag");
}
const remoteSha = resolveRemoteSha(targetRef);
if (!remoteSha) {
throw new Error(`Target ref ${targetRef} does not resolve to a commit`);
}
if (identity.kind !== "release tag") {
if (!isAncestor(targetSha, remoteSha)) {
throw new Error(
`Target SHA ${targetSha} is not reachable from release branch ${targetRef} at ${remoteSha}`,
);
}
} else if (remoteSha.toLowerCase() !== targetSha.toLowerCase()) {
throw new Error(`Target ref ${targetRef} does not resolve to ${targetSha}`);
}
if (identity.baseTag) {
const baseSha = resolveRemoteSha(identity.baseTag);
if (baseSha.toLowerCase() !== targetSha.toLowerCase()) {
throw new Error(
`Fallback correction ${identity.releaseTag} must use the same source commit as ${identity.baseTag}; expected ${targetSha}, found ${baseSha || "missing"}.`,
);
}
}
return targetRef;
}
function resolveSha(requestedSha: string) {
const rev = requestedSha || "HEAD";
return run("git", ["rev-parse", "--verify", `${rev}^{commit}`], { dryRun: false });
}
function fetchTargetRef(targetRef: string) {
if (!targetRef) {
return;
}
const context = parseReleaseContextRef(targetRef);
if (!context) {
throw new Error("Target ref must be a canonical OpenClaw release branch or tag");
}
const sourceRef = `refs/${context.kind === "release tag" ? "tags" : "heads"}/${context.ref}`;
run("git", ["fetch", "--no-tags", "origin", sourceRef], {
stdio: "inherit",
});
}
function resolveTargetSha(requestedSha: string, targetRef: string) {
fetchTargetRef(targetRef);
const revision = requestedSha || "HEAD";
const resolved = runStatus("git", ["rev-parse", "--verify", `${revision}^{commit}`], {
stdio: ["ignore", "pipe", "ignore"],
});
const resolvedSha = typeof resolved.stdout === "string" ? resolved.stdout.trim() : "";
if (resolved.status !== 0 || !resolvedSha) {
throw new Error(
targetRef
? `Target SHA ${revision} is not available locally after fetching ${targetRef}`
: `Target SHA ${revision} is not available locally; pass --target-ref so it can be fetched by name`,
);
}
return resolvedSha;
}
function preflightTargetShaFetch(targetSha: string) {
const directory = mkdtempSync(join(tmpdir(), "openclaw-release-fetch-"));
try {
run("git", ["-C", directory, "init", "-q"]);
const result = runStatus(
"git",
[
"-C",
directory,
"fetch",
"--no-tags",
"--depth=1",
"--filter=blob:none",
`https://github.com/${REPOSITORY}.git`,
targetSha,
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
if (result.status !== 0) {
throw new Error(
`GitHub refused to serve Validation SHA ${targetSha} by bare SHA; child checkouts fetch it the same way, so dispatch would fail. Push it to a GitHub branch first. ${stringValue(result.stderr).trim().slice(-2000) || result.error?.message || "git fetch failed"}`,
);
}
console.log(`Validation SHA fetchable by bare SHA: ${targetSha}`);
} finally {
rmSync(directory, { force: true, recursive: true });
}
}
function targetVersionForTarget(
targetSha: string,
readPackageJson: (sha: string) => string = (sha) => run("git", ["show", `${sha}:package.json`]),
): string {
let version: unknown;
try {
version = JSON.parse(readPackageJson(targetSha)).version;
} catch {
throw new Error(`Could not read package.json from target SHA ${targetSha}`);
}
if (typeof version !== "string" || !/^[0-9]{4}\.[0-9]+\.[0-9]+(?:-.+)?$/u.test(version)) {
throw new Error(`Target SHA ${targetSha} has an invalid package version`);
}
return version;
}
function releaseProfileForVersion(version: string): "beta" | "stable" {
if (version.includes("-alpha.")) {
throw new Error("Alpha releases are retired; use a beta prerelease instead.");
}
return /-beta\.[1-9][0-9]*$/u.test(version) ? "beta" : "stable";
}
export function releaseProfileForTarget(
targetSha: string,
readPackageJson: (sha: string) => string = (sha) => run("git", ["show", `${sha}:package.json`]),
): "beta" | "stable" {
return releaseProfileForVersion(targetVersionForTarget(targetSha, readPackageJson));
}
export function verifyTrustedWorkflowRef(
workflowSha: string,
trustedWorkflowRef: string,
resolveRemoteTagSha: (tag: string) => string = (tag) =>
run("git", ["ls-remote", "--tags", "origin", `refs/tags/${tag}`]).split(/\s+/u)[0] ?? "",
isMainAncestor: (sha: string) => boolean = (sha) =>
runStatus("git", ["merge-base", "--is-ancestor", sha, "refs/remotes/origin/main"]).status === 0,
) {
if (trustedWorkflowRef === "main") {
if (!isMainAncestor(workflowSha)) {
throw new Error(
`Workflow SHA ${workflowSha} is not reachable from current origin/main; refusing an untrusted release harness.`,
);
}
return;
}
const tagMatch = trustedWorkflowRef.match(TRUSTED_WORKFLOW_TAG_PATTERN);
if (!tagMatch) {
throw new Error(
"trusted workflow ref must be main or a protected release-publish/<12hex>-<decimal> tag",
);
}
if (workflowSha.slice(0, 12) !== tagMatch[1]) {
throw new Error(
`Trusted workflow tag ${trustedWorkflowRef} does not match Tooling SHA ${workflowSha}`,
);
}
const remoteTagSha = resolveRemoteTagSha(trustedWorkflowRef);
if (!remoteTagSha) {
throw new Error(`Trusted workflow tag ${trustedWorkflowRef} does not exist on origin`);
}
if (remoteTagSha.toLowerCase() !== workflowSha.toLowerCase()) {
throw new Error(
`Trusted workflow tag ${trustedWorkflowRef} resolves to ${remoteTagSha}, expected ${workflowSha}`,
);
}
}
function resolveTrustedWorkflowSha(requestedSha: string, trustedWorkflowRef: string) {
if (trustedWorkflowRef === "main") {
run("git", ["fetch", "--no-tags", "origin", "refs/heads/main:refs/remotes/origin/main"], {
stdio: "inherit",
});
}
const workflowSha = resolveSha(requestedSha || "origin/main");
verifyTrustedWorkflowRef(workflowSha, trustedWorkflowRef);
return workflowSha;
}
function requireDispatch(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function exactKeys(value: unknown, keys: string[]): value is Record<string, unknown> {
return isJsonRecord(value) && isDeepStrictEqual(Object.keys(value).toSorted(), keys.toSorted());
}
export function dispatchInputsDigest(inputs: DispatchInputs): string {
const wireInputs = Object.fromEntries(
Object.keys(inputs)
.filter((key) => String(inputs[key]) !== "")
.toSorted()
.map((key) => [key, String(inputs[key])]),
);
return `sha256:${createHash("sha256").update(JSON.stringify(wireInputs)).digest("hex")}`;
}
function validateDispatchRecord(value: unknown): asserts value is DispatchRecord {
requireDispatch(
exactKeys(value, ["kind", "request", "phase", "refs", "error", "run"]) &&
value.kind === REQUEST_KIND &&
["prepared", "attempted", "observed", "rejected"].includes(stringValue(value.phase)) &&
["none", "transport", "unclassified", "http-rejection"].includes(stringValue(value.error)),
"Invalid retained dispatch record",
);
const request = value.request;
requireDispatch(
exactKeys(request, [
"id",
"host",
"repository",
"workflowId",
"workflowPath",
"event",
"workflowSha",
"trustedWorkflowRef",
"targetSha",
"targetVersion",
"targetContextRef",
"workflowRef",
"wireInputs",
"inputs",
"effectiveSoak",
]) &&
typeof request.id === "string" &&
/^[a-f0-9-]{36}$/u.test(request.id) &&
request.host === "github.com" &&
request.repository === REPOSITORY &&
request.workflowPath === TRUSTED_WORKFLOW_PATH &&
request.event === "workflow_dispatch" &&
Number.isSafeInteger(request.workflowId) &&
Number(request.workflowId) > 0 &&
typeof request.workflowSha === "string" &&
SHA_PATTERN.test(request.workflowSha) &&
typeof request.targetSha === "string" &&
SHA_PATTERN.test(request.targetSha) &&
typeof request.targetVersion === "string" &&
/^[0-9]{4}\.[0-9]+\.[0-9]+(?:-.+)?$/u.test(request.targetVersion) &&
typeof request.targetContextRef === "string" &&
typeof request.trustedWorkflowRef === "string" &&
(request.trustedWorkflowRef === "main" ||
TRUSTED_WORKFLOW_TAG_PATTERN.test(request.trustedWorkflowRef)) &&
typeof request.workflowRef === "string" &&
new RegExp(`^release-ci/${request.workflowSha.slice(0, 12)}-[0-9]+$`, "u").test(
request.workflowRef,
) &&
isJsonRecord(request.wireInputs) &&
isJsonRecord(request.inputs) &&
isDeepStrictEqual(
Object.keys(request.wireInputs).toSorted(),
Object.keys(request.inputs).toSorted(),
),
"Invalid retained dispatch request identity",
);
for (const [key, input] of Object.entries(request.inputs)) {
requireDispatch(
/^[a-z][a-z0-9_]*$/u.test(key) &&
(typeof input === "string" ||
typeof input === "boolean" ||
(typeof input === "number" && Number.isFinite(input))) &&
request.wireInputs[key] === String(input),
"Invalid retained dispatch inputs",
);
}
requireDispatch(
request.inputs.ref === request.targetSha &&
request.inputs.expected_sha === request.targetSha &&
(request.targetContextRef === request.targetSha
? !request.inputs.target_context_ref
: request.inputs.target_context_ref === request.targetContextRef) &&
request.effectiveSoak ===
(request.inputs.run_release_soak === true ||
request.inputs.release_profile === "stable" ||
request.inputs.release_profile === "full"),
"Retained dispatch selection does not match its identity",
);
if (request.inputs.trusted_workflow_json) {
requireDispatch(
typeof request.inputs.trusted_workflow_json === "string",
"Invalid retained tooling input",
);
const supplied = JSON.parse(request.inputs.trusted_workflow_json);
const enveloped = isJsonRecord(supplied) && Object.hasOwn(supplied, "trustedWorkflow");
const identity = enveloped
? decodePublicationDispatchEnvelope(request.inputs.trusted_workflow_json).trustedWorkflow
: supplied;
requireDispatch(
!enveloped ||
(!Object.hasOwn(request.inputs, "validation_purpose") &&
!Object.hasOwn(request.inputs, "publication_selection_json") &&
!Object.hasOwn(request.inputs, "extension_test_exclude_patterns_json")),
"Retained dispatch contains conflicting source intent representations",
);
requireDispatch(
typeof request.inputs.trusted_workflow_json === "string" &&
isDeepStrictEqual(identity, {
fullRef:
request.trustedWorkflowRef === "main"
? "refs/heads/main"
: `refs/tags/${request.trustedWorkflowRef}`,
ref: request.trustedWorkflowRef,
sha: request.workflowSha,
}),
"Retained trusted workflow identity changed",
);
}
requireDispatch(
exactKeys(value.refs, ["workflow"]) &&
Object.values(value.refs).every((state) =>
["intended", "uncertain", "created"].includes(stringValue(state)),
) &&
(value.run === null ||
(exactKeys(value.run, ["id", "attempt"]) &&
Number.isSafeInteger(value.run.id) &&
Number(value.run.id) > 0 &&
Number.isSafeInteger(value.run.attempt) &&
Number(value.run.attempt) > 0)) &&
(value.phase === "observed" ? value.run !== null : value.run === null) &&
(value.phase !== "rejected" || value.error === "http-rejection"),
"Invalid retained dispatch outcome",
);
}
function assertRequestPath(path: string) {
let current = resolve(path);
while (true) {
try {
const info = lstatSync(current);
requireDispatch(!info.isSymbolicLink(), "Request path must not contain symlinks");
if (current === resolve(path)) {
requireDispatch(
info.isFile() && (info.mode & 0o077) === 0,
"Request must be a private regular file",
);
} else {
requireDispatch(info.isDirectory(), "Request parent must be a directory");
}
} catch (error) {
if (!isJsonRecord(error) || error.code !== "ENOENT") {
throw error;
}
}
const parent = dirname(current);
if (parent === current) {
break;
}
current = parent;
}
}
function readDispatchRecord(path: string): DispatchRecord {
assertRequestPath(path);
const bytes = readBoundedRegularFile(path, {
maxBytes: MAX_REQUEST_BYTES,
label: "Retained dispatch request",
});
const value: unknown = JSON.parse(bytes.toString("utf8"));
requireDispatch(
bytes.equals(Buffer.from(`${JSON.stringify(value)}\n`)),
"Retained request is not complete canonical JSON",
);
validateDispatchRecord(value);
return value;
}
function retainDispatchRecord(path: string, record: DispatchRecord, previous?: DispatchRecord) {
validateDispatchRecord(record);
const bytes = `${JSON.stringify(record)}\n`;
requireDispatch(
Buffer.byteLength(bytes) <= MAX_REQUEST_BYTES,
"Dispatch request exceeds its byte limit",
);
assertRequestPath(path);
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
assertRequestPath(path);
const temporary = `${path}.${randomUUID()}.tmp`;
const descriptor = openSync(temporary, "wx", 0o600);
try {
try {
writeFileSync(descriptor, bytes);
fsyncSync(descriptor);
} finally {
closeSync(descriptor);
}
if (previous) {
requireDispatch(
isDeepStrictEqual(readDispatchRecord(path), previous),
"Retained request changed during dispatch",
);
renameSync(temporary, path);
} else {
// A fully written exclusive claim prevents a second caller from issuing the POST.
linkSync(temporary, path);
unlinkSync(temporary);
}
const directory = openSync(dirname(path), "r");
try {
fsyncSync(directory);
} finally {
closeSync(directory);
}
} finally {
rmSync(temporary, { force: true });
}
}
function resolveDispatchSelection(workflowSha: string, overrides: Record<string, string>) {
const workflow: unknown = parseYaml(
run("git", ["show", `${workflowSha}:${TRUSTED_WORKFLOW_PATH}`]),
);
requireDispatch(
isJsonRecord(workflow) &&
isJsonRecord(workflow.env) &&
workflow.env.FULL_RELEASE_DISPATCH_WITNESS_CONTRACT === "1",
`Tooling SHA ${workflowSha} does not support FULL_RELEASE_DISPATCH_WITNESS_CONTRACT=1; no remote refs or run were created. Keep the frozen Tooling SHA. Existing runs use frv status; a new request needs separately approved witness-capable tooling.`,
);
requireDispatch(
workflow.env.FULL_RELEASE_SOURCE_ADMISSION_CONTRACT === "1",
`Tooling SHA ${workflowSha} does not support source admission; no remote refs or run were created. Keep the frozen tooling SHA. Reopen existing requests read-only; new tooling requires separate approval.`,
);
requireDispatch(
isJsonRecord(workflow.on) &&
isJsonRecord(workflow.on.workflow_dispatch) &&
isJsonRecord(workflow.on.workflow_dispatch.inputs),
"Pinned workflow input schema is invalid",
);
const definitions = workflow.on.workflow_dispatch.inputs;
requireDispatch(
Object.keys(definitions).length <= 25,
"Pinned workflow exceeds 25 dispatch inputs",
);
const {
validation_purpose,
publication_selection_json,
extension_test_exclude_patterns_json,
known_flaky_jobs_json,
...wireOverrides
} = overrides;
const laneInputs =
extension_test_exclude_patterns_json === undefined
? undefined
: { extension_test_exclude_patterns_json };
requireDispatch(
laneInputs === undefined || workflow.env.FULL_RELEASE_LANE_INPUTS_CONTRACT === "1",
`Tooling SHA ${workflowSha} does not support packed lane inputs; no remote refs or run were created. Keep the frozen Tooling SHA.`,
);
requireDispatch(
known_flaky_jobs_json === undefined,
"Automatic test retries are disabled; remove known_flaky_jobs_json and diagnose the failed job.",
);
const intent = normalizePublicationIntent(validation_purpose, publication_selection_json);
requireDispatch(
intent.validationPurpose !== "publish" ||
workflow.env.FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT === "1",
`Tooling SHA ${workflowSha} does not support registry admission for fresh publish requests; no remote refs or run were created. Keep the frozen tooling SHA. Reopen existing requests read-only; new tooling requires separate approval.`,
);
wireOverrides.trusted_workflow_json = publicationDispatchEnvelope(
JSON.parse(overrides.trusted_workflow_json || "null"),
intent,
laneInputs,
);
requireDispatch(
Object.keys(wireOverrides).every((key) => Object.hasOwn(definitions, key)),
"Undeclared workflow input",
);
const inputs: DispatchInputs = {};
const wireInputs: Record<string, string> = {};
for (const [key, definition] of Object.entries(definitions)) {
requireDispatch(
/^[a-z][a-z0-9_]*$/u.test(key) && isJsonRecord(definition),
"Invalid workflow input definition",
);
const raw: unknown =
wireOverrides[key] ?? definition.default ?? (definition.type === "boolean" ? false : "");
const text = String(raw);
let value: string | number | boolean = text;
if (definition.type === "boolean") {
requireDispatch(["true", "false"].includes(text), `Input ${key} must be true or false`);
value = text === "true";
} else if (definition.type === "number") {
requireDispatch(
text.trim() !== "" && Number.isFinite(Number(text)),
`Input ${key} must be a number`,
);
value = Number(text);
} else {
requireDispatch(
["string", "choice", "environment"].includes(stringValue(definition.type)),
`Unsupported input type for ${key}`,
);
if (definition.type === "choice") {
requireDispatch(
Array.isArray(definition.options) && definition.options.includes(text),
`Invalid choice for ${key}`,
);
}
}
inputs[key] = value;
wireInputs[key] = String(value);
}
decodePublicationDispatchEnvelope(inputs.trusted_workflow_json);
return {
inputs,
wireInputs,
effectiveSoak:
inputs.run_release_soak === true ||
inputs.release_profile === "stable" ||
inputs.release_profile === "full",
};
}
function parseGhHttpResponse(output: string) {
const match = /^HTTP\/[\d.]+ (\d{3})[^\r\n]*\r?\n([\s\S]*?)\r?\n\r?\n([\s\S]*)$/u.exec(output);
requireDispatch(match, "GitHub response did not include complete HTTP headers");
const headers = new Headers();
for (const line of match[2]!.split(/\r?\n/u)) {
if (!line) {
continue;
}
const separator = line.indexOf(":");
requireDispatch(separator > 0, "GitHub response contains malformed headers");
headers.append(line.slice(0, separator), line.slice(separator + 1).trim());
}
return { status: Number(match[1]), headers, body: match[3]! };
}
function readDispatchRuns(request: DispatchRequest) {
const runs: Record<string, unknown>[] = [];
let total: number | undefined;
for (let page = 1; page <= MAX_RUN_PAGES; page += 1) {
const response = parseGhHttpResponse(
readGhApi(`repos/${REPOSITORY}/actions/workflows/${request.workflowId}/runs`, [
"--include",
"-f",
`branch=${request.workflowRef}`,
"-f",
"event=workflow_dispatch",
"-f",
`per_page=${RUN_PAGE_SIZE}`,
"-f",
`page=${page}`,
]),
);
requireDispatch(
response.status === 200,
"Dispatch run inventory returned a non-success response",
);
const value: unknown = JSON.parse(response.body);
requireDispatch(
isJsonRecord(value) &&
Array.isArray(value.workflow_runs) &&
Number.isSafeInteger(value.total_count) &&
Number(value.total_count) >= 0 &&
Number(value.total_count) <= RUN_PAGE_SIZE * MAX_RUN_PAGES,
"Dispatch run inventory is incomplete or exceeds its bound",
);
total ??= Number(value.total_count);
requireDispatch(
value.total_count === total &&
value.workflow_runs.length === Math.min(RUN_PAGE_SIZE, total - runs.length),
"Dispatch run pagination changed or is incomplete",
);
for (const item of value.workflow_runs) {
requireDispatch(
isJsonRecord(item) &&
Number.isSafeInteger(item.id) &&
Number(item.id) > 0 &&
!runs.some((other) => other.id === item.id),
"Dispatch run inventory contains invalid or repeated IDs",
);
runs.push(item);
}
const next = response.headers.get("link")?.match(/<([^>]+)>;\s*rel="next"/u)?.[1];
if (runs.length === total) {
requireDispatch(!next, "Dispatch run pagination is uncertain");
return runs;
}
requireDispatch(next, "Dispatch run inventory omitted its next page");
const url = new URL(next);
requireDispatch(
url.origin === "https://api.github.com" &&
url.pathname === `/repos/${REPOSITORY}/actions/workflows/${request.workflowId}/runs` &&
url.searchParams.get("page") === String(page + 1) &&
url.searchParams.get("branch") === request.workflowRef &&
url.searchParams.get("event") === "workflow_dispatch" &&
url.searchParams.get("per_page") === String(RUN_PAGE_SIZE),
"Dispatch run pagination changed scope",
);
}
throw new Error("Dispatch run inventory exceeded its page bound");
}
function assertDispatchRun(workflowRun: unknown, request: DispatchRequest, expected: DispatchRun) {
requireDispatch(
isJsonRecord(workflowRun) &&
workflowRun.id === expected.id &&
workflowRun.run_attempt === expected.attempt &&
workflowRun.workflow_id === request.workflowId &&
workflowRun.head_sha === request.workflowSha &&
workflowRun.head_branch === request.workflowRef &&
workflowRun.event === request.event &&
[
request.workflowPath,
`${request.workflowPath}@${request.workflowRef}`,
`${request.workflowPath}@refs/heads/${request.workflowRef}`,
].includes(stringValue(workflowRun.path)) &&
isJsonRecord(workflowRun.repository) &&
workflowRun.repository.full_name === request.repository &&
isJsonRecord(workflowRun.head_repository) &&
workflowRun.head_repository.full_name === request.repository &&
workflowRun.display_title === "Full Release Validation" &&
workflowRun.html_url === `https://github.com/${REPOSITORY}/actions/runs/${expected.id}`,
"Dispatch run does not match the exact retained workflow/ref/event/attempt identity",
);
}
async function readDispatchWitness(request: DispatchRequest, observed: DispatchRun) {
const name = `full-release-dispatch-inputs-${observed.id}-${observed.attempt}`;
const inventory: unknown = JSON.parse(
readGhApi(
`repos/${REPOSITORY}/actions/runs/${observed.id}/artifacts`,
["-f", `name=${name}`, "-f", "per_page=100"],
{ ...GH_READ_OPTIONS, maxBuffer: MAX_REQUEST_BYTES },
),
);
requireDispatch(
isJsonRecord(inventory) &&
Array.isArray(inventory.artifacts) &&
Number.isSafeInteger(inventory.total_count) &&
inventory.total_count === inventory.artifacts.length &&
inventory.total_count <= 100,
"Dispatch witness inventory is incomplete",
);
if (inventory.artifacts.length === 0) {
return false;
}
requireDispatch(inventory.artifacts.length === 1, "Dispatch witness is ambiguous");
const metadata: unknown = inventory.artifacts[0];
requireDispatch(
isJsonRecord(metadata) &&
typeof metadata.id === "number" &&
Number.isSafeInteger(metadata.id) &&
metadata.id > 0 &&
metadata.name === name &&
typeof metadata.size_in_bytes === "number" &&
Number.isSafeInteger(metadata.size_in_bytes) &&
metadata.size_in_bytes > 0 &&
metadata.size_in_bytes <= MAX_WITNESS_ARCHIVE_BYTES &&
typeof metadata.digest === "string" &&
/^sha256:[a-f0-9]{64}$/u.test(metadata.digest) &&
metadata.expired === false &&
typeof metadata.expires_at === "string" &&
Date.parse(metadata.expires_at) > Date.now() &&
isJsonRecord(metadata.workflow_run) &&
metadata.workflow_run.id === observed.id &&
metadata.workflow_run.head_sha === request.workflowSha,
"Dispatch witness metadata does not match its exact run",
);
const artifactEndpoint = `repos/${REPOSITORY}/actions/artifacts/${metadata.id}`;
const exactMetadata: unknown = JSON.parse(
readGhApi(artifactEndpoint, [], { ...GH_READ_OPTIONS, maxBuffer: MAX_REQUEST_BYTES }),
);
requireDispatch(
isJsonRecord(exactMetadata) &&
exactMetadata.id === metadata.id &&
exactMetadata.name === name &&
exactMetadata.size_in_bytes === metadata.size_in_bytes &&
exactMetadata.digest === metadata.digest &&
exactMetadata.expired === false &&
exactMetadata.expires_at === metadata.expires_at &&
Date.parse(metadata.expires_at) > Date.now() &&
isJsonRecord(exactMetadata.workflow_run) &&
exactMetadata.workflow_run.id === observed.id &&
exactMetadata.workflow_run.head_sha === request.workflowSha,
"Dispatch witness metadata changed from its exact artifact tuple",
);
// Keep credentials and redirects owned by the selected CLI; ZIP bytes must not be decoded.
const archiveArgs = [
"api",
"--method",
"GET",
`${artifactEndpoint}/zip`,
"--hostname",
"github.com",
"-H",
GH_NO_CACHE_HEADER,
];
const archiveOptions = {
...GH_READ_OPTIONS,
encoding: null,
maxBuffer: MAX_WITNESS_ARCHIVE_BYTES,
stdio: ["ignore", "pipe", "pipe"],
} satisfies ExecFileSyncOptionsWithBufferEncoding;
let archiveBytes: Uint8Array<ArrayBuffer>;
try {
archiveBytes = execPlainGh([...archiveArgs, "--allow-escape-sequences"], archiveOptions);
} catch (error) {
if (!isUnsupportedAllowEscapeSequencesFlag(error)) {
throw error;
}
archiveBytes = execPlainGh(archiveArgs, archiveOptions);
}
requireDispatch(
archiveBytes.byteLength === metadata.size_in_bytes &&
`sha256:${createHash("sha256").update(archiveBytes).digest("hex")}` === metadata.digest,
"Dispatch witness archive does not match its exact size and digest",
);
const files = inspectActionsArtifactZipWithPolicy(archiveBytes, {
expectedEntries: [WITNESS_FILE],
maxArchiveBytes: MAX_WITNESS_ARCHIVE_BYTES,
maxCompressedEntryBytes: () => MAX_WITNESS_ARCHIVE_BYTES,
maxEntryBytes: () => MAX_REQUEST_BYTES,
maxExpandedBytes: MAX_REQUEST_BYTES,
});
const witness: unknown = JSON.parse(files.get(WITNESS_FILE).toString("utf8"));
requireDispatch(
isDeepStrictEqual(witness, {
kind: WITNESS_KIND,
serverUrl: "https://github.com",
repository: REPOSITORY,
workflowRef: `${REPOSITORY}/${request.workflowPath}@refs/heads/${request.workflowRef}`,
event: request.event,
ref: `refs/heads/${request.workflowRef}`,
sha: request.workflowSha,
runId: String(observed.id),
runAttempt: String(observed.attempt),
inputsDigest: dispatchInputsDigest(request.wireInputs),
}),
"Dispatch input witness does not match the complete retained request",
);
return true;
}
async function reconcileDispatch(record: DispatchRecord): Promise<DispatchRun> {
requireDispatch(
record.phase !== "prepared",
"No attempted workflow POST was retained; dispatch remains unknown",
);
requireDispatch(
record.phase !== "rejected",
"dispatch=rejected: GitHub rejected the retained request",
);
const request = record.request;
const witnessDeadline = Date.now() + FULL_RELEASE_WITNESS_QUEUE_WAIT_MS;
for (let attempt = 0; ; attempt += 1) {
const runs = readDispatchRuns(request);
let queuedRun = "";
if (runs.length > 0) {
requireDispatch(
runs.length === 1,
"Multiple dispatch runs exist for the retained transport; adoption is ambiguous",
);
const observed = record.run ?? { id: Number(runs[0]!.id), attempt: 1 };
assertDispatchRun(runs[0], request, observed);
const current: unknown = JSON.parse(
readGhApi(`repos/${REPOSITORY}/actions/runs/${observed.id}`),
);
assertDispatchRun(current, request, observed);
if (isJsonRecord(current) && ACTIVE_RUN_STATUSES.has(stringValue(current.status))) {
queuedRun = `${observed.id} (${stringValue(current.status)})`;
}
if (await readDispatchWitness(request, observed)) {
// Recheck both identity and inventory after the archive read, which can span a rerun.
assertDispatchRun(
JSON.parse(readGhApi(`repos/${REPOSITORY}/actions/runs/${observed.id}`)),
request,
observed,
);
const after = readDispatchRuns(request);
requireDispatch(after.length === 1, "Dispatch run inventory changed during reconciliation");
assertDispatchRun(after[0], request, observed);
return observed;
}
}
let delay = FULL_RELEASE_RUN_DISCOVERY_DELAYS_MS[attempt];
if (delay === undefined && queuedRun && Date.now() < witnessDeadline) {
// The exact run exists but has not reached the job that uploads its input witness.
console.warn(`dispatch=pending-witness: run ${queuedRun} has not uploaded its witness yet`);
delay = Math.min(FULL_RELEASE_GITHUB_POLL_INTERVAL_MS, witnessDeadline - Date.now());
}
if (delay === undefined) {
break;
}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
}
throw new Error("Could not determine Full Release Validation run id: discovery exhausted");
}
async function reopenDispatch(path: string, args: ReturnType<typeof parseArgs>, argv: string[]) {
const record = readDispatchRecord(path);
const request = record.request;
let retainedInputs = request.wireInputs;
let retainedIntent: ReturnType<typeof publicationIntentInputs> | undefined;
const rawIdentity = request.wireInputs.trusted_workflow_json;
if (rawIdentity && Object.hasOwn(JSON.parse(rawIdentity), "trustedWorkflow")) {
const envelope = decodePublicationDispatchEnvelope(rawIdentity);
retainedIntent = publicationIntentInputs(envelope);
retainedInputs = {
...retainedInputs,
...envelope.laneInputs,
validation_purpose: retainedIntent.validationPurpose,
publication_selection_json: retainedIntent.publicationSelectionJson,
};
}
requireDispatch(
(!args.sha || args.sha === request.targetSha) &&
(!args.workflowSha || args.workflowSha === request.workflowSha) &&
(!args.targetRef || args.targetRef === request.targetContextRef) &&
(!argv.includes("--trusted-workflow-ref") ||
args.trustedWorkflowRef === request.trustedWorkflowRef) &&
args.specifiedInputs.every((key) =>
key === "publication_selection_json" && retainedIntent
? publicationIntentInputs(
normalizePublicationIntent(retainedIntent.validationPurpose, args.inputs[key]),
).publicationSelectionJson === retainedIntent.publicationSelectionJson
: key === "extension_test_exclude_patterns_json"
? normalizePublicationLaneInputs({ [key]: args.inputs[key] })[key] ===
retainedInputs[key]
: args.inputs[key] === retainedInputs[key],
),
"Reopen arguments conflict with the retained request",
);
try {
const observed = await reconcileDispatch(record);
console.log(
`dispatch=observed: https://github.com/${REPOSITORY}/actions/runs/${observed.id} attempt=${observed.attempt}`,
);
} catch (error) {
console.error(
`dispatch=${record.phase === "rejected" ? "rejected" : "unknown"} error=${record.error}`,
);
console.error(`Retained workflow ref: refs/heads/${request.workflowRef}`);
throw error;
}
}
function readWorkflowRun(parentRunId: string, workflowSha: string) {
if (!/^[1-9][0-9]*$/u.test(parentRunId)) {
throw new Error("parent run ID must be a positive decimal");
}
const workflowRun: unknown = JSON.parse(
readGhApi(`repos/${REPOSITORY}/actions/runs/${parentRunId}`, [], GH_READ_OPTIONS),
);
if (!isJsonRecord(workflowRun)) {
throw new Error(`Full Release Validation run ${parentRunId} returned an invalid response`);
}
if (workflowRun.head_sha !== workflowSha) {
throw new Error(
`Full Release Validation run ${parentRunId} head ${displayValue(workflowRun.head_sha)} does not match trusted workflow SHA ${workflowSha}`,
);
}
return workflowRun;
}
function readActiveParentJobs(parentRunId: string) {
const response: unknown = JSON.parse(
readGhApi(
`repos/${REPOSITORY}/actions/runs/${parentRunId}/jobs`,
["-f", "per_page=100"],
GH_READ_OPTIONS,
),
);
if (!isJsonRecord(response) || !Array.isArray(response.jobs)) {
throw new Error(`Full Release Validation run ${parentRunId} returned invalid jobs`);
}
return response.jobs
.filter((job) => isJsonRecord(job) && job.status !== "completed")
.map((job) => ({
name: isJsonRecord(job) ? stringValue(job.name, "<unnamed>") : "<unnamed>",
status: isJsonRecord(job) ? stringValue(job.status, "pending") : "pending",
url: isJsonRecord(job) ? stringValue(job.html_url) : "",
}));
}
export function validateReleaseDecisionPayload(
payload: unknown,
expected: {
parentRunAttempt: number;
parentRunId: string;
workflowSha: string;
},
) {
return validateReleaseStateArtifact(
payload,
{
parentRunAttempt: expected.parentRunAttempt,
parentRunId: expected.parentRunId,
workflowSha: expected.workflowSha,
},
"decision",
);
}
export function releaseDecisionStopsForeground(state: unknown) {
return [
"blocked_diagnostics_running",
"blocked_complete",
"orchestration_error",
"cancelled_with_children",
].includes(stringValue(state));
}
export function tryReadReleaseDecision(
parentRunId: string,
parentRunAttempt: number,
workflowSha: string,
runStatusImpl: (command: string, args: string[], options?: CommandOptions) => CommandStatus = (
_command,
args,
options,
) => runGhStatus(args, options),
) {
const artifactName = `full-release-decision-${parentRunId}-${parentRunAttempt}`;
const downloadDir = mkdtempSync(join(tmpdir(), "openclaw-release-decision-"));
try {
const result = runStatusImpl(
"gh",
[
"run",
"download",
parentRunId,
"--repo",
REPOSITORY,
"--name",
artifactName,
"--dir",
downloadDir,
],
{ stdio: ["ignore", "ignore", "pipe"], timeoutMs: GH_READ_TIMEOUT_MS },
);
if (result.status !== 0) {
const stderr = stringValue(result.stderr);
if (isReleaseGhArtifactMissingError({ cause: result.error, stderr })) {
return undefined;
}
const downloadError = Object.assign(
result.error instanceof Error
? result.error
: new Error(
`Release Decision artifact download failed${
stderr.trim() ? `: ${stderr.trim().slice(0, 500)}` : ""
}`,
),
{
signal: result.signal,
status: result.status,
stderr,
},
);
if (classifyReleaseGhTransportError(downloadError) === "transient") {
console.warn(
`Release Decision artifact unavailable this poll; retrying: ${downloadError.message}`,
);
return undefined;
}
throw new Error(
`Release Decision artifact download failed${
stderr.trim() ? `: ${stderr.trim().slice(0, 500)}` : ""
}`,
{ cause: downloadError },
);
}
const decisionPath = join(downloadDir, RELEASE_DECISION_FILE);
if (!existsSync(decisionPath)) {
throw new Error(
`Release Decision artifact ${artifactName} omitted ${RELEASE_DECISION_FILE}.`,
);
}
if (statSync(decisionPath).size > MAX_RELEASE_ARTIFACT_BYTES) {
throw new Error(`Release Decision artifact ${artifactName} exceeds the size limit.`);
}
return validateReleaseDecisionPayload(JSON.parse(readFileSync(decisionPath, "utf8")), {
parentRunAttempt,
parentRunId,
workflowSha,
});
} finally {
rmSync(downloadDir, { force: true, recursive: true });
}
}
function releaseDecisionAvailable(parentRunId: string, parentRunAttempt: number) {
const artifactName = `full-release-decision-${parentRunId}-${parentRunAttempt}`;
try {
const response: unknown = JSON.parse(
readGhApi(
`repos/${REPOSITORY}/actions/runs/${parentRunId}/artifacts`,
["-f", "per_page=100", "-f", `name=${artifactName}`],
{ ...GH_READ_OPTIONS, stdio: ["ignore", "pipe", "pipe"] },
),
);
if (!isJsonRecord(response) || !Array.isArray(response.artifacts)) {
throw new Error(`Full Release Validation run ${parentRunId} returned invalid artifacts`);
}
return response.artifacts.some(
(artifact) =>
isJsonRecord(artifact) && artifact.name === artifactName && artifact.expired === false,
);
} catch (error) {
if (classifyReleaseGhTransportError(error) !== "transient") {
throw error;
}
console.warn(`Release Decision metadata unavailable this poll; retrying: ${String(error)}`);
return false;
}
}
function waitForWorkflowRun(parentRunId: string, workflowSha: string, record?: DispatchRecord) {
let lastSummary = "";
let consecutiveErrors = 0;
const startedAt = Date.now();
const deadline = startedAt + FULL_RELEASE_WAIT_TIMEOUT_MINUTES * 60_000;
let nextProgressAt = startedAt + FULL_RELEASE_PROGRESS_INTERVAL_MS;
let decision: { attempt: number; state: "unavailable" | "ready" | "passed" } | undefined;
while (Date.now() < deadline) {
let suite: Record<string, unknown> | undefined;
try {
suite = readWorkflowRun(parentRunId, workflowSha);
consecutiveErrors = 0;
} catch (error) {
consecutiveErrors += 1;
if (consecutiveErrors >= 3) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
console.warn(`Parent run status query failed; retrying: ${message}`);
}
const status = stringValue(suite?.status, "pending").toLowerCase();
const conclusion = stringValue(suite?.conclusion, "pending").toLowerCase();
const summary = `${status}/${conclusion}`;
if (summary !== lastSummary) {
console.log(`Parent run status: ${summary}`);
lastSummary = summary;
}
if (suite) {
if (record?.run) {
assertDispatchRun(suite, record.request, record.run);
}
const attempt = requiredPositiveInteger(suite.run_attempt, "parent run attempt");
if (decision?.attempt !== attempt) {
decision = { attempt, state: "unavailable" };
}
// Metadata is only a readiness hint. Once advertised, keep trying the
// authoritative download across status regressions until this attempt validates.
if (
decision.state === "unavailable" &&
(suite.status === "completed" || releaseDecisionAvailable(parentRunId, attempt))
) {
decision.state = "ready";
}
if (decision.state === "ready") {
const releaseDecision = tryReadReleaseDecision(parentRunId, attempt, workflowSha);
if (releaseDecision && releaseDecisionStopsForeground(releaseDecision.state)) {
throw new Error(
`${formatReleaseStateOutcome(releaseDecision)}\nhttps://github.com/openclaw/openclaw/actions/runs/${parentRunId}`,
);
}
// The workflow uploads one immutable decision per attempt; final success
// still requires the parent's terminal conclusion and strict evidence verifier.
if (releaseDecision?.state === "passed") {
decision.state = "passed";
}
}
}
if (suite?.status === "completed" && stringValue(suite.conclusion)) {
if (suite.conclusion === "success") {
return suite;
}
throw new Error(
`Full Release Validation concluded ${stringValue(suite.conclusion, "unknown").toLowerCase()}: https://github.com/openclaw/openclaw/actions/runs/${parentRunId}`,
);
}
const now = Date.now();
if (now >= nextProgressAt) {
const elapsedMinutes = Math.floor((now - startedAt) / 60_000);
try {
const activeJobs = readActiveParentJobs(parentRunId);
console.log(
`Parent run progress after ${elapsedMinutes}m: ${activeJobs.length} active job(s)`,
);
for (const job of activeJobs) {
console.log(`- ${job.name}: ${job.status}${job.url ? ` ${job.url}` : ""}`);
}
} catch (error) {
console.warn(
`Parent run progress query failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
nextProgressAt = now + FULL_RELEASE_PROGRESS_INTERVAL_MS;
}
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
break;
}
Atomics.wait(
new Int32Array(new SharedArrayBuffer(4)),
0,
0,
Math.min(FULL_RELEASE_GITHUB_POLL_INTERVAL_MS, remainingMs),
);
}
throw new Error(
`Timed out after ${FULL_RELEASE_WAIT_TIMEOUT_MINUTES} minutes waiting for Full Release Validation: https://github.com/openclaw/openclaw/actions/runs/${parentRunId}`,
);
}
export function releaseEvidenceVerificationArgs(
parentRunId: unknown,
verifierSourceSha: string,
verifierSourceFile: string,
trustedWorkflowRef = "main",
) {
if (!/^[1-9][0-9]*$/u.test(String(parentRunId))) {
throw new Error("parent run ID must be a positive decimal");
}
const trustedWorkflowFullRef =
trustedWorkflowRef === "main"
? "refs/heads/main"
: TRUSTED_WORKFLOW_TAG_PATTERN.test(trustedWorkflowRef)
? `refs/tags/${trustedWorkflowRef}`
: "";
if (!trustedWorkflowFullRef) {
throw new Error("trusted workflow ref must be main or a protected release-publish tag");
}
return [
"--validate-run",
String(parentRunId),
"--trusted-workflow-ref",
trustedWorkflowRef,
"--trusted-workflow-full-ref",
trustedWorkflowFullRef,
"--trusted-workflow-sha",
verifierSourceSha,
"--json",
"--verifier-source-sha",
verifierSourceSha,
"--verifier-source-file",
verifierSourceFile,
];
}
export function shouldDeleteTemporaryWorkflowRef(params: TemporaryRefParams) {
return (
!params.keepBranch &&
(params.dryRun || (params.parentConclusion === "success" && params.evidenceVerified))
);
}
export function assertTrustedWorkflowHarness(
workflowSha: string,
pathExists: (relativePath: string) => boolean = (relativePath) =>
runStatus("git", ["cat-file", "-e", `${workflowSha}:${relativePath}`], {
stdio: ["ignore", "ignore", "ignore"],
}).status === 0,
readPath: (relativePath: string) => string = (relativePath) =>
run("git", ["show", `${workflowSha}:${relativePath}`]),
): TrustedWorkflowHarness {
if (!pathExists(TRUSTED_WORKFLOW_PATH)) {
throw new Error(
`trusted workflow SHA ${workflowSha} does not contain ${TRUSTED_WORKFLOW_PATH}`,
);
}
let workflow: unknown;
try {
workflow = parseYaml(readPath(TRUSTED_WORKFLOW_PATH));
} catch (error) {
throw new Error(
`Tooling SHA ${workflowSha} contains invalid ${TRUSTED_WORKFLOW_PATH}: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
const contract =
isJsonRecord(workflow) && isJsonRecord(workflow.env)
? workflow.env[RELEASE_ISOLATION_TOOLING_CONTRACT_ENV]
: undefined;
if (contract !== "1" && contract !== RELEASE_ISOLATION_TOOLING_CONTRACT) {
throw new Error(
`Tooling SHA ${workflowSha} does not declare a supported ${RELEASE_ISOLATION_TOOLING_CONTRACT_ENV} in ${TRUSTED_WORKFLOW_PATH}`,
);
}
const workflowInputs =
isJsonRecord(workflow) &&
isJsonRecord(workflow.on) &&
isJsonRecord(workflow.on.workflow_dispatch) &&
isJsonRecord(workflow.on.workflow_dispatch.inputs)
? workflow.on.workflow_dispatch.inputs
: undefined;
if (!workflowInputs || !Object.hasOwn(workflowInputs, "expected_sha")) {
throw new Error(
`Tooling SHA ${workflowSha} is missing workflow_dispatch input expected_sha in ${TRUSTED_WORKFLOW_PATH}`,
);
}
if (
contract === RELEASE_ISOLATION_TOOLING_CONTRACT &&
!Object.hasOwn(workflowInputs, "trusted_workflow_json")
) {
throw new Error(
`Tooling SHA ${workflowSha} declares ${RELEASE_ISOLATION_TOOLING_CONTRACT_ENV}=2 but is missing workflow_dispatch input trusted_workflow_json in ${TRUSTED_WORKFLOW_PATH}`,
);
}
const verifierPath = RELEASE_EVIDENCE_VERIFIER_PATHS.find((relativePath) =>
pathExists(relativePath),
);
if (!verifierPath) {
throw new Error(
`trusted workflow SHA ${workflowSha} does not contain a supported release evidence verifier`,
);
}
return { contract, verifierPath };
}
export function releaseEvidenceVerifierPath(worktreeRoot: string) {
const candidates = RELEASE_EVIDENCE_VERIFIER_PATHS.map((relativePath) =>
join(worktreeRoot, relativePath),
);
const verifier = candidates.find((candidate) => existsSync(candidate));
if (!verifier) {
throw new Error("trusted workflow checkout does not contain a release evidence verifier");
}
return verifier;
}
function verifyReleaseEvidence(
parentRunId: string,
workflowSha: string,
trustedWorkflowRef: string,
) {
const verifierWorktree = mkdtempSync(join(tmpdir(), "openclaw-release-verifier-"));
try {
run("git", ["worktree", "add", "--detach", verifierWorktree, workflowSha], {
stdio: ["ignore", "ignore", "inherit"],
});
const verifier = releaseEvidenceVerifierPath(verifierWorktree);
const evidence: unknown = JSON.parse(
run(process.execPath, [
verifier,
...releaseEvidenceVerificationArgs(parentRunId, workflowSha, verifier, trustedWorkflowRef),
]),
);
if (
!isJsonRecord(evidence) ||
evidence.valid !== true ||
!isJsonRecord(evidence.current) ||
!isJsonRecord(evidence.root)
) {
throw new Error(`Full Release Validation evidence is invalid for run ${parentRunId}.`);
}
console.log(
`ok release evidence current=${displayValue(evidence.current.runId)} root=${displayValue(evidence.root.runId)} reused=${Boolean(evidence.evidenceReuse)}`,
);
} finally {
runStatus("git", ["worktree", "remove", "--force", verifierWorktree], {
stdio: ["ignore", "ignore", "ignore"],
});
rmSync(verifierWorktree, { force: true, recursive: true });
}
}
async function main() {
const argv = process.argv.slice(2);
const args = parseArgs(argv);
const reopenPath = args.reconcileRequest || args.requestFile;
if (reopenPath) {
assertRequestPath(reopenPath);
if (args.reconcileRequest || existsSync(reopenPath)) {
await reopenDispatch(reopenPath, args, argv);
return;
}
}
const targetSha = resolveTargetSha(args.sha, args.targetRef);
preflightTargetShaFetch(targetSha);
const targetVersion = targetVersionForTarget(targetSha);
const targetProfile = releaseProfileForVersion(targetVersion);
args.inputs.release_profile ??= targetProfile;
args.inputs.allow_unreleased_changelog ??= args.targetRef ? "false" : "true";
const targetContextRef = verifyTargetRef(args.targetRef, targetSha, targetVersion);
const workflowSha = resolveTrustedWorkflowSha(args.workflowSha, args.trustedWorkflowRef);
const trustedWorkflowHarness = assertTrustedWorkflowHarness(workflowSha);
// Read target blobs with trusted tooling before creating the workflow ref.
validatePackageSourceRef(targetSha, {
allowUnreleasedChangelog: args.inputs.allow_unreleased_changelog === "true",
});
if (trustedWorkflowHarness.contract === "1") {
args.inputs.reuse_evidence = "false";
}
const shortSha = workflowSha.slice(0, 12);
const branch = `release-ci/${shortSha}-${Date.now()}`;
const remoteBranchRef = `refs/heads/${branch}`;
const dispatchInputs = {
ref: targetSha,
expected_sha: targetSha,
...(trustedWorkflowHarness.contract === RELEASE_ISOLATION_TOOLING_CONTRACT
? {
trusted_workflow_json: JSON.stringify({
fullRef:
args.trustedWorkflowRef === "main"
? "refs/heads/main"
: `refs/tags/${args.trustedWorkflowRef}`,
ref: args.trustedWorkflowRef,
sha: workflowSha,
}),
}
: {}),
...(targetContextRef !== targetSha ? { target_context_ref: targetContextRef } : {}),
...args.inputs,
};
const selection = resolveDispatchSelection(workflowSha, dispatchInputs);
const requestId = randomUUID();
const requestPath = resolve(
args.requestFile || join(".artifacts", "full-release-validation", `${requestId}.json`),
);
let record: DispatchRecord | undefined;
if (!args.dryRun) {
const workflow: unknown = JSON.parse(
readGhApi(`repos/${REPOSITORY}/actions/workflows/${WORKFLOW}`),
);
requireDispatch(
isJsonRecord(workflow) &&
workflow.path === TRUSTED_WORKFLOW_PATH &&
Number.isSafeInteger(workflow.id) &&
Number(workflow.id) > 0,
"Workflow metadata does not match the pinned workflow path",
);
record = {
kind: REQUEST_KIND,
request: {
id: requestId,
host: "github.com",
repository: REPOSITORY,
workflowId: Number(workflow.id),
workflowPath: TRUSTED_WORKFLOW_PATH,
event: "workflow_dispatch",
workflowSha,
trustedWorkflowRef: args.trustedWorkflowRef,
targetSha,
targetVersion,
targetContextRef,
workflowRef: branch,
...selection,
},
phase: "prepared",
refs: { workflow: "intended" },
error: "none",
run: null,
};
retainDispatchRecord(requestPath, record);
}
console.log(`Request artifact: ${requestPath}${args.dryRun ? " (dry run; not written)" : ""}`);
console.log(`Validation SHA: ${targetSha}`);
console.log(`Tooling SHA: ${workflowSha}`);
console.log(`Trusted workflow ref: ${args.trustedWorkflowRef}`);
console.log(
`Frozen validation tuple: candidate=${targetSha} tooling=${workflowSha} rerun_group=${args.inputs.rerun_group}`,
);
console.log(`Temporary workflow ref: ${branch}`);
let parentRunId: string | undefined;
let parentConclusion = "";
let evidenceVerified = false;
let workflowRefCreated = false;
let dispatchAttempted = false;
let operationError: Error | undefined;
const retain = (next: DispatchRecord) => {
retainDispatchRecord(requestPath, next, record);
record = next;
};
try {
let payloadDirectory: string | undefined;
let dispatchOutput = "";
let dispatchError: unknown;
try {
let payloadPath = "";
if (!args.dryRun) {
const payload = JSON.stringify({ ref: branch, inputs: selection.wireInputs });
requireDispatch(
Buffer.byteLength(payload) <= MAX_REQUEST_BYTES,
"Dispatch payload exceeds its byte limit",
);
payloadDirectory = mkdtempSync(join(tmpdir(), "openclaw-release-dispatch-payload-"));
payloadPath = join(payloadDirectory, "dispatch.json");
writeFileSync(payloadPath, payload, { flag: "wx", mode: 0o600 });
}
if (record) {
retain({ ...record, refs: { workflow: "uncertain" } });
}
createTemporaryRef(remoteBranchRef, workflowSha, args.dryRun);
workflowRefCreated = true;
if (record) {
retain({ ...record, phase: "attempted", refs: { workflow: "created" } });
}
const dispatchArgs = [
"api",
"--include",
"--method",
"POST",
`repos/${REPOSITORY}/actions/workflows/${WORKFLOW}/dispatches`,
"--hostname",
"github.com",
"--input",
payloadPath,
];
// Once dispatch starts, the workflow ref may be needed for GitHub reruns even when
// the client loses the response. Cleanup resumes only after verified success.
dispatchAttempted = true;
try {
if (args.dryRun) {
console.log(
`+ gh api --method POST repos/${REPOSITORY}/actions/workflows/${WORKFLOW}/dispatches (input values omitted)`,
);
} else {
dispatchOutput = runGh(dispatchArgs, { stdio: ["ignore", "pipe", "pipe"] });
}
} catch (error) {
dispatchError = error;
dispatchOutput =
error instanceof Error && "stdout" in error ? stringValue(error.stdout) : "";
}
} finally {
if (payloadDirectory) {
try {
rmSync(payloadDirectory, { recursive: true, force: true });
} catch {
// A local cleanup failure must not change the observed POST outcome.
console.warn(
`Could not remove dispatch payload directory: ${JSON.stringify(payloadDirectory)}`,
);
}
}
}
if (record) {
let responseStatus = 0;
try {
responseStatus = parseGhHttpResponse(dispatchOutput).status;
} catch {
// A missing or partial response is not evidence that GitHub rejected the POST.
}
if ([400, 401, 403, 404, 422].includes(responseStatus)) {
retain({ ...record, phase: "rejected", error: "http-rejection" });
throw new Error(`dispatch=rejected: GitHub returned HTTP ${responseStatus}`);
}
if (dispatchError || responseStatus !== 204) {
retain({
...record,
error:
classifyReleaseGhTransportError(dispatchError) === "transient"
? "transport"
: "unclassified",
});
}
const observed = await reconcileDispatch(record);
retain({ ...record, phase: "observed", run: observed });
parentRunId = String(observed.id);
console.log(`dispatch=observed: attempt=${observed.attempt}`);
}
if (parentRunId) {
console.log(`Parent run: https://github.com/openclaw/openclaw/actions/runs/${parentRunId}`);
const completedRun = waitForWorkflowRun(parentRunId, workflowSha, record);
parentConclusion = stringValue(completedRun.conclusion);
if (parentConclusion !== "success") {
throw new Error(
`Full Release Validation concluded ${parentConclusion.toLowerCase() || "without a conclusion"}: https://github.com/openclaw/openclaw/actions/runs/${parentRunId}`,
);
}
verifyReleaseEvidence(parentRunId, workflowSha, args.trustedWorkflowRef);
evidenceVerified = true;
}
} catch (error) {
operationError = error instanceof Error ? error : new Error(String(error));
if (record) {
console.error(
`dispatch=${record.phase === "rejected" ? "rejected" : record.phase === "observed" ? "observed" : "unknown"} error=${record.error}`,
);
console.error(`Retained workflow ref: ${remoteBranchRef}`);
console.error(
`node scripts/full-release-validation-at-sha.mjs --reconcile-request ${JSON.stringify(requestPath)}`,
);
}
}
const cleanupBeforeDispatch =
!dispatchAttempted && workflowRefCreated && record?.refs.workflow !== "uncertain";
const cleanupAfterSuccess = shouldDeleteTemporaryWorkflowRef({
keepBranch: args.keepBranch,
dryRun: args.dryRun,
parentConclusion,
evidenceVerified,
});
let cleanupError: Error | undefined;
if (workflowRefCreated && (cleanupBeforeDispatch || cleanupAfterSuccess)) {
try {
deleteTemporaryRef(remoteBranchRef, args.dryRun);
} catch (error) {
cleanupError = error instanceof Error ? error : new Error(String(error));
}
} else if (workflowRefCreated) {
console.warn(
args.keepBranch
? `Kept ${remoteBranchRef}`
: `Kept ${remoteBranchRef}: ${
parentConclusion === "success"
? "release evidence was not verified"
: `parent concluded ${parentConclusion || "without a conclusion"}`
}. Keep it through GitHub reruns or evidence diagnosis; delete it after verified success.`,
);
}
if (operationError && cleanupError) {
throw new Error(
`${commandFailureMessage(operationError)}; temporary ref cleanup also failed: ${commandFailureMessage(cleanupError)}`,
{ cause: new AggregateError([operationError, cleanupError]) },
);
}
if (operationError) {
throw operationError;
}
if (cleanupError) {
throw cleanupError;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
await main();
} catch (error) {
console.error(
`[full-release-validation] FAILED: ${error instanceof Error ? error.message : String(error)}`,
);
console.error("[full-release-validation] FAILED (exit 1)");
process.exitCode = 1;
}
}