mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* fix(release): remove FRV flake receipts * docs(release): require successful FRV CI gate
4763 lines
168 KiB
TypeScript
4763 lines
168 KiB
TypeScript
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
chmodSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { runInNewContext } from "node:vm";
|
|
import { crc32 } from "node:zlib";
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { parse } from "yaml";
|
|
import { continueFailed, preflightContinuation } from "../../scripts/frv.mjs";
|
|
import { buildFullReleaseCandidateRequest } from "../../scripts/full-release-candidate-contract.mjs";
|
|
import {
|
|
createPublicationAdmission,
|
|
createPublicationObservations,
|
|
createPublicationSourceFact,
|
|
publicationDispatchEnvelope,
|
|
publicationIntentInputs,
|
|
publicationObservationJson,
|
|
publicationSourceRequest,
|
|
publicationSourceJson,
|
|
} from "../../scripts/full-release-publication-contract.mjs";
|
|
import {
|
|
buildReleaseExecutionPlanArtifact,
|
|
composeReleaseAttemptJobs,
|
|
MAX_RELEASE_ARTIFACT_BYTES,
|
|
releaseCompositeJobsSha256,
|
|
releaseExecutionPlanSha256,
|
|
type ReleaseExecutionPlan,
|
|
} from "../../scripts/full-release-validation-policy.mjs";
|
|
import { canonicalizeJsonValue } from "../../scripts/lib/canonical-json.mjs";
|
|
import {
|
|
releaseChildDispatchInputs,
|
|
releaseChildReuseSha256,
|
|
} from "../../scripts/lib/full-release-child-request.mjs";
|
|
import { validateReusableReleaseChild } from "../../scripts/lib/full-release-child-reuse.mjs";
|
|
import { FULL_RELEASE_CHILD_EVIDENCE_JOB } from "../../scripts/lib/full-release-evidence.mjs";
|
|
import { runManagedCommand } from "../../scripts/lib/managed-child-process.mts";
|
|
import {
|
|
artifactDownloadTimeoutMs,
|
|
createReleaseEvidenceClient,
|
|
expectedChildDispatches,
|
|
expectedSelectedChildDispatches,
|
|
manifestChildEntries,
|
|
readManifestArtifactArchive,
|
|
requiredChildKeysForRerunGroup,
|
|
resolveManifestChildOriginAttempt,
|
|
runReleaseCiGh,
|
|
selectExactChildRun,
|
|
selectExactChildRunFromPages,
|
|
selectManifestArtifact,
|
|
selectManifestParentJob,
|
|
selectedChildKeys,
|
|
tryReadReleaseDecisionArtifact,
|
|
validateEvidenceReuseChain,
|
|
validateManifestArtifactCompatibility,
|
|
validateManifestArtifactIdentity,
|
|
validateManifestChildRun,
|
|
validateParentManifest,
|
|
validateParentRunBinding,
|
|
validatePerformanceArtifactOnlyJobs,
|
|
validateReleaseRunEvidence,
|
|
validateRequestedEvidenceReuse,
|
|
} from "../../scripts/release-ci-summary.mjs";
|
|
import { authenticateFullReleaseValidationEvidence } from "../../scripts/validate-full-release-validation-evidence.mjs";
|
|
import { fullReleaseCandidateBindingFixture } from "../helpers/full-release-candidate.js";
|
|
import { withinTest } from "../helpers/promise.js";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const SCRIPT = "scripts/release-ci-summary.mjs";
|
|
const MANIFEST_ARTIFACT_ENTRY = "full-release-validation-manifest.json";
|
|
const hasUnzip = spawnSync("unzip", ["-v"], { stdio: "ignore" }).status === 0;
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
async function runInterruptedPlanFixture(
|
|
args: string[],
|
|
env: NodeJS.ProcessEnv,
|
|
signal: AbortSignal,
|
|
) {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
const command = runManagedCommand({
|
|
bin: process.execPath,
|
|
args,
|
|
env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
timeoutMs: 12_000,
|
|
requireProcessTreeExit: process.platform !== "win32",
|
|
signal,
|
|
onReady(child) {
|
|
child.stdout!.on("data", (chunk: Buffer) => {
|
|
stdout += chunk.toString();
|
|
});
|
|
child.stderr!.on("data", (chunk: Buffer) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
},
|
|
});
|
|
try {
|
|
return { status: await withinTest(command, signal), stdout, stderr };
|
|
} finally {
|
|
await command.catch(() => {});
|
|
}
|
|
}
|
|
|
|
function publicationSourceFixture(
|
|
fixture: ReturnType<typeof trustedMainNpmFixture>,
|
|
fullCoverage: boolean,
|
|
route: "normal" | "prepared" = "normal",
|
|
) {
|
|
return createPublicationSourceFact(
|
|
publicationSourceRequest({
|
|
PUBLICATION_INPUTS_JSON: JSON.stringify({
|
|
ref: fixture.targetSha,
|
|
release_profile: "beta",
|
|
rerun_group: "all",
|
|
skip_package_telegram_e2e: true,
|
|
...(fullCoverage
|
|
? {
|
|
provider: fixture.manifest.validationInputs.provider,
|
|
mode: fixture.manifest.validationInputs.mode,
|
|
npm_telegram_provider_mode: fixture.manifest.validationInputs.npmTelegramProviderMode,
|
|
}
|
|
: {}),
|
|
trusted_workflow_json: publicationDispatchEnvelope(null, {
|
|
validationPurpose: "publish",
|
|
publicationSelection: {
|
|
route,
|
|
npmDistTag: "beta",
|
|
publishOpenclawNpm: true,
|
|
pluginPublishScope: "all-publishable",
|
|
plugins: [],
|
|
},
|
|
}),
|
|
}),
|
|
PUBLICATION_TOOLING_JSON: JSON.stringify({
|
|
fullRef: "refs/heads/main",
|
|
sha: fixture.workflowSha,
|
|
}),
|
|
PUBLICATION_COVERAGE_POLICY: "npm-beta-v1",
|
|
PUBLICATION_TARGET_CONTEXT: "release/2026.8.28",
|
|
PUBLICATION_TARGET_SHA: fixture.targetSha,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_REF: "refs/heads/main",
|
|
GITHUB_SHA: fixture.workflowSha,
|
|
GITHUB_RUN_ID: fixture.runId,
|
|
GITHUB_RUN_ATTEMPT: "1",
|
|
}),
|
|
{ packages: [], platforms: [] },
|
|
{
|
|
version: "2026.8.28-beta.1",
|
|
packages: [{ name: "openclaw", version: "2026.8.28-beta.1", targets: ["npm"] }],
|
|
platforms: [],
|
|
},
|
|
);
|
|
}
|
|
|
|
function publicationRestoreFixture(fullCoverage = false, route: "normal" | "prepared" = "normal") {
|
|
const fixture = trustedMainNpmFixture();
|
|
const source = publicationSourceFixture(fixture, fullCoverage, route);
|
|
const time = "2026-08-28T12:00:00.000Z";
|
|
const observations = createPublicationObservations(source, {
|
|
sourceDigest: source.digest,
|
|
prerequisitesCompletedAt: time,
|
|
collectionStartedAt: time,
|
|
collectionCompletedAt: time,
|
|
npm: [
|
|
{
|
|
name: "openclaw",
|
|
version: "2026.8.28-beta.1",
|
|
required: true,
|
|
observedAt: time,
|
|
outcome: "observed",
|
|
state: {
|
|
packageExists: true,
|
|
hasVersionHistory: true,
|
|
selectedVersionExists: false,
|
|
latestVersion: null,
|
|
},
|
|
},
|
|
],
|
|
clawhub: [],
|
|
pendingAuthority: [],
|
|
plans: {
|
|
npm: { all: [], candidates: [], skippedPublished: [], warnings: [] },
|
|
clawhub: {
|
|
all: [],
|
|
candidates: [],
|
|
skippedPublished: [],
|
|
bootstrapCandidates: [],
|
|
missingTrustedPublisher: [],
|
|
warnings: [],
|
|
},
|
|
},
|
|
});
|
|
const admission = createPublicationAdmission(
|
|
source,
|
|
observations,
|
|
{
|
|
id: "444",
|
|
name: `full-release-publication-observations-${fixture.runId}-1`,
|
|
digest: `sha256:${"c".repeat(64)}`,
|
|
sizeInBytes: 4096,
|
|
},
|
|
time,
|
|
);
|
|
const plan = Object.assign(fixture.executionPlan, {
|
|
sourceAdmissionContract: "1",
|
|
sourceAdmission: source,
|
|
publicationAdmissionContract: "1",
|
|
publicationAdmission: admission,
|
|
});
|
|
plan.sha256 = releaseExecutionPlanSha256(plan);
|
|
return { ...fixture, plan, source, admission, request: { ...source, runAttempt: 2 } };
|
|
}
|
|
|
|
describe("original publication admission reader", () => {
|
|
it.each([
|
|
"changed-admission",
|
|
"skipped-original-upload",
|
|
"core-prepared",
|
|
"wrong-channel",
|
|
"wrong-target",
|
|
"wrong-attempt",
|
|
"wrong-route",
|
|
"continuation",
|
|
"continuation-mutated",
|
|
"continuation-unsupported",
|
|
"continuation-diagnostic-unsupported",
|
|
"continuation-diagnostic-missing-witness",
|
|
])("authenticates B in the complete strict summary: %s", async (fault) => {
|
|
const fixture = publicationRestoreFixture(
|
|
true,
|
|
fault === "core-prepared" ? "prepared" : "normal",
|
|
);
|
|
const continuation = fault.startsWith("continuation");
|
|
const restoreContract = continuation && !fault.endsWith("unsupported");
|
|
if (fault.includes("diagnostic")) {
|
|
const {
|
|
repository,
|
|
candidateSha,
|
|
targetContextRef,
|
|
tooling,
|
|
workflow,
|
|
runId,
|
|
runAttempt,
|
|
coverage,
|
|
} = fixture.source;
|
|
fixture.source = createPublicationSourceFact(
|
|
{
|
|
repository,
|
|
candidateSha,
|
|
targetContextRef,
|
|
tooling,
|
|
workflow,
|
|
runId,
|
|
runAttempt,
|
|
coverage,
|
|
validationPurpose: "diagnostic",
|
|
publicationSelection: null,
|
|
},
|
|
null,
|
|
null,
|
|
);
|
|
Object.assign(fixture.plan, { sourceAdmission: fixture.source, publicationAdmission: null });
|
|
fixture.plan.sha256 = releaseExecutionPlanSha256(fixture.plan);
|
|
}
|
|
Object.assign(fixture.manifest, {
|
|
sourceAdmissionContract: "1",
|
|
sourceAdmission: fixture.source,
|
|
publicationAdmissionContract: "1",
|
|
publicationAdmission: structuredClone(fixture.admission),
|
|
trustedWorkflow: fixture.plan.trustedWorkflow,
|
|
executionPlanSha256: fixture.plan.sha256,
|
|
});
|
|
Object.assign(fixture.manifest.validationInputs, publicationIntentInputs(fixture.source));
|
|
delete fixture.manifest.evidenceReuse;
|
|
const parent = {
|
|
...fixture.parentRun,
|
|
name: "Full Release Validation",
|
|
display_title: "Full Release Validation",
|
|
repository: { full_name: "openclaw/openclaw" },
|
|
head_repository: { full_name: "openclaw/openclaw" },
|
|
};
|
|
const originalJobs = [
|
|
{
|
|
id: 901,
|
|
name: "Resolve target ref",
|
|
run_attempt: 1,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
steps: [{ name: "Finalize publication admission", conclusion: "success" }],
|
|
},
|
|
{
|
|
id: 902,
|
|
name: "Seal release execution plan",
|
|
run_attempt: 1,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
steps: [
|
|
{
|
|
name: "Seal immutable release execution plan",
|
|
number: 5,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
started_at: "2026-08-28T12:01:00.000Z",
|
|
completed_at: "2026-08-28T12:01:01.000Z",
|
|
},
|
|
{
|
|
name: "Upload immutable release execution plan",
|
|
number: 6,
|
|
status: "completed",
|
|
conclusion: fault === "skipped-original-upload" ? "skipped" : "success",
|
|
started_at: "2026-08-28T12:01:01.000Z",
|
|
completed_at: "2026-08-28T12:01:03.000Z",
|
|
},
|
|
...(restoreContract
|
|
? [
|
|
{
|
|
name: "Record immutable release execution plan digest",
|
|
number: 7,
|
|
status: "completed",
|
|
conclusion: fault.endsWith("missing-witness") ? "skipped" : "success",
|
|
started_at: "2026-08-28T12:01:03.000Z",
|
|
completed_at: "2026-08-28T12:01:03.000Z",
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
];
|
|
const retained = {
|
|
plan: fixture.plan,
|
|
artifact: {
|
|
created_at: "2026-08-28T12:01:02.000Z",
|
|
workflow_run: { head_sha: fixture.workflowSha, head_branch: "main" },
|
|
},
|
|
};
|
|
const client = {
|
|
...fixture.client,
|
|
validateChildReuse: async () => {
|
|
throw new Error("publication admission fixture does not reuse children");
|
|
},
|
|
getJobLog: async (id: number) =>
|
|
id === 902
|
|
? `2026-08-28T12:01:03.750Z FRV_EXECUTION_PLAN_SHA256=${releaseExecutionPlanSha256(fixture.plan)}\n`
|
|
: fixture.client.getJobLog(id),
|
|
getRun: async (id: string) => fixture.client.getRun(id),
|
|
getParentJobs: async (id: string) => fixture.client.getParentJobs(id),
|
|
getWorkflowSource: vi.fn(
|
|
() =>
|
|
'env:\n FULL_RELEASE_SOURCE_ADMISSION_CONTRACT: "1"\n FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT: "1"\n' +
|
|
(restoreContract ? ' FULL_RELEASE_EXECUTION_PLAN_RESTORE_CONTRACT: "1"\n' : ""),
|
|
),
|
|
getRunAttempt: vi.fn((runId: string, attempt: number) => {
|
|
expect(attempt).toBe(1);
|
|
return runId === fixture.runId ? parent : fixture.client.getRun(runId);
|
|
}),
|
|
getArtifact: () => {
|
|
throw new Error("unexpected artifact lookup after fixture archive load");
|
|
},
|
|
getRunAttemptJobs: vi.fn(async (runId: string) =>
|
|
runId === fixture.runId ? originalJobs : fixture.client.getRunAttemptJobs(runId),
|
|
),
|
|
loadExecutionPlanEvidence: vi.fn(() => retained),
|
|
};
|
|
if (fault === "changed-admission") {
|
|
const changed = structuredClone(fixture.admission);
|
|
const row = expectDefined(changed.observations.npm[0], "required root observation");
|
|
if (row.outcome !== "observed") {
|
|
throw new Error("fixture root observation unavailable");
|
|
}
|
|
row.state.latestVersion = "2026.8.27";
|
|
changed.binding.observationsDigest = `sha256:${createHash("sha256").update(publicationObservationJson(changed.observations)).digest("hex")}`;
|
|
Object.assign(fixture.manifest, { publicationAdmission: changed });
|
|
}
|
|
const strictOptions = {
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
};
|
|
const consumerCase = !["changed-admission", "skipped-original-upload"].includes(fault);
|
|
if (fault.startsWith("continuation")) {
|
|
const observed = structuredClone(fixture.plan);
|
|
if (fault === "continuation-mutated") {
|
|
const admission = expectDefined(observed.publicationAdmission, "admission");
|
|
const row = expectDefined(admission.observations.npm[0], "root observation");
|
|
if (row.outcome !== "observed") {
|
|
throw new Error("fixture root observation unavailable");
|
|
}
|
|
row.state.latestVersion = "2026.8.27";
|
|
admission.binding.observationsDigest = `sha256:${createHash("sha256").update(publicationObservationJson(admission.observations)).digest("hex")}`;
|
|
observed.sha256 = releaseExecutionPlanSha256(observed);
|
|
}
|
|
const continuationClient = {
|
|
getReleaseEvidenceClient: () => client,
|
|
getRun: client.getRun,
|
|
getAttemptJobs: vi.fn(async () => {
|
|
throw new Error("unsupported parent must not read child attempts");
|
|
}),
|
|
getParentJobs: async () => [
|
|
...originalJobs,
|
|
...fixture.client.getParentJobs(fixture.runId),
|
|
],
|
|
getRunAttempt: async (id: string) =>
|
|
id === fixture.runId ? parent : fixture.client.getRun(id),
|
|
getJobLog: async (id: number) =>
|
|
id === 901
|
|
? `RERUN_GROUP: all\nFAIL_FAST: false\nTARGET_SHA: ${fixture.targetSha}`
|
|
: fixture.client.getJobLog(id),
|
|
rerunFailed: vi.fn(),
|
|
rerunParent: vi.fn(),
|
|
};
|
|
const unsupported = fault.endsWith("unsupported") || fault.endsWith("missing-witness");
|
|
const result = unsupported
|
|
? continueFailed(observed, fixture.runId, continuationClient)
|
|
: preflightContinuation(observed, fixture.runId, continuationClient);
|
|
if (fault === "continuation") {
|
|
await expect(result).resolves.toBeDefined();
|
|
} else if (unsupported) {
|
|
await expect(result).rejects.toThrow(
|
|
fault.endsWith("unsupported")
|
|
? "frozen workflow cannot restore publication admission"
|
|
: "publication original plan digest witness did not succeed",
|
|
);
|
|
expect(continuationClient.rerunFailed).not.toHaveBeenCalled();
|
|
expect(continuationClient.rerunParent).not.toHaveBeenCalled();
|
|
expect(continuationClient.getAttemptJobs).not.toHaveBeenCalled();
|
|
} else {
|
|
await expect(result).rejects.toThrow(
|
|
"continuation differs from the authenticated original publication plan",
|
|
);
|
|
}
|
|
expect(client.loadExecutionPlanEvidence).toHaveBeenCalledTimes(unsupported ? 0 : 1);
|
|
return;
|
|
}
|
|
const result = consumerCase
|
|
? authenticateFullReleaseValidationEvidence(
|
|
{
|
|
run: parent,
|
|
expectedRepository: "openclaw/openclaw",
|
|
expectedRunId: fixture.runId,
|
|
expectedRunAttempt: fault === "wrong-attempt" ? 2 : 1,
|
|
expectedTargetSha: fault === "wrong-target" ? "d".repeat(40) : fixture.targetSha,
|
|
expectedReleaseTag: "v2026.8.28-beta.1",
|
|
isTrustedMainAncestor: () => true,
|
|
verifierSourceContent: strictOptions.verifierSourceContent,
|
|
verifierSourceSha: strictOptions.verifierSourceSha,
|
|
...(fault === "wrong-route"
|
|
? {
|
|
expectedPublicationSelection: {
|
|
...expectDefined(fixture.source.publicationSelection, "publication selection"),
|
|
route: "prepared" as const,
|
|
},
|
|
}
|
|
: {
|
|
expectedCoreNpmPublication: {
|
|
npmDistTag: fault === "wrong-channel" ? "latest" : "beta",
|
|
},
|
|
}),
|
|
},
|
|
client,
|
|
)
|
|
: validateReleaseRunEvidence(strictOptions, client);
|
|
if (fault === "core-prepared") {
|
|
const value = await result;
|
|
const verified = "evidence" in value ? value.evidence : value;
|
|
expect(verified.children).toHaveLength(5);
|
|
expect(verified.current.manifest.publicationAdmission).toEqual(fixture.admission);
|
|
expect(client.loadExecutionPlanEvidence).toHaveBeenCalledTimes(1);
|
|
expect(client.getWorkflowSource).toHaveBeenCalledTimes(1);
|
|
} else {
|
|
await expect(result).rejects.toThrow(
|
|
/publication|consumer|targetSha mismatch|differently selected|attempt differs/iu,
|
|
);
|
|
}
|
|
});
|
|
|
|
it.for([
|
|
"workflow-restore",
|
|
"workflow-restore-failed-resolution",
|
|
"workflow-plan-restore",
|
|
"workflow-plan-mutated",
|
|
"workflow-plan-prewrite",
|
|
"workflow-restore-cached",
|
|
"workflow-plan-cached",
|
|
"cached-plan-mutated",
|
|
"cached-plan-missing-witness",
|
|
"cached-plan-duplicate-witness",
|
|
"cached-plan-outside-witness",
|
|
"cached-plan-wrong-witness-step",
|
|
"cached-plan-failed-witness",
|
|
"cached-plan-historical",
|
|
"reuploaded-plan",
|
|
"interrupted-sealer",
|
|
"duplicate-sealer",
|
|
"skipped-seal",
|
|
"failed-upload",
|
|
"late-artifact",
|
|
"upload-before-seal",
|
|
"wrong-repository",
|
|
"wrong-attempt",
|
|
"wrong-workflow",
|
|
"wrong-sha",
|
|
"deleted-capability",
|
|
"deleted-admission",
|
|
"missing-plan",
|
|
"duplicate-plan",
|
|
"incomplete-list",
|
|
"wrong-archive-digest",
|
|
"extra-entry",
|
|
"symlink-entry",
|
|
"changed-selection",
|
|
])("authenticates retained attempt one before any new observations: %s", async (fault, t) => {
|
|
const fixture = publicationRestoreFixture();
|
|
const cachedRestore = fault.includes("cached");
|
|
const witnessContract =
|
|
(cachedRestore && fault !== "cached-plan-historical") || fault.startsWith("reuploaded-plan");
|
|
const originalPlanDigest = releaseExecutionPlanSha256(fixture.plan);
|
|
if (fault === "cached-plan-mutated") {
|
|
const row = expectDefined(fixture.admission.observations.npm[0], "root observation");
|
|
if (row.outcome !== "observed") {
|
|
throw new Error("fixture root observation unavailable");
|
|
}
|
|
row.state.latestVersion = "2026.8.27";
|
|
fixture.admission.binding.observationsDigest = `sha256:${createHash("sha256").update(publicationObservationJson(fixture.admission.observations)).digest("hex")}`;
|
|
fixture.plan.sha256 = releaseExecutionPlanSha256(fixture.plan);
|
|
}
|
|
let interruptedWork: ReturnType<typeof runInterruptedPlanFixture> | undefined;
|
|
const caseTempDirs =
|
|
fault === "interrupted-sealer"
|
|
? useAutoCleanupTempDirTracker((cleanup) =>
|
|
t.onTestFinished(async () => {
|
|
// The interrupted fixture owns its root until its managed process tree has joined.
|
|
await interruptedWork?.catch(() => {});
|
|
cleanup();
|
|
}),
|
|
)
|
|
: tempDirs;
|
|
const root = caseTempDirs.make("publication-original-reader-");
|
|
const request = join(root, "request.json");
|
|
const fixturesPath = join(root, "fixtures.json");
|
|
const archivePath = join(root, "plan.zip");
|
|
const callsPath = join(root, "calls.jsonl");
|
|
const gh = join(root, "gh");
|
|
const workflowPath = ".github/workflows/full-release-validation.yml";
|
|
const workflowBytes = Buffer.from(
|
|
'env:\n FULL_RELEASE_SOURCE_ADMISSION_CONTRACT: "1"\n' +
|
|
(fault === "deleted-capability"
|
|
? ""
|
|
: ' FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT: "1"\n') +
|
|
(witnessContract ? ' FULL_RELEASE_EXECUTION_PLAN_RESTORE_CONTRACT: "1"\n' : ""),
|
|
);
|
|
if (fault === "deleted-admission") {
|
|
Reflect.deleteProperty(fixture.plan, "publicationAdmissionContract");
|
|
Reflect.deleteProperty(fixture.plan, "publicationAdmission");
|
|
fixture.plan.sha256 = releaseExecutionPlanSha256(fixture.plan);
|
|
}
|
|
if (fault === "changed-selection") {
|
|
fixture.request.publicationSelection = {
|
|
...expectDefined(fixture.request.publicationSelection, "publication selection"),
|
|
route: "prepared",
|
|
};
|
|
}
|
|
if (fault === "interrupted-sealer") {
|
|
const pendingGh = gh;
|
|
const ready = join(root, "pending-gh-ready");
|
|
const settled = join(root, "pending-gh-settled");
|
|
const receipts = join(root, "pending-gh-receipts");
|
|
const written = join(root, "interrupted-plan.json");
|
|
const outputsPath = join(root, "interrupted-outputs");
|
|
const admissionPath = join(root, "source-admission.json");
|
|
writeFileSync(
|
|
admissionPath,
|
|
JSON.stringify({
|
|
sourceAdmissionContract: "1",
|
|
sourceAdmission: fixture.source,
|
|
publicationAdmissionContract: "1",
|
|
publicationAdmission: fixture.admission,
|
|
}),
|
|
);
|
|
writeFileSync(
|
|
pendingGh,
|
|
`#!${process.execPath}
|
|
const fs = require("node:fs");
|
|
const receipts = fs.openSync(${JSON.stringify(receipts)}, "w");
|
|
const parent = process.ppid;
|
|
process.once("exit", () => {
|
|
fs.writeFileSync(${JSON.stringify(settled)}, "settled");
|
|
// Synchronous FIFO writes cannot be lost when this orphan exits immediately afterward.
|
|
fs.writeSync(receipts, "settled\\n");
|
|
});
|
|
setInterval(() => { if (process.ppid !== parent) process.exit(0); }, 10);
|
|
setTimeout(() => {}, 30000);
|
|
fs.writeFileSync(${JSON.stringify(ready)}, String(process.pid));
|
|
fs.writeSync(receipts, "ready\\n");
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const inputs = {
|
|
childPhaseVersion: 3,
|
|
sourceAdmissionContract: "1",
|
|
sourceAdmission: fixture.source,
|
|
parentRunId: fixture.runId,
|
|
parentRunAttempt: 1,
|
|
workflowRef: "main",
|
|
workflowSha: fixture.workflowSha,
|
|
rerunGroup: "all",
|
|
coveragePolicy: fixture.plan.coveragePolicy,
|
|
targetVersion: fixture.source.projection!.version,
|
|
resolveTargetResult: "success",
|
|
candidateAcquisitionResult: "skipped",
|
|
candidateRequestInput: fixture.plan.candidateRequest,
|
|
trustedWorkflow: fixture.plan.trustedWorkflow,
|
|
evidenceReuse: true,
|
|
evidenceRunId: "99",
|
|
evidenceRootRunId: "99",
|
|
evidencePolicy: "exact-target-full-validation-v1",
|
|
evidenceSha: fixture.targetSha,
|
|
evidenceRunUrl: "https://example.invalid/runs/99",
|
|
evidenceChangedPaths: [],
|
|
};
|
|
interruptedWork = runInterruptedPlanFixture(
|
|
[
|
|
"--input-type=module",
|
|
"--eval",
|
|
`
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import { closeSync, createReadStream, existsSync, openSync, readFileSync } from "node:fs";
|
|
import { once } from "node:events";
|
|
execFileSync("mkfifo", [${JSON.stringify(receipts)}]);
|
|
// Keep the FIFO open until gh owns its writer; then EOF joins that fixture's exit.
|
|
let heldWriter = openSync(${JSON.stringify(receipts)}, "r+");
|
|
const receiptStream = createReadStream(${JSON.stringify(receipts)}, { encoding: "utf8" });
|
|
const receiptClosed = new Promise(resolve => receiptStream.once("close", resolve));
|
|
const ended = once(receiptStream, "end");
|
|
let markReady;
|
|
const reached = new Promise(resolve => { markReady = resolve; });
|
|
let receiptText = "";
|
|
receiptStream.on("data", chunk => {
|
|
receiptText += chunk;
|
|
if (receiptText.includes("ready\\n")) markReady();
|
|
});
|
|
await once(receiptStream, "open");
|
|
const child = spawn(process.execPath, [${JSON.stringify(resolve("scripts/full-release-validation-state.mjs"))}, "plan"], { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
child.stdout.pipe(process.stdout); child.stderr.pipe(process.stderr);
|
|
const closed = new Promise(resolve => child.once("close", (code, signal) => resolve({ code, signal })));
|
|
const timeout = setTimeout(() => child.kill("SIGKILL"), 8000);
|
|
try {
|
|
await Promise.race([reached, closed.then(() => {
|
|
// The durable marker is written before the pipe receipt; close can win their delivery race.
|
|
if (!existsSync(${JSON.stringify(ready)})) throw new Error("reuse did not reach controlled gh: " + (existsSync(${JSON.stringify(written)}) ? readFileSync(${JSON.stringify(written)}, "utf8") : "no checkpoint"));
|
|
})]);
|
|
closeSync(heldWriter); heldWriter = undefined;
|
|
child.kill("SIGTERM");
|
|
const result = await closed;
|
|
if (result.code !== 1 || result.signal !== null) throw new Error("interruption did not write and exit 1");
|
|
await ended;
|
|
if (!existsSync(${JSON.stringify(settled)})) throw new Error("controlled gh did not settle after owner interruption");
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
if (heldWriter !== undefined) closeSync(heldWriter);
|
|
receiptStream.destroy();
|
|
child.kill("SIGKILL");
|
|
await closed;
|
|
await receiptClosed;
|
|
}
|
|
`,
|
|
],
|
|
{
|
|
PATH: `${root}:${process.env.PATH ?? ""}`,
|
|
OPENCLAW_GH_BIN: pendingGh,
|
|
GH_TOKEN: "synthetic-evidence-token",
|
|
FULL_RELEASE_SOURCE_ADMISSION_CONTRACT: "1",
|
|
FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT: "1",
|
|
FULL_RELEASE_EXECUTION_PLAN_PATH: written,
|
|
PUBLICATION_ADMISSION_PATH: admissionPath,
|
|
FULL_RELEASE_PLAN_INPUTS_JSON: JSON.stringify(inputs),
|
|
GITHUB_OUTPUT: outputsPath,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ID: fixture.runId,
|
|
GITHUB_RUN_ATTEMPT: "1",
|
|
GITHUB_REF_NAME: "main",
|
|
GITHUB_SHA: fixture.workflowSha,
|
|
TARGET_SHA: fixture.targetSha,
|
|
RELEASE_PROFILE: "beta",
|
|
RERUN_GROUP: "all",
|
|
},
|
|
t.signal,
|
|
);
|
|
const interrupted = await interruptedWork;
|
|
expect(interrupted.status, interrupted.stderr).toBe(0);
|
|
Object.assign(fixture.plan, JSON.parse(readFileSync(written, "utf8")));
|
|
expect(fixture.plan.errors).toContainEqual(
|
|
expect.objectContaining({ kind: "collector_cancelled" }),
|
|
);
|
|
expect(fixture.plan.publicationAdmission).toEqual(fixture.admission);
|
|
const workflow = parse(readFileSync(".github/workflows/full-release-validation.yml", "utf8"));
|
|
const upload = workflow.jobs.release_execution_plan.steps.find(
|
|
(step: { name: string }) => step.name === "Upload immutable release execution plan",
|
|
);
|
|
const outputs = Object.fromEntries(
|
|
readFileSync(outputsPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => line.split("=")),
|
|
);
|
|
expect(outputs.sha256).toBe(fixture.plan.sha256);
|
|
expect(outputs.source_parent_attempt).toBe("1");
|
|
expect(
|
|
runInNewContext(
|
|
String(upload.if)
|
|
.replace(/^\$\{\{|\}\}$/gu, "")
|
|
.trim(),
|
|
{
|
|
always: () => true,
|
|
github: { run_attempt: 1 },
|
|
steps: { plan: { outputs } },
|
|
},
|
|
),
|
|
).toBe(true);
|
|
expect(upload.with.overwrite).toBe(false);
|
|
}
|
|
const entry = "full-release-execution-plan.json";
|
|
const archive = makeStoredZip({
|
|
[entry]: JSON.stringify(fixture.plan) + "\n",
|
|
...(fault === "extra-entry" ? { "unexpected.json": "{}" } : {}),
|
|
});
|
|
if (fault === "symlink-entry") {
|
|
const central = archive.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
|
|
archive[central + 5] = 3;
|
|
archive.writeUInt32LE((0o120777 << 16) >>> 0, central + 38);
|
|
}
|
|
const artifact = {
|
|
id: 456,
|
|
name: `full-release-execution-plan-${fixture.runId}`,
|
|
size_in_bytes: archive.length,
|
|
digest: `sha256:${fault === "wrong-archive-digest" ? "a".repeat(64) : createHash("sha256").update(archive).digest("hex")}`,
|
|
expired: false,
|
|
created_at:
|
|
fault === "late-artifact" || fault.startsWith("reuploaded-plan")
|
|
? "2026-08-29T12:01:02.000Z"
|
|
: "2026-08-28T12:01:02.000Z",
|
|
workflow_run: {
|
|
id: Number(fixture.runId),
|
|
head_branch: "main",
|
|
head_sha: fixture.workflowSha,
|
|
},
|
|
};
|
|
const artifacts =
|
|
fault === "missing-plan" || cachedRestore
|
|
? []
|
|
: fault === "duplicate-plan"
|
|
? [artifact, { ...artifact, id: 457 }]
|
|
: [artifact];
|
|
const fixtures = {
|
|
parent: {
|
|
id: Number(fixture.runId),
|
|
repository: {
|
|
full_name: fault === "wrong-repository" ? "other/repo" : "openclaw/openclaw",
|
|
},
|
|
head_repository: { full_name: "openclaw/openclaw" },
|
|
path: fault === "wrong-workflow" ? ".github/workflows/ci.yml" : workflowPath,
|
|
event: "workflow_dispatch",
|
|
run_attempt: fault === "wrong-attempt" ? 2 : 1,
|
|
head_sha: fault === "wrong-sha" ? "f".repeat(40) : fixture.workflowSha,
|
|
head_branch: "main",
|
|
},
|
|
workflow: {
|
|
type: "file",
|
|
encoding: "base64",
|
|
path: workflowPath,
|
|
size: workflowBytes.length,
|
|
sha: createHash("sha1")
|
|
.update(`blob ${workflowBytes.length}\0`)
|
|
.update(workflowBytes)
|
|
.digest("hex"),
|
|
content: workflowBytes.toString("base64"),
|
|
},
|
|
jobs: {
|
|
total_count: fault === "duplicate-sealer" ? 3 : 2,
|
|
jobs: [
|
|
{
|
|
id: 999,
|
|
name: "Resolve target ref",
|
|
run_attempt: 1,
|
|
status: "completed",
|
|
conclusion: fault.endsWith("failed-resolution") ? "failure" : "success",
|
|
steps: [{ name: "Finalize publication admission", conclusion: "success" }],
|
|
},
|
|
...Array.from({ length: fault === "duplicate-sealer" ? 2 : 1 }, (_, index) => ({
|
|
id: 1000 + index,
|
|
name: "Seal release execution plan",
|
|
run_attempt: 1,
|
|
status: "completed",
|
|
conclusion: fault === "interrupted-sealer" ? "failure" : "success",
|
|
steps: [
|
|
{
|
|
name: "Seal immutable release execution plan",
|
|
number: 5,
|
|
status: "completed",
|
|
conclusion:
|
|
fault === "skipped-seal"
|
|
? "skipped"
|
|
: fault === "interrupted-sealer"
|
|
? "failure"
|
|
: "success",
|
|
started_at: "2026-08-28T12:01:00.000Z",
|
|
completed_at: "2026-08-28T12:01:01.000Z",
|
|
},
|
|
{
|
|
name: "Upload immutable release execution plan",
|
|
number: fault === "upload-before-seal" ? 4 : 6,
|
|
status: "completed",
|
|
conclusion: fault === "failed-upload" ? "failure" : "success",
|
|
started_at: "2026-08-28T12:01:01.000Z",
|
|
completed_at: "2026-08-28T12:01:03.000Z",
|
|
},
|
|
...(witnessContract
|
|
? [
|
|
{
|
|
name:
|
|
fault === "cached-plan-wrong-witness-step"
|
|
? "Unrelated successful step"
|
|
: "Record immutable release execution plan digest",
|
|
number: 7,
|
|
status: "completed",
|
|
conclusion: fault === "cached-plan-failed-witness" ? "failure" : "success",
|
|
started_at: "2026-08-28T12:01:03.000Z",
|
|
completed_at: "2026-08-28T12:01:03.000Z",
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
})),
|
|
],
|
|
},
|
|
listing: { total_count: fault === "incomplete-list" ? 2 : artifacts.length, artifacts },
|
|
artifact,
|
|
log:
|
|
fault === "cached-plan-missing-witness"
|
|
? "2026-08-28T12:01:03.750Z unrelated output\n"
|
|
: `${fault === "cached-plan-outside-witness" ? "2026-08-28T12:01:02.750Z" : "2026-08-28T12:01:03.750Z"} FRV_EXECUTION_PLAN_SHA256=${originalPlanDigest}\n`.repeat(
|
|
fault === "cached-plan-duplicate-witness" ? 2 : 1,
|
|
),
|
|
};
|
|
writeFileSync(request, JSON.stringify(fixture.request));
|
|
writeFileSync(fixturesPath, JSON.stringify(fixtures));
|
|
writeFileSync(archivePath, archive);
|
|
writeFileSync(
|
|
gh,
|
|
`#!${process.execPath}
|
|
const fs = require("node:fs");
|
|
const args = process.argv.slice(2), endpoint = args[1];
|
|
fs.appendFileSync(${JSON.stringify(callsPath)}, JSON.stringify(args) + "\\n");
|
|
if (args[0] !== "api") throw new Error("unexpected non-read");
|
|
const fixture = JSON.parse(fs.readFileSync(${JSON.stringify(fixturesPath)}, "utf8"));
|
|
let value;
|
|
if (endpoint === "repos/openclaw/openclaw/actions/runs/${fixture.runId}/attempts/1") value = fixture.parent;
|
|
else if (endpoint.startsWith("repos/openclaw/openclaw/actions/runs/${fixture.runId}/attempts/1/jobs?")) value = fixture.jobs;
|
|
else if (endpoint === "repos/openclaw/openclaw/contents/${workflowPath}?ref=${fixture.workflowSha}") value = fixture.workflow;
|
|
else if (endpoint.startsWith("repos/openclaw/openclaw/actions/runs/${fixture.runId}/artifacts?")) value = fixture.listing;
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/jobs/1000/logs") {
|
|
process.stdout.write(fixture.log); process.exit(0);
|
|
}
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/artifacts/456") value = fixture.artifact;
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/artifacts/456/zip") {
|
|
process.stdout.write(fs.readFileSync(${JSON.stringify(archivePath)})); process.exit(0);
|
|
} else throw new Error("unplanned evidence request");
|
|
process.stdout.write(JSON.stringify(value));
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const workflowRestore = fault.startsWith("workflow-restore");
|
|
const planRestore = fault.startsWith("workflow-plan");
|
|
const cachedPlanPath = join(
|
|
root,
|
|
"full-release-execution-plan",
|
|
"full-release-execution-plan.json",
|
|
);
|
|
const originalPlanBytes = JSON.stringify(fixture.plan) + "\n";
|
|
if (planRestore || cachedRestore) {
|
|
mkdirSync(dirname(cachedPlanPath), { recursive: true });
|
|
const cached = structuredClone(fixture.plan);
|
|
if (fault === "workflow-plan-mutated") {
|
|
// A self-consistent cache is still not authority over the authenticated original.
|
|
const row = fixture.admission.observations.npm[0]!;
|
|
const changedAdmission = structuredClone(fixture.admission);
|
|
const changed = changedAdmission.observations.npm[0]!;
|
|
expect(row.outcome).toBe("observed");
|
|
if (changed.outcome === "observed") {
|
|
changed.state.latestVersion = "2026.8.27";
|
|
}
|
|
changedAdmission.binding.observationsDigest = `sha256:${createHash("sha256").update(publicationObservationJson(changedAdmission.observations)).digest("hex")}`;
|
|
cached.publicationAdmission = changedAdmission;
|
|
cached.sha256 = releaseExecutionPlanSha256(cached);
|
|
}
|
|
writeFileSync(
|
|
cachedPlanPath,
|
|
fault === "workflow-plan-mutated" ? JSON.stringify(cached) + "\n" : originalPlanBytes,
|
|
);
|
|
}
|
|
let restoreCommand: string | undefined;
|
|
if (workflowRestore) {
|
|
writeFileSync(join(root, "publication-source-request.json"), JSON.stringify(fixture.request));
|
|
const workflow = parse(readFileSync(".github/workflows/full-release-validation.yml", "utf8"));
|
|
restoreCommand = workflow.jobs.resolve_target.steps.find(
|
|
(step: { name: string }) => step.name === "Admit publication source",
|
|
).run;
|
|
}
|
|
const result = spawnSync(
|
|
workflowRestore ? "bash" : process.execPath,
|
|
planRestore
|
|
? [resolve("scripts/full-release-validation-state.mjs"), "plan"]
|
|
: workflowRestore
|
|
? ["-c", expectDefined(restoreCommand, "actual admission command")]
|
|
: [
|
|
"--input-type=module",
|
|
"--eval",
|
|
`
|
|
import { readFileSync } from "node:fs";
|
|
import { restoreOriginalPublicationAdmission } from ${JSON.stringify(pathToFileURL(resolve(SCRIPT)).href)};
|
|
const request = JSON.parse(readFileSync(${JSON.stringify(request)}, "utf8"));
|
|
const restored = await restoreOriginalPublicationAdmission({ request${cachedRestore ? `, cachedPlan: JSON.parse(readFileSync(${JSON.stringify(cachedPlanPath)}, "utf8"))` : ""} });
|
|
process.stdout.write(JSON.stringify(restored));
|
|
`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${root}:${process.env.PATH ?? ""}`,
|
|
OPENCLAW_GH_BIN: gh,
|
|
GH_TOKEN: "synthetic-evidence-token",
|
|
GITHUB_RUN_ATTEMPT:
|
|
fault === "workflow-plan-prewrite" ? "1" : fault === "workflow-plan-cached" ? "3" : "2",
|
|
RUNNER_TEMP: root,
|
|
FULL_RELEASE_EXECUTION_PLAN_PATH: cachedPlanPath,
|
|
GITHUB_OUTPUT: join(root, "outputs"),
|
|
PUBLICATION_REQUIRED: "true",
|
|
// A new observation path cannot run without this unavailable target.
|
|
PUBLICATION_TARGET_ROOT: join(root, "unavailable-candidate"),
|
|
...(planRestore
|
|
? {
|
|
FULL_RELEASE_SOURCE_ADMISSION_CONTRACT: "1",
|
|
FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT: "1",
|
|
FULL_RELEASE_EXECUTION_PLAN_PATH: cachedPlanPath,
|
|
FULL_RELEASE_RESTORE_PLAN: fault === "workflow-plan-prewrite" ? "false" : "true",
|
|
FULL_RELEASE_PLAN_INPUTS_JSON: "must-not-be-read",
|
|
SOURCE_ADMISSION_JSON: JSON.stringify(fixture.source),
|
|
CANDIDATE_REQUEST_JSON: JSON.stringify(fixture.plan.candidateRequest),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ID: fixture.runId,
|
|
GITHUB_SHA: fixture.workflowSha,
|
|
GITHUB_REF_NAME: "main",
|
|
RELEASE_PROFILE: fixture.source.coverage.release_profile,
|
|
RERUN_GROUP: fixture.source.coverage.rerun_group,
|
|
TARGET_SHA: fixture.targetSha,
|
|
TARGET_VERSION: fixture.source.projection!.version,
|
|
TARGET_CONTEXT_REF: fixture.source.targetContextRef,
|
|
COVERAGE_POLICY: fixture.source.coverage.coverage_policy,
|
|
}
|
|
: {}),
|
|
},
|
|
timeout: 10_000,
|
|
},
|
|
);
|
|
const calls = existsSync(callsPath) ? readFileSync(callsPath, "utf8") : "";
|
|
expect(calls).not.toContain("registry.npmjs.org");
|
|
expect(calls).not.toContain("clawhub.ai");
|
|
expect(calls).not.toContain("auth");
|
|
expect(calls).not.toContain("/444");
|
|
if (fault === "workflow-plan-restore" || fault === "workflow-plan-cached") {
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readFileSync(cachedPlanPath, "utf8")).toBe(originalPlanBytes);
|
|
if (cachedRestore) {
|
|
expect(calls).not.toContain("/artifacts");
|
|
}
|
|
} else if (
|
|
fault === "workflow-restore" ||
|
|
fault === "workflow-restore-cached" ||
|
|
fault === "reuploaded-plan" ||
|
|
fault === "interrupted-sealer"
|
|
) {
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const restored = workflowRestore
|
|
? {
|
|
source: JSON.parse(
|
|
readFileSync(join(root, "publication-source-admission.json"), "utf8"),
|
|
),
|
|
admission: JSON.parse(readFileSync(join(root, "publication-admission.json"), "utf8"))
|
|
.publicationAdmission,
|
|
plan: fixture.plan,
|
|
}
|
|
: JSON.parse(result.stdout);
|
|
expect(restored.source).toEqual(fixture.source);
|
|
expect(restored.admission).toEqual(fixture.admission);
|
|
expect(restored.plan.sha256).toBe(fixture.plan.sha256);
|
|
if (cachedRestore) {
|
|
expect(calls).not.toContain("/artifacts");
|
|
}
|
|
expect(existsSync(join(root, "publication-observations.json"))).toBe(false);
|
|
} else {
|
|
expect(result.status, result.stderr).not.toBe(0);
|
|
expect(result.stdout).toBe("");
|
|
expect(existsSync(join(root, "publication-admission.json"))).toBe(false);
|
|
if (fault === "workflow-plan-mutated") {
|
|
expect(result.stderr).toContain("differs from its authenticated original");
|
|
}
|
|
}
|
|
if (planRestore && !cachedRestore) {
|
|
const workflow = parse(readFileSync(".github/workflows/full-release-validation.yml", "utf8"));
|
|
const upload = workflow.jobs.release_execution_plan.steps.find(
|
|
(step: { name: string }) => step.name === "Upload immutable release execution plan",
|
|
);
|
|
const outputs = existsSync(join(root, "outputs"))
|
|
? Object.fromEntries(
|
|
readFileSync(join(root, "outputs"), "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => line.split("=")),
|
|
)
|
|
: {};
|
|
const condition = String(upload.if)
|
|
.replace(/^\$\{\{|\}\}$/gu, "")
|
|
.trim();
|
|
const uploadSelected = runInNewContext(condition, {
|
|
always: () => true,
|
|
github: { run_attempt: fault === "workflow-plan-prewrite" ? 1 : 2 },
|
|
steps: { plan: { outputs: { sha256: "", source_parent_attempt: "", ...outputs } } },
|
|
});
|
|
if (uploadSelected && upload.with.overwrite === true) {
|
|
const replacement = makeStoredZip({ [entry]: readFileSync(cachedPlanPath, "utf8") });
|
|
fixtures.artifact = {
|
|
...artifact,
|
|
size_in_bytes: replacement.length,
|
|
digest: `sha256:${createHash("sha256").update(replacement).digest("hex")}`,
|
|
created_at: "2026-08-29T12:01:02.000Z",
|
|
};
|
|
fixtures.listing.artifacts = [fixtures.artifact];
|
|
writeFileSync(fixturesPath, JSON.stringify(fixtures));
|
|
writeFileSync(archivePath, replacement);
|
|
}
|
|
const subsequent = spawnSync(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"--eval",
|
|
`
|
|
import { restoreOriginalPublicationAdmission } from ${JSON.stringify(pathToFileURL(resolve(SCRIPT)).href)};
|
|
const restored = await restoreOriginalPublicationAdmission({ request: ${JSON.stringify(fixture.request)} });
|
|
process.stdout.write(JSON.stringify(restored.plan));
|
|
`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${root}:${process.env.PATH ?? ""}`,
|
|
OPENCLAW_GH_BIN: gh,
|
|
GH_TOKEN: "synthetic-evidence-token",
|
|
},
|
|
timeout: 10_000,
|
|
},
|
|
);
|
|
expect.soft(uploadSelected).toBe(false);
|
|
expect.soft(subsequent.status, subsequent.stderr).toBe(0);
|
|
if (subsequent.status === 0) {
|
|
expect(JSON.parse(subsequent.stdout)).toEqual(fixture.plan);
|
|
}
|
|
expect(readFileSync(archivePath)).toEqual(archive);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("GitHub API commands", () => {
|
|
it("budgets large artifact downloads for a conservative transfer rate", () => {
|
|
expect(artifactDownloadTimeoutMs(55 * 1024 * 1024)).toBeGreaterThan(60_000);
|
|
expect(artifactDownloadTimeoutMs(245 * 1024 * 1024)).toBeGreaterThan(15 * 60_000);
|
|
expect(() => artifactDownloadTimeoutMs(0)).toThrow("artifact download size is invalid");
|
|
});
|
|
|
|
it.skipIf(!hasUnzip)(
|
|
"routes evidence reads through cached GitHub and downloads through plain GitHub",
|
|
() => {
|
|
const root = mkdtempSync(join(tmpdir(), "release-ci-gh-routing-"));
|
|
const workflowSha = "0".repeat(40);
|
|
const targetSha = "8".repeat(40);
|
|
const verifierSha = "c".repeat(40);
|
|
const fixture = trustedMainPackageFixture({ manifestVersion: 3, targetSha, workflowSha });
|
|
const runId = fixture.runId;
|
|
const childRunId = String(fixture.childRun.id);
|
|
const candidateChild = expectDefined(
|
|
expectedChildDispatches(runId, 1, "main", 3).find(
|
|
(child) => child.manifestKey === "releaseChecksCandidate",
|
|
),
|
|
"candidate child",
|
|
);
|
|
fixture.parentJob.name = candidateChild.parentJobName;
|
|
fixture.childRun.display_title = candidateChild.displayTitle;
|
|
fixture.childRun.conclusion = "success";
|
|
const composite = composeReleaseAttemptJobs(
|
|
[
|
|
{
|
|
jobs: [
|
|
{
|
|
name: "Run QA Lab live Discord lane",
|
|
status: "completed",
|
|
conclusion: "success",
|
|
},
|
|
{
|
|
name: "Run QA Lab parity lane (core)",
|
|
status: "completed",
|
|
conclusion: "success",
|
|
},
|
|
{
|
|
name: "cross_os_release_checks / Linux / packaged fresh",
|
|
status: "completed",
|
|
conclusion: "success",
|
|
},
|
|
],
|
|
runAttempt: 1,
|
|
},
|
|
],
|
|
{ effectiveRunAttempt: 1, plannedRunAttempt: 1 },
|
|
);
|
|
const childEvidence = {
|
|
releaseChecksCandidate: {
|
|
compositeJobsSha256: composite.sha256,
|
|
dispatchActor: "github-actions[bot]",
|
|
effectiveRunAttempt: 1,
|
|
jobs: composite.jobs,
|
|
observedRunAttempts: [1],
|
|
plannedRunAttempt: 1,
|
|
repository: "openclaw/openclaw",
|
|
runId: childRunId,
|
|
triggeringActor: "github-actions[bot]",
|
|
},
|
|
};
|
|
Object.assign(fixture.manifest, {
|
|
advisoryJobs: [],
|
|
childEvidence,
|
|
childRuns: {
|
|
releaseChecksCandidate: childRunId,
|
|
normalCi: "",
|
|
npmTelegram: "",
|
|
pluginPrereleaseIndependent: "",
|
|
pluginPrereleaseCandidate: "",
|
|
releaseChecksIndependent: "",
|
|
},
|
|
version: 4,
|
|
});
|
|
const artifactId = fixture.artifact.id;
|
|
const archive = makeStoredZip({
|
|
[MANIFEST_ARTIFACT_ENTRY]: JSON.stringify(fixture.manifest),
|
|
});
|
|
const archivePath = join(root, "manifest.zip");
|
|
const fixturesPath = join(root, "fixtures.json");
|
|
const shimLog = join(root, "shim.log");
|
|
const plainLog = join(root, "plain.log");
|
|
const shimGh = join(root, "gh");
|
|
const plainGh = join(root, "plain-gh");
|
|
fixture.artifact.digest = artifactDigest(archive);
|
|
fixture.artifact.size_in_bytes = archive.length;
|
|
writeFileSync(archivePath, archive);
|
|
writeFileSync(
|
|
fixturesPath,
|
|
JSON.stringify({
|
|
artifact: fixture.artifact,
|
|
artifactList: { artifacts: [fixture.artifact] },
|
|
child: fixture.childRun,
|
|
jobLog: `TARGET_SHA: ${targetSha}\nDispatched: https://github.com/openclaw/openclaw/actions/runs/${childRunId} (attempt 1)`,
|
|
jobs: { jobs: [fixture.parentJob] },
|
|
lineage: { merge_base_commit: { sha: workflowSha }, status: "ahead" },
|
|
parent: fixture.parentRun,
|
|
parentView: fixture.parentView,
|
|
rate: { resources: { core: { limit: 5000, remaining: 4999, reset: 2_000_000_000 } } },
|
|
workflow: {
|
|
type: "file",
|
|
encoding: "base64",
|
|
path: ".github/workflows/full-release-validation.yml",
|
|
content: Buffer.from("name: Full Release Validation\n").toString("base64"),
|
|
size: Buffer.byteLength("name: Full Release Validation\n"),
|
|
sha: createHash("sha1")
|
|
.update(
|
|
`blob ${Buffer.byteLength("name: Full Release Validation\n")}\0name: Full Release Validation\n`,
|
|
)
|
|
.digest("hex"),
|
|
},
|
|
}),
|
|
);
|
|
writeFileSync(
|
|
shimGh,
|
|
`#!/usr/bin/env node
|
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
const args = process.argv.slice(2);
|
|
appendFileSync(process.env.SHIM_LOG, JSON.stringify(args) + "\\n");
|
|
const fixtures = JSON.parse(readFileSync(process.env.FIXTURES, "utf8"));
|
|
const endpoint = args[1] ?? "";
|
|
let output;
|
|
if (args[0] === "run" && args[1] === "view") output = fixtures.parentView;
|
|
else if (args[0] === "auth" && args[1] === "token") output = "wrapper-only-token";
|
|
else if (endpoint === "rate_limit") output = fixtures.rate;
|
|
else if (endpoint === "repos/openclaw/openclaw/contents/.github/workflows/full-release-validation.yml?ref=${workflowSha}") output = fixtures.workflow;
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/runs/${runId}") output = fixtures.parent;
|
|
else if (endpoint.startsWith("repos/openclaw/openclaw/actions/runs/${runId}/artifacts?")) output = fixtures.artifactList;
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/artifacts/${artifactId}") output = fixtures.artifact;
|
|
else if (endpoint.startsWith("repos/openclaw/openclaw/actions/runs/${runId}/jobs?")) output = fixtures.jobs;
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/runs/${childRunId}") output = fixtures.child;
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/jobs/${fixture.parentJob.id}/logs") output = fixtures.jobLog;
|
|
else if (endpoint === "repos/openclaw/openclaw/compare/${workflowSha}...${verifierSha}?per_page=1&page=2") output = fixtures.lineage;
|
|
else { console.error("unexpected cached gh request: " + args.join(" ")); process.exit(43); }
|
|
process.stdout.write(typeof output === "string" ? output : JSON.stringify(output));
|
|
`,
|
|
);
|
|
writeFileSync(
|
|
plainGh,
|
|
`#!/usr/bin/env node
|
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
const args = process.argv.slice(2);
|
|
appendFileSync(process.env.PLAIN_LOG, JSON.stringify(args) + "\\n");
|
|
if (process.env.GH_TOKEN !== "wrapper-only-token") {
|
|
console.error("plain gh did not receive wrapper authentication");
|
|
process.exit(41);
|
|
}
|
|
if (args[0] !== "api" || args[1] !== "repos/openclaw/openclaw/actions/artifacts/${artifactId}/zip") {
|
|
console.error("plain gh used for evidence read: " + args.join(" "));
|
|
process.exit(42);
|
|
}
|
|
process.stdout.write(readFileSync(process.env.ARCHIVE));
|
|
`,
|
|
);
|
|
chmodSync(shimGh, 0o755);
|
|
chmodSync(plainGh, 0o755);
|
|
|
|
try {
|
|
const env: NodeJS.ProcessEnv = {
|
|
...process.env,
|
|
ARCHIVE: archivePath,
|
|
FIXTURES: fixturesPath,
|
|
OPENCLAW_GH_BIN: plainGh,
|
|
PATH: `${root}:${process.env.PATH ?? ""}`,
|
|
PLAIN_LOG: plainLog,
|
|
SHIM_LOG: shimLog,
|
|
};
|
|
delete env.GH_ENTERPRISE_TOKEN;
|
|
delete env.GITHUB_ENTERPRISE_TOKEN;
|
|
delete env.GITHUB_TOKEN;
|
|
delete env.GH_TOKEN;
|
|
const lineageResult = spawnSync(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"--eval",
|
|
`import { createReleaseEvidenceClient } from ${JSON.stringify(pathToFileURL(resolve(SCRIPT)).href)};
|
|
process.stdout.write(JSON.stringify(createReleaseEvidenceClient("openclaw/openclaw").compareCommitLineage("${workflowSha}", "${verifierSha}")));`,
|
|
],
|
|
{ encoding: "utf8", env },
|
|
);
|
|
expect(lineageResult.status).toBe(0);
|
|
expect(JSON.parse(lineageResult.stdout)).toEqual({
|
|
merge_base_commit: { sha: workflowSha },
|
|
status: "ahead",
|
|
});
|
|
|
|
const result = spawnSync(process.execPath, [SCRIPT, runId], { encoding: "utf8", env });
|
|
|
|
expect(result.stderr).toBe("");
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
`child: ${childRunId} OpenClaw Release Checks completed/success`,
|
|
);
|
|
expect(result.stdout).not.toContain("Advisory lane failed");
|
|
const shimCalls = readFileSync(shimLog, "utf8");
|
|
const plainCalls = readFileSync(plainLog, "utf8");
|
|
expect(shimCalls).toContain('"run","view"');
|
|
expect(shimCalls).toContain('"auth","token"');
|
|
expect(shimCalls).toContain(`"repos/openclaw/openclaw/actions/runs/${runId}"`);
|
|
expect(shimCalls).toContain(
|
|
`"repos/openclaw/openclaw/compare/${workflowSha}...${verifierSha}?per_page=1&page=2"`,
|
|
);
|
|
expect(shimCalls).toContain(
|
|
JSON.stringify([
|
|
"api",
|
|
`repos/openclaw/openclaw/actions/jobs/${fixture.parentJob.id}/logs`,
|
|
"--allow-escape-sequences",
|
|
]),
|
|
);
|
|
expect(shimCalls).not.toContain(`/actions/artifacts/${artifactId}/zip`);
|
|
expect(plainCalls.trim()).toBe(
|
|
JSON.stringify(["api", `repos/openclaw/openclaw/actions/artifacts/${artifactId}/zip`]),
|
|
);
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
function runParentJobLogProbe(shimBody: string) {
|
|
const root = mkdtempSync(join(tmpdir(), "release-ci-job-log-"));
|
|
const shimLog = join(root, "shim.log");
|
|
const shimGh = join(root, "gh");
|
|
writeFileSync(
|
|
shimGh,
|
|
`#!/usr/bin/env node
|
|
import { appendFileSync } from "node:fs";
|
|
const args = process.argv.slice(2);
|
|
appendFileSync(process.env.SHIM_LOG, JSON.stringify(args) + "\\n");
|
|
${shimBody}
|
|
`,
|
|
);
|
|
chmodSync(shimGh, 0o755);
|
|
|
|
try {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"--eval",
|
|
`import { createReleaseEvidenceClient } from ${JSON.stringify(pathToFileURL(resolve(SCRIPT)).href)};
|
|
try {
|
|
process.stdout.write(await createReleaseEvidenceClient("owner/repo").getJobLog("123"));
|
|
} catch (error) {
|
|
process.stdout.write(JSON.stringify({
|
|
message: error instanceof Error ? error.message : String(error),
|
|
stderr: typeof error === "object" && error !== null && "stderr" in error
|
|
? String(error.stderr)
|
|
: "",
|
|
}));
|
|
process.exitCode = 17;
|
|
}`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
PATH: `${root}:${process.env.PATH ?? ""}`,
|
|
SHIM_LOG: shimLog,
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
calls: readFileSync(shimLog, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line)),
|
|
result,
|
|
};
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
describe("parent job log compatibility", () => {
|
|
const flaggedArgs = ["api", "repos/owner/repo/actions/jobs/123/logs", "--allow-escape-sequences"];
|
|
const legacyArgs = ["api", "repos/owner/repo/actions/jobs/123/logs"];
|
|
|
|
it("retries once without the flag for the exact legacy gh error", () => {
|
|
const { calls, result } = runParentJobLogProbe(`
|
|
if (args.includes("--allow-escape-sequences")) {
|
|
process.stderr.write("unknown flag: --allow-escape-sequences\\n\\nUsage: gh api <endpoint> [flags]\\n");
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write("\\u001b[31mlegacy log\\u001b[0m");
|
|
`);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(result.stdout).toBe("\u001b[31mlegacy log\u001b[0m");
|
|
expect(calls).toEqual([flaggedArgs, legacyArgs]);
|
|
});
|
|
|
|
it("propagates unrelated errors without retrying", () => {
|
|
const { calls, result } = runParentJobLogProbe(`
|
|
process.stderr.write("error: unknown flag: --allow-escape-sequences\\n");
|
|
process.exit(1);
|
|
`);
|
|
|
|
expect(result.status).toBe(17);
|
|
expect(result.stderr).toBe("");
|
|
expect(JSON.parse(result.stdout)).toEqual(
|
|
expect.objectContaining({
|
|
message: expect.stringContaining("Command failed: gh"),
|
|
stderr: "error: unknown flag: --allow-escape-sequences\n",
|
|
}),
|
|
);
|
|
expect(calls).toEqual([flaggedArgs]);
|
|
});
|
|
});
|
|
|
|
describe("runReleaseCiGh", () => {
|
|
it("bounds each GitHub lookup with a timeout and SIGKILL", () => {
|
|
const execFileSyncImpl = vi.fn(() => "result");
|
|
|
|
expect(
|
|
runReleaseCiGh(["api", "repos/openclaw/openclaw/actions/runs/1"], { execFileSyncImpl }),
|
|
).toBe("result");
|
|
expect(execFileSyncImpl).toHaveBeenCalledOnce();
|
|
expect(execFileSyncImpl).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
["api", "repos/openclaw/openclaw/actions/runs/1"],
|
|
expect.objectContaining({
|
|
encoding: "utf8",
|
|
killSignal: "SIGKILL",
|
|
timeout: 60_000,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Release execution plan artifact reads", () => {
|
|
it("treats GitHub CLI 2.93 missing named artifacts as unavailable", () => {
|
|
const root = tempDirs.make("release-plan-missing-artifact-");
|
|
const ghPath = join(root, "gh");
|
|
writeFileSync(
|
|
ghPath,
|
|
`#!${process.execPath}
|
|
console.error("no artifact matches any of the names or patterns provided");
|
|
process.exit(1);
|
|
`,
|
|
);
|
|
chmodSync(ghPath, 0o755);
|
|
const previousPath = process.env.PATH;
|
|
process.env.PATH = `${root}:${previousPath ?? ""}`;
|
|
try {
|
|
expect(createReleaseEvidenceClient("openclaw/openclaw").loadExecutionPlan("123")).toBe(
|
|
undefined,
|
|
);
|
|
} finally {
|
|
if (previousPath === undefined) {
|
|
delete process.env.PATH;
|
|
} else {
|
|
process.env.PATH = previousPath;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Release Decision artifact polling", () => {
|
|
const parent = { attempt: 1, headSha: "a".repeat(40) };
|
|
|
|
it("treats GitHub CLI 2.93 missing named artifacts as unavailable", () => {
|
|
expect(
|
|
tryReadReleaseDecisionArtifact(parent, "123", "openclaw/openclaw", () => {
|
|
throw Object.assign(
|
|
new Error("no artifact matches any of the names or patterns provided"),
|
|
{
|
|
stderr: "no artifact matches any of the names or patterns provided",
|
|
},
|
|
);
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it.each(["HTTP 503: Server Error", "HTTP 403: secondary rate limit"])(
|
|
"treats transient download transport failure %s as unavailable this poll",
|
|
(message) => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
try {
|
|
expect(
|
|
tryReadReleaseDecisionArtifact(parent, "123", "openclaw/openclaw", () => {
|
|
throw Object.assign(new Error(message), { stderr: message });
|
|
}),
|
|
).toBeUndefined();
|
|
expect(warn).toHaveBeenCalledWith(
|
|
expect.stringContaining("release decision artifact unavailable this poll"),
|
|
);
|
|
} finally {
|
|
warn.mockRestore();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("keeps authentication and invocation failures hard", () => {
|
|
for (const message of [
|
|
"HTTP 401: Bad credentials",
|
|
"unknown flag: --name\nUsage: gh run download",
|
|
]) {
|
|
expect(() =>
|
|
tryReadReleaseDecisionArtifact(parent, "123", "openclaw/openclaw", () => {
|
|
throw Object.assign(new Error(message), { stderr: message });
|
|
}),
|
|
).toThrow("release decision artifact read failed");
|
|
}
|
|
});
|
|
});
|
|
|
|
function u16(value: number): Buffer {
|
|
const buffer = Buffer.alloc(2);
|
|
buffer.writeUInt16LE(value);
|
|
return buffer;
|
|
}
|
|
|
|
function u32(value: number): Buffer {
|
|
const buffer = Buffer.alloc(4);
|
|
buffer.writeUInt32LE(value);
|
|
return buffer;
|
|
}
|
|
|
|
function makeStoredZip(files: Record<string, string>): Buffer {
|
|
const localParts: Buffer[] = [];
|
|
const centralParts: Buffer[] = [];
|
|
let offset = 0;
|
|
|
|
for (const [name, contents] of Object.entries(files)) {
|
|
const nameBuffer = Buffer.from(name, "utf8");
|
|
const contentsBuffer = Buffer.from(contents, "utf8");
|
|
const checksum = crc32(contentsBuffer);
|
|
const localHeader = Buffer.concat([
|
|
u32(0x04034b50),
|
|
u16(20),
|
|
u16(0),
|
|
u16(0),
|
|
u16(0),
|
|
u16(0),
|
|
u32(checksum),
|
|
u32(contentsBuffer.length),
|
|
u32(contentsBuffer.length),
|
|
u16(nameBuffer.length),
|
|
u16(0),
|
|
nameBuffer,
|
|
]);
|
|
localParts.push(localHeader, contentsBuffer);
|
|
centralParts.push(
|
|
Buffer.concat([
|
|
u32(0x02014b50),
|
|
u16(20),
|
|
u16(20),
|
|
u16(0),
|
|
u16(0),
|
|
u16(0),
|
|
u16(0),
|
|
u32(checksum),
|
|
u32(contentsBuffer.length),
|
|
u32(contentsBuffer.length),
|
|
u16(nameBuffer.length),
|
|
u16(0),
|
|
u16(0),
|
|
u16(0),
|
|
u16(0),
|
|
u32((0o100644 << 16) >>> 0),
|
|
u32(offset),
|
|
nameBuffer,
|
|
]),
|
|
);
|
|
offset += localHeader.length + contentsBuffer.length;
|
|
}
|
|
|
|
const localData = Buffer.concat(localParts);
|
|
const centralDirectory = Buffer.concat(centralParts);
|
|
return Buffer.concat([
|
|
localData,
|
|
centralDirectory,
|
|
u32(0x06054b50),
|
|
u16(0),
|
|
u16(0),
|
|
u16(Object.keys(files).length),
|
|
u16(Object.keys(files).length),
|
|
u32(centralDirectory.length),
|
|
u32(localData.length),
|
|
u16(0),
|
|
]);
|
|
}
|
|
|
|
function artifactDigest(bytes: Buffer): string {
|
|
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
}
|
|
|
|
function rawManifest({
|
|
candidateBinding,
|
|
evidenceReuse,
|
|
rerunGroup = "all",
|
|
runId = "29090000000",
|
|
targetSha = "a".repeat(40),
|
|
version = 2,
|
|
workflowFullRef,
|
|
workflowRefType,
|
|
workflowSha,
|
|
}: {
|
|
candidateBinding?: unknown;
|
|
evidenceReuse?: unknown;
|
|
rerunGroup?: string;
|
|
runId?: string;
|
|
targetSha?: string;
|
|
version?: 2 | 3;
|
|
workflowFullRef?: string;
|
|
workflowRefType?: "branch" | "tag";
|
|
workflowSha?: string;
|
|
}): {
|
|
candidateBinding?: unknown;
|
|
childRuns: Record<string, string | { blocking: boolean; conclusion: string; runId: string }>;
|
|
controls: Record<string, unknown>;
|
|
evidenceReuse?: unknown;
|
|
releaseProfile: string;
|
|
rerunGroup: string;
|
|
runAttempt: string;
|
|
runId: string;
|
|
runReleaseSoak: string;
|
|
targetRef?: string;
|
|
targetSha: string;
|
|
validationInputs: Record<string, string>;
|
|
version: 2 | 3;
|
|
workflowFullRef?: string;
|
|
workflowName: string;
|
|
workflowRef: string;
|
|
workflowRefType?: "branch" | "tag";
|
|
workflowSha?: string;
|
|
} {
|
|
return {
|
|
...(candidateBinding === undefined ? {} : { candidateBinding }),
|
|
childRuns: {
|
|
normalCi: "101",
|
|
npmTelegram: "",
|
|
pluginPrerelease: "202",
|
|
productPerformance: { blocking: true, conclusion: "success", runId: "303" },
|
|
releaseChecks: "404",
|
|
},
|
|
controls: {
|
|
performanceBlocking: true,
|
|
performanceReportPublication: "artifact-only",
|
|
stableSoakRequired: false,
|
|
},
|
|
evidenceReuse,
|
|
releaseProfile: "beta",
|
|
rerunGroup,
|
|
runAttempt: "2",
|
|
runId,
|
|
runReleaseSoak: "false",
|
|
targetSha,
|
|
validationInputs: {
|
|
allowUnreleasedChangelog: "false",
|
|
codexPluginSpec: "",
|
|
crossOsSuiteFilter: "",
|
|
liveSuiteFilter: "",
|
|
mode: "direct",
|
|
npmTelegramPackageSpec: "",
|
|
npmTelegramProviderMode: "mock-openai",
|
|
npmTelegramScenario: "",
|
|
packageAcceptancePackageSpec: "",
|
|
provider: "openai",
|
|
releasePackageSpec: "",
|
|
skipPackageTelegramE2e: "false",
|
|
targetContextRef: "",
|
|
},
|
|
version,
|
|
workflowName: "Full Release Validation",
|
|
workflowRef: "main",
|
|
...(workflowSha ? { workflowSha } : {}),
|
|
...(version === 3
|
|
? {
|
|
workflowFullRef: workflowFullRef ?? "refs/heads/main",
|
|
workflowRefType: workflowRefType ?? "branch",
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
function trustedMainPackageFixture({
|
|
manifestVersion = 2,
|
|
parentPath = ".github/workflows/full-release-validation.yml",
|
|
targetSha = "8".repeat(40),
|
|
workflowFullRef,
|
|
workflowRef = "main",
|
|
workflowRefType,
|
|
workflowSha = "0".repeat(40),
|
|
}: {
|
|
manifestVersion?: 2 | 3;
|
|
parentPath?: string;
|
|
targetSha?: string;
|
|
workflowFullRef?: string;
|
|
workflowRef?: string;
|
|
workflowRefType?: "branch" | "tag";
|
|
workflowSha?: string;
|
|
} = {}) {
|
|
const runId = "29071366025";
|
|
const childRunId = "29071382629";
|
|
const manifest = rawManifest({
|
|
rerunGroup: "package",
|
|
runId,
|
|
targetSha,
|
|
version: manifestVersion,
|
|
workflowFullRef,
|
|
workflowRefType,
|
|
workflowSha,
|
|
});
|
|
manifest.childRuns = {
|
|
normalCi: "",
|
|
npmTelegram: "",
|
|
pluginPrerelease: "",
|
|
productPerformance: { blocking: true, conclusion: "", runId: "" },
|
|
releaseChecks: childRunId,
|
|
};
|
|
manifest.releaseProfile = "full";
|
|
manifest.runAttempt = "1";
|
|
manifest.runReleaseSoak = "true";
|
|
manifest.workflowRef = workflowRef;
|
|
|
|
const parentRun = {
|
|
conclusion: "success",
|
|
event: "workflow_dispatch",
|
|
head_branch: workflowRef,
|
|
head_sha: workflowSha,
|
|
html_url: `https://github.com/openclaw/openclaw/actions/runs/${runId}`,
|
|
id: Number(runId),
|
|
path: parentPath,
|
|
repository: { full_name: "openclaw/openclaw" },
|
|
run_attempt: 1,
|
|
status: "completed",
|
|
};
|
|
const parentView = {
|
|
attempt: 1,
|
|
conclusion: "success",
|
|
headBranch: workflowRef,
|
|
headSha: workflowSha,
|
|
jobs: [],
|
|
status: "completed",
|
|
url: parentRun.html_url,
|
|
};
|
|
const child = expectedChildDispatches(runId, 1, workflowRef).find(
|
|
(entry) => entry.manifestKey === "releaseChecks",
|
|
);
|
|
if (!child) {
|
|
throw new Error("missing release checks child fixture");
|
|
}
|
|
const parentJob = {
|
|
completed_at: "2026-07-10T01:10:00Z",
|
|
conclusion: "success",
|
|
id: 86293408710,
|
|
name: child.parentJobName,
|
|
run_attempt: 1,
|
|
started_at: "2026-07-10T01:00:00Z",
|
|
status: "completed",
|
|
steps: [],
|
|
};
|
|
const childRun = {
|
|
actor: { login: "github-actions[bot]" },
|
|
conclusion: "success",
|
|
display_title: child.displayTitle,
|
|
event: "workflow_dispatch",
|
|
head_branch: workflowRef,
|
|
head_sha: workflowSha,
|
|
html_url: `https://github.com/openclaw/openclaw/actions/runs/${childRunId}`,
|
|
id: Number(childRunId),
|
|
path: ".github/workflows/openclaw-release-checks.yml",
|
|
repository: { full_name: "openclaw/openclaw" },
|
|
run_attempt: 1,
|
|
status: "completed",
|
|
triggering_actor: { login: "github-actions[bot]" },
|
|
};
|
|
const artifact = {
|
|
digest: `sha256:${"9".repeat(64)}`,
|
|
expired: false,
|
|
id: 8220114429,
|
|
name: `full-release-validation-${runId}-1`,
|
|
size_in_bytes: 507,
|
|
workflow_run: {
|
|
head_branch: workflowRef,
|
|
head_sha: workflowSha,
|
|
id: Number(runId),
|
|
},
|
|
};
|
|
const compareCommits = (base: string, head: string) => {
|
|
expect(base).toBe(workflowSha);
|
|
return {
|
|
merge_base_commit: { sha: workflowSha },
|
|
status: base === head ? "identical" : "ahead",
|
|
};
|
|
};
|
|
const client = {
|
|
getWorkflowSource: (_sha: string) => "name: Full Release Validation\n",
|
|
compareCommitLineage: compareCommits,
|
|
compareCommits,
|
|
getJobLog(jobId: number) {
|
|
expect(jobId).toBe(parentJob.id);
|
|
return [
|
|
`TARGET_SHA: ${targetSha}`,
|
|
`Dispatched openclaw-release-checks.yml: ${childRun.html_url} (attempt ${childRun.run_attempt})`,
|
|
].join("\n");
|
|
},
|
|
getParentJobs(requestedRunId: string) {
|
|
expect(requestedRunId).toBe(runId);
|
|
return [parentJob];
|
|
},
|
|
getRef(fullRef: string) {
|
|
return { object: { sha: workflowSha }, ref: fullRef };
|
|
},
|
|
getRun(requestedRunId: string) {
|
|
if (requestedRunId === runId) {
|
|
return parentRun;
|
|
}
|
|
if (requestedRunId === childRunId) {
|
|
return childRun;
|
|
}
|
|
throw new Error(`unexpected run: ${requestedRunId}`);
|
|
},
|
|
getRunView(requestedRunId: string) {
|
|
expect(requestedRunId).toBe(runId);
|
|
return parentView;
|
|
},
|
|
loadManifest(requestedRunId: string, requestedRunAttempt: number) {
|
|
expect(requestedRunId).toBe(runId);
|
|
expect(requestedRunAttempt).toBe(1);
|
|
return { artifact, manifest };
|
|
},
|
|
};
|
|
|
|
return {
|
|
artifact,
|
|
childRun,
|
|
client,
|
|
manifest,
|
|
parentJob,
|
|
parentRun,
|
|
parentView,
|
|
runId,
|
|
targetSha,
|
|
workflowSha,
|
|
};
|
|
}
|
|
|
|
type ReleaseCiWatchState = {
|
|
attempt: number;
|
|
conclusion: string;
|
|
jobs: Array<{ conclusion: string; name: string; status: string; url?: string }>;
|
|
status: string;
|
|
url?: string;
|
|
};
|
|
|
|
function verifyFixture(
|
|
fixture: Pick<
|
|
| ReturnType<typeof trustedMainPackageFixture>
|
|
| ReturnType<typeof trustedMainFullFixture>
|
|
| ReturnType<typeof trustedMainNpmFixture>
|
|
| ReturnType<typeof trustedMainChildReuseFixture>,
|
|
"runId" | "client"
|
|
>,
|
|
) {
|
|
return validateReleaseRunEvidence(
|
|
{
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
fixture.client,
|
|
);
|
|
}
|
|
|
|
function trustedMainFullFixture() {
|
|
const fixture = trustedMainPackageFixture({ manifestVersion: 3 });
|
|
const children = expectedChildDispatches(fixture.runId, 1, "main", 3).filter(
|
|
(child) => child.manifestKey !== "npmTelegram",
|
|
);
|
|
const runs = children.map((child, index) => ({
|
|
...fixture.childRun,
|
|
display_title: child.displayTitle,
|
|
id: 101 + index,
|
|
path: `.github/workflows/${child.workflow}`,
|
|
}));
|
|
const jobs = children.map((child, index) => ({
|
|
...fixture.parentJob,
|
|
id: 201 + index,
|
|
name: child.parentJobName,
|
|
}));
|
|
const manifest = {
|
|
...fixture.manifest,
|
|
childRuns: {
|
|
...fixture.manifest.childRuns,
|
|
...Object.fromEntries(
|
|
children.map((child, index) => {
|
|
const runId = String(expectDefined(runs[index], "child run").id);
|
|
return [
|
|
child.manifestKey,
|
|
child.manifestKey === "productPerformance"
|
|
? { blocking: true, conclusion: "success", runId }
|
|
: runId,
|
|
];
|
|
}),
|
|
),
|
|
},
|
|
rerunGroup: "all",
|
|
version: 4,
|
|
};
|
|
const client = {
|
|
...fixture.client,
|
|
getJobLog: vi.fn((jobId: number) => {
|
|
const index = jobs.findIndex((job) => job.id === jobId);
|
|
const child = expectDefined(children[index], "dispatch child");
|
|
const run = expectDefined(runs[index], "child run");
|
|
return `TARGET_SHA: ${fixture.targetSha}\n-f publish_reports=false\nDispatched ${child.workflow}: https://github.com/openclaw/openclaw/actions/runs/${run.id} (attempt 1)`;
|
|
}),
|
|
getParentJobs: vi.fn((runId: string) =>
|
|
runId === fixture.runId
|
|
? jobs
|
|
: [{ ...fixture.parentJob, name: "Verify artifact-only report mode" }],
|
|
),
|
|
getRun: vi.fn((runId: string) =>
|
|
runId === fixture.runId
|
|
? fixture.parentRun
|
|
: expectDefined(
|
|
runs.find((run) => String(run.id) === runId),
|
|
"child run",
|
|
),
|
|
),
|
|
loadExecutionPlan: vi.fn(() => undefined),
|
|
loadManifest: () => ({ artifact: fixture.artifact, manifest }),
|
|
};
|
|
return { ...fixture, client, manifest, runs };
|
|
}
|
|
|
|
function trustedMainNpmFixture(releaseProfile: "beta" | "stable" = "beta") {
|
|
const fixture = trustedMainFullFixture();
|
|
const beta = releaseProfile === "beta";
|
|
const coveragePolicy = beta ? "npm-beta-v1" : "npm-stable-v1";
|
|
const targetVersion = beta ? "2026.8.28-beta.1" : "2026.8.28";
|
|
Object.assign(fixture.manifest, { releaseProfile, runReleaseSoak: String(!beta) });
|
|
Object.assign(fixture.manifest.validationInputs, {
|
|
coveragePolicy,
|
|
skipPackageTelegramE2e: String(beta),
|
|
targetContextRef: "release/2026.8.28",
|
|
targetVersion,
|
|
});
|
|
fixture.manifest.controls.performanceBlocking = !beta;
|
|
fixture.manifest.controls.stableSoakRequired = !beta;
|
|
if (beta) {
|
|
fixture.manifest.childRuns.productPerformance = { blocking: false, conclusion: "", runId: "" };
|
|
}
|
|
const plannedChildren = expectedChildDispatches(fixture.runId, 1, "main", 3).map((child) => {
|
|
const run = fixture.runs.find((entry) => entry.display_title === child.displayTitle);
|
|
const selected =
|
|
child.manifestKey !== "npmTelegram" && !(beta && child.manifestKey === "productPerformance");
|
|
return {
|
|
displayTitle: child.displayTitle,
|
|
key: child.manifestKey,
|
|
required: selected,
|
|
result: selected ? "success" : "skipped",
|
|
runAttempt: selected ? 1 : null,
|
|
runId: selected ? String(expectDefined(run, "selected child run").id) : "",
|
|
selected,
|
|
source: "fresh",
|
|
url: selected ? expectDefined(run, "selected child run").html_url : "",
|
|
workflow: child.workflow,
|
|
workflowRef: "main",
|
|
workflowSha: fixture.workflowSha,
|
|
};
|
|
});
|
|
const executionPlan = buildReleaseExecutionPlanArtifact({
|
|
attemptEvidenceVersion: 3,
|
|
candidate: null,
|
|
children: plannedChildren,
|
|
coveragePolicy,
|
|
evidenceReuse: { requested: false },
|
|
expected: {
|
|
candidateRequest: buildFullReleaseCandidateRequest({
|
|
repository: "openclaw/openclaw",
|
|
targetSha: fixture.targetSha,
|
|
toolingSha: fixture.workflowSha,
|
|
releaseProfile,
|
|
releaseSoak: !beta,
|
|
upgradeSurvivorBaseline: "openclaw@latest",
|
|
upgradeSurvivorBaselines: "",
|
|
upgradeSurvivorScenarios: "",
|
|
allowFrozenTargetScenarioOmissions: false,
|
|
allowUnreleasedChangelog: false,
|
|
packagePublished: false,
|
|
sharedImagePolicy: "no-push-artifact",
|
|
}),
|
|
parentRunAttempt: 1,
|
|
parentRunId: fixture.runId,
|
|
repository: "openclaw/openclaw",
|
|
targetSha: fixture.targetSha,
|
|
workflowRef: "main",
|
|
workflowSha: fixture.workflowSha,
|
|
},
|
|
gates: [{ name: "Resolve target ref", required: true, result: "success" }],
|
|
releaseProfile,
|
|
rerunGroup: "all",
|
|
targetVersion,
|
|
trustedWorkflow: { fullRef: "refs/heads/main", ref: "main", sha: fixture.workflowSha },
|
|
});
|
|
const jobs = [{ ...fixture.parentJob, name: "test" }];
|
|
const performanceJobs = [{ ...fixture.parentJob, name: "Verify artifact-only report mode" }];
|
|
const jobsForChild = (key: string) => (key === "productPerformance" ? performanceJobs : jobs);
|
|
const manifest = Object.assign(fixture.manifest, {
|
|
childEvidence: Object.fromEntries(
|
|
plannedChildren
|
|
.filter((child) => child.selected)
|
|
.map((child) => {
|
|
const composite = composeReleaseAttemptJobs(
|
|
[{ jobs: jobsForChild(child.key), runAttempt: 1 }],
|
|
{ effectiveRunAttempt: 1, plannedRunAttempt: 1 },
|
|
);
|
|
return [
|
|
child.key,
|
|
{
|
|
compositeJobsSha256: composite.sha256,
|
|
dispatchActor: "github-actions[bot]",
|
|
effectiveRunAttempt: 1,
|
|
jobs: composite.jobs,
|
|
observedRunAttempts: [1],
|
|
plannedRunAttempt: 1,
|
|
repository: "openclaw/openclaw",
|
|
runId: child.runId,
|
|
triggeringActor: "github-actions[bot]",
|
|
},
|
|
];
|
|
}),
|
|
),
|
|
executionPlanSha256: executionPlan.sha256,
|
|
sourceParentRunAttempt: 1,
|
|
});
|
|
const originalLog = fixture.client.getJobLog;
|
|
const client = {
|
|
...fixture.client,
|
|
getJobLog: vi.fn(
|
|
(jobId: number) => `${originalLog(jobId)}\nCI_RELEASE_SCOPE: npm-${releaseProfile}`,
|
|
),
|
|
getRunAttemptJobs: vi.fn((runId: string) =>
|
|
jobsForChild(
|
|
expectDefined(
|
|
plannedChildren.find((child) => child.runId === runId),
|
|
"child",
|
|
).key,
|
|
),
|
|
),
|
|
loadExecutionPlan: vi.fn<() => ReleaseExecutionPlan | undefined>(() => executionPlan),
|
|
};
|
|
return { ...fixture, client, executionPlan, manifest };
|
|
}
|
|
|
|
function trustedMainChildReuseFixture() {
|
|
const fixture = trustedMainNpmFixture();
|
|
const child = expectDefined(
|
|
fixture.executionPlan.children.find((entry) => entry.key === "normalCi"),
|
|
"planned CI child",
|
|
);
|
|
const run = expectDefined(
|
|
fixture.runs.find((entry) => String(entry.id) === child.runId),
|
|
"CI run",
|
|
);
|
|
const repository = { id: 1, full_name: "openclaw/openclaw" };
|
|
Object.assign(run, {
|
|
display_title: "CI full-release-validation-77-1-ci",
|
|
head_sha: fixture.manifest.workflowSha,
|
|
repository,
|
|
head_repository: repository,
|
|
html_url: `https://github.com/openclaw/openclaw/actions/runs/${run.id}`,
|
|
});
|
|
Object.assign(child, {
|
|
source: "reused",
|
|
sourceParentAttempt: 1,
|
|
workflowSha: run.head_sha,
|
|
displayTitle: run.display_title,
|
|
url: run.html_url,
|
|
});
|
|
const jobs = [
|
|
{
|
|
...fixture.parentJob,
|
|
id: 501,
|
|
run_id: run.id,
|
|
head_sha: run.head_sha,
|
|
name: "node tests",
|
|
},
|
|
{
|
|
...fixture.parentJob,
|
|
id: 502,
|
|
run_id: run.id,
|
|
head_sha: run.head_sha,
|
|
name: FULL_RELEASE_CHILD_EVIDENCE_JOB,
|
|
steps: [
|
|
"Checkout trusted child evidence tooling",
|
|
"Seal exact child attempt evidence",
|
|
"Upload sealed child evidence",
|
|
].map((name) => ({ name, status: "completed", conclusion: "success" })),
|
|
},
|
|
];
|
|
const attempt = composeReleaseAttemptJobs([{ jobs, runAttempt: 1 }], {
|
|
effectiveRunAttempt: 1,
|
|
plannedRunAttempt: 1,
|
|
});
|
|
const composite = {
|
|
compositeJobsSha256: attempt.sha256,
|
|
dispatchActor: "github-actions[bot]",
|
|
effectiveRunAttempt: 1,
|
|
jobs: attempt.jobs,
|
|
observedRunAttempts: [1],
|
|
plannedRunAttempt: 1,
|
|
repository: repository.full_name,
|
|
runId: String(run.id),
|
|
triggeringActor: "github-actions[bot]",
|
|
};
|
|
fixture.manifest.childEvidence.normalCi = composite;
|
|
const workload = {
|
|
...composite,
|
|
jobs: composite.jobs.filter((job) => job.name !== FULL_RELEASE_CHILD_EVIDENCE_JOB),
|
|
};
|
|
workload.compositeJobsSha256 = releaseCompositeJobsSha256(workload);
|
|
const inputs = releaseChildDispatchInputs(readFileSync(".github/workflows/ci.yml", "utf8"), [
|
|
"-f",
|
|
`target_ref=${fixture.targetSha}`,
|
|
"-f",
|
|
"release_scope=npm-beta",
|
|
]);
|
|
const payload = {
|
|
...workload,
|
|
schema: "openclaw.full-release-child-evidence/v1",
|
|
role: "normalCi",
|
|
targetSha: fixture.targetSha,
|
|
workflowSha: run.head_sha,
|
|
workflowRef: run.head_branch,
|
|
workflowPath: run.path,
|
|
displayTitle: run.display_title,
|
|
dispatchId: "full-release-validation-77-1-ci",
|
|
sourceParentRunId: "77",
|
|
sourceParentAttempt: 1,
|
|
workloadConclusion: "success",
|
|
inputs: Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== "")),
|
|
publisher: { jobId: "502", jobName: FULL_RELEASE_CHILD_EVIDENCE_JOB },
|
|
};
|
|
const receiptSha256 = createHash("sha256")
|
|
.update(JSON.stringify(canonicalizeJsonValue(payload)))
|
|
.digest("hex");
|
|
const archive = makeStoredZip({
|
|
"full-release-child-evidence.json": JSON.stringify({ ...payload, sha256: receiptSha256 }),
|
|
});
|
|
const artifact = {
|
|
id: 503,
|
|
name: `full-release-child-evidence-${fixture.targetSha}-normalCi-${run.id}-1`,
|
|
digest: artifactDigest(archive),
|
|
expired: false,
|
|
expires_at: "2027-01-01T00:00:00Z",
|
|
size_in_bytes: archive.length,
|
|
workflow_run: { id: run.id, head_sha: run.head_sha, repository_id: 1, head_repository_id: 1 },
|
|
};
|
|
const selection = {
|
|
repository: repository.full_name,
|
|
targetSha: fixture.targetSha,
|
|
role: "normalCi",
|
|
runId: String(run.id),
|
|
runAttempt: 1,
|
|
workflowSha: run.head_sha,
|
|
workflowRef: run.head_branch,
|
|
displayTitle: run.display_title,
|
|
sourceParentRunId: "77",
|
|
sourceParentAttempt: 1,
|
|
url: run.html_url,
|
|
inputs,
|
|
receiptSha256,
|
|
artifact: {
|
|
id: String(artifact.id),
|
|
name: artifact.name,
|
|
digest: artifact.digest,
|
|
expiresAt: artifact.expires_at,
|
|
sizeInBytes: artifact.size_in_bytes,
|
|
},
|
|
};
|
|
Object.assign(fixture.executionPlan, { childReuse: { normalCi: selection } });
|
|
fixture.executionPlan.sha256 = releaseExecutionPlanSha256(fixture.executionPlan);
|
|
fixture.manifest.executionPlanSha256 = fixture.executionPlan.sha256;
|
|
const origin = {
|
|
...fixture.parentRun,
|
|
id: 77,
|
|
head_sha: run.head_sha,
|
|
head_branch: run.head_branch,
|
|
conclusion: "failure" as string | null,
|
|
status: "completed",
|
|
repository,
|
|
head_repository: repository,
|
|
};
|
|
const github = vi.fn(async (endpoint: string) => {
|
|
if (endpoint === `actions/runs/${run.id}`) {
|
|
return run;
|
|
}
|
|
if (endpoint === `compare/${run.head_sha}...main?per_page=1`) {
|
|
return { status: "ahead", merge_base_commit: { sha: run.head_sha } };
|
|
}
|
|
if (endpoint === `actions/artifacts/${artifact.id}`) {
|
|
return artifact;
|
|
}
|
|
if (endpoint === `actions/runs/${run.id}/attempts/1/jobs?per_page=100&page=1`) {
|
|
return { total_count: jobs.length, jobs };
|
|
}
|
|
if (endpoint === "actions/runs/77/attempts/1") {
|
|
return origin;
|
|
}
|
|
throw new Error(`Unexpected child evidence endpoint: ${endpoint}`);
|
|
});
|
|
const adoptionLog = [
|
|
`TARGET_SHA: ${fixture.targetSha}`,
|
|
"CI_RELEASE_SCOPE: npm-beta",
|
|
`FRV_CHILD_REUSE_SHA256=${releaseChildReuseSha256(selection)}`,
|
|
`Reused ci.yml: ${run.html_url} (attempt 1)`,
|
|
].join("\n");
|
|
const client = {
|
|
...fixture.client,
|
|
validateChildReuse: (
|
|
selected: Parameters<typeof validateReusableReleaseChild>[0],
|
|
request: Parameters<typeof validateReusableReleaseChild>[1],
|
|
) =>
|
|
validateReusableReleaseChild(selected, request, {
|
|
github,
|
|
downloadArchive: async () => ({ artifactMetadata: artifact, archiveBytes: archive }),
|
|
now: Date.parse("2026-09-23T00:00:00Z"),
|
|
}),
|
|
getJobLog: vi.fn((jobId: number) =>
|
|
jobId === 201 ? adoptionLog : fixture.client.getJobLog(jobId),
|
|
),
|
|
getRunAttemptJobs: (runId: string) =>
|
|
runId === String(run.id) ? jobs : fixture.client.getRunAttemptJobs(runId),
|
|
};
|
|
return { ...fixture, client, origin, selection, run, artifact, github };
|
|
}
|
|
|
|
function createReleaseCiWatchFixture(states: ReleaseCiWatchState[]) {
|
|
const root = mkdtempSync(join(tmpdir(), "release-ci-watch-"));
|
|
const callsPath = join(root, "calls.jsonl");
|
|
const ghPath = join(root, "gh");
|
|
const indexPath = join(root, "index.txt");
|
|
const preloadPath = join(root, "immediate-timers.mjs");
|
|
const fixture = trustedMainPackageFixture();
|
|
const { runId } = fixture;
|
|
const parent = { ...fixture.parentRun, conclusion: null, status: "in_progress" };
|
|
const parentView = { ...fixture.parentView, conclusion: "", jobs: [], status: "in_progress" };
|
|
writeFileSync(callsPath, "");
|
|
writeFileSync(indexPath, "0");
|
|
writeFileSync(
|
|
ghPath,
|
|
`#!${process.execPath}
|
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
const args = process.argv.slice(2);
|
|
appendFileSync(process.env.RELEASE_CI_WATCH_CALLS, JSON.stringify(args) + "\\n");
|
|
const endpoint = args[1] ?? "";
|
|
const states = ${JSON.stringify(states)};
|
|
let output;
|
|
if (args[0] === "run" && args[1] === "view") {
|
|
if (args[args.indexOf("--json") + 1] === "status,conclusion,attempt,headSha,jobs") {
|
|
const index = Number(readFileSync(process.env.RELEASE_CI_WATCH_INDEX, "utf8"));
|
|
output = { headSha: ${JSON.stringify(fixture.workflowSha)}, ...states[Math.min(index, states.length - 1)] };
|
|
writeFileSync(process.env.RELEASE_CI_WATCH_INDEX, String(index + 1));
|
|
} else output = ${JSON.stringify(parentView)};
|
|
} else if (args[0] === "run" && args[1] === "download") {
|
|
const dir = args[args.indexOf("--dir") + 1];
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(dir + "/full-release-decision.json", JSON.stringify({
|
|
version: 2,
|
|
kind: "openclaw.full-release-decision",
|
|
mode: "decision",
|
|
parentRunId: ${JSON.stringify(runId)},
|
|
parentRunAttempt: 1,
|
|
sourceParentRunAttempt: 1,
|
|
workflowRef: "main",
|
|
workflowSha: ${JSON.stringify(fixture.workflowSha)},
|
|
targetSha: ${JSON.stringify(fixture.targetSha)},
|
|
releaseProfile: "stable",
|
|
rerunGroup: "all",
|
|
executionPlanSha256: "${"d".repeat(64)}",
|
|
state: "blocked_diagnostics_running",
|
|
activeRunIds: ["101"],
|
|
blockers: [{ child: "normalCi", job: "test", conclusion: "failure", runId: "101", url: "https://example.invalid/job" }],
|
|
errors: [],
|
|
cancellation: { requested: false, cancelledRunIds: [] },
|
|
plan: [{ key: "normalCi", workflow: "ci.yml", displayTitle: "CI", dispatchName: "Dispatch CI", required: true, selected: true, source: "fresh", result: "success", runId: "101", runAttempt: 1, url: "https://example.invalid/run", workflowRef: "main", workflowSha: ${JSON.stringify(fixture.workflowSha)} }],
|
|
children: {}
|
|
}));
|
|
process.exit(0);
|
|
} else if (endpoint === "rate_limit") output = { resources: { core: { limit: 5000, remaining: 4999, reset: 2_000_000_000 } } };
|
|
else if (endpoint === "repos/openclaw/openclaw/actions/runs/${runId}") output = ${JSON.stringify(parent)};
|
|
else if (endpoint.startsWith("repos/openclaw/openclaw/actions/runs/${runId}/artifacts?")) output = { artifacts: [] };
|
|
else { console.error("unexpected gh call: " + args.join(" ")); process.exit(43); }
|
|
process.stdout.write(JSON.stringify(output));
|
|
`,
|
|
);
|
|
writeFileSync(
|
|
preloadPath,
|
|
"globalThis.setTimeout = (callback, _delay, ...args) => { queueMicrotask(() => callback(...args)); return 0; };\n",
|
|
);
|
|
chmodSync(ghPath, 0o755);
|
|
const env = {
|
|
...process.env,
|
|
PATH: `${root}:${process.env.PATH ?? ""}`,
|
|
RELEASE_CI_WATCH_CALLS: callsPath,
|
|
RELEASE_CI_WATCH_INDEX: indexPath,
|
|
};
|
|
return {
|
|
cleanup: () => rmSync(root, { force: true, recursive: true }),
|
|
readCalls: (): string[][] =>
|
|
readFileSync(callsPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line) as string[]),
|
|
run: () =>
|
|
spawnSync(
|
|
process.execPath,
|
|
["--import", preloadPath, resolve(SCRIPT), runId, "--watch", "--interval", "1"],
|
|
{ encoding: "utf8", env, timeout: 20_000 },
|
|
),
|
|
runId,
|
|
};
|
|
}
|
|
|
|
describe("release CI summary child correlation", () => {
|
|
it("reports an early release blocker once while the diagnostic drain continues", () => {
|
|
const fixture = createReleaseCiWatchFixture([
|
|
{
|
|
attempt: 1,
|
|
conclusion: "",
|
|
jobs: [
|
|
{ conclusion: "failure", name: "Release Decision", status: "completed" },
|
|
{ conclusion: "", name: "Diagnostic Drain", status: "in_progress" },
|
|
],
|
|
status: "in_progress",
|
|
},
|
|
]);
|
|
try {
|
|
const result = fixture.run();
|
|
const calls = fixture.readCalls();
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("Full Release Validation state: blocked_diagnostics_running");
|
|
expect(result.stderr).toContain("Diagnostic Drain is still collecting terminal evidence");
|
|
expect(
|
|
calls.filter(
|
|
(args) =>
|
|
args[0] === "run" &&
|
|
args[args.indexOf("--json") + 1] === "status,conclusion,attempt,headSha,jobs",
|
|
),
|
|
).toHaveLength(1);
|
|
expect(calls.filter((args) => args[0] === "api" && args[1] === "rate_limit")).toHaveLength(1);
|
|
} finally {
|
|
fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
it("selects one immutable manifest artifact bound to the exact parent run", () => {
|
|
const { artifact, runId } = trustedMainPackageFixture();
|
|
const legacyArtifact = {
|
|
...artifact,
|
|
id: artifact.id + 1,
|
|
name: `full-release-validation-${runId}`,
|
|
};
|
|
expect(selectManifestArtifact([artifact], runId, 1)).toBe(artifact);
|
|
expect(selectManifestArtifact([legacyArtifact, artifact], runId, 1)).toBe(artifact);
|
|
expect(selectManifestArtifact([legacyArtifact], runId, 1)).toBe(legacyArtifact);
|
|
expect(validateManifestArtifactCompatibility(legacyArtifact, { version: 2 }, runId, 1)).toBe(
|
|
legacyArtifact,
|
|
);
|
|
expect(
|
|
selectManifestArtifact(
|
|
[{ ...artifact, workflow_run: { ...artifact.workflow_run, id: 1 } }],
|
|
runId,
|
|
1,
|
|
),
|
|
).toBeUndefined();
|
|
expect(() =>
|
|
selectManifestArtifact([artifact, { ...artifact, id: artifact.id + 1 }], runId, 1),
|
|
).toThrow("multiple release validation manifest artifacts");
|
|
expect(() =>
|
|
selectManifestArtifact(
|
|
[legacyArtifact, { ...legacyArtifact, id: legacyArtifact.id + 1 }],
|
|
runId,
|
|
1,
|
|
),
|
|
).toThrow("multiple legacy release validation manifest artifacts");
|
|
expect(() => selectManifestArtifact([legacyArtifact], runId, 2)).toThrow(
|
|
"legacy release validation manifest requires run attempt 1",
|
|
);
|
|
expect(() =>
|
|
validateManifestArtifactCompatibility(legacyArtifact, { version: 3 }, runId, 1),
|
|
).toThrow("legacy release validation manifest artifact is not compatible");
|
|
expect(selectManifestArtifact([artifact], runId, 2)).toBeUndefined();
|
|
expect(() => selectManifestArtifact([{ ...artifact, digest: undefined }], runId, 1)).toThrow(
|
|
"manifest artifact digest is invalid",
|
|
);
|
|
expect(() =>
|
|
validateManifestArtifactIdentity(
|
|
{ ...artifact, digest: `sha256:${"8".repeat(64)}` },
|
|
{
|
|
artifactDigest: artifact.digest,
|
|
artifactId: artifact.id,
|
|
runAttempt: 1,
|
|
runId,
|
|
},
|
|
),
|
|
).toThrow("manifest artifact identity mismatch");
|
|
});
|
|
|
|
it.skipIf(!hasUnzip)(
|
|
"hashes and safely streams one bounded manifest entry from the exact artifact ZIP",
|
|
() => {
|
|
const root = mkdtempSync(join(tmpdir(), "release-manifest-artifact-"));
|
|
try {
|
|
const archivePath = join(root, "manifest.zip");
|
|
const manifest = { runAttempt: 1, runId: "29071366025", evidence: "x".repeat(128 * 1024) };
|
|
const archive = makeStoredZip({
|
|
[MANIFEST_ARTIFACT_ENTRY]: JSON.stringify(manifest),
|
|
});
|
|
writeFileSync(archivePath, archive);
|
|
expect(readManifestArtifactArchive(archivePath, artifactDigest(archive))).toEqual(manifest);
|
|
expect(() => readManifestArtifactArchive(archivePath, `sha256:${"0".repeat(64)}`)).toThrow(
|
|
"artifact digest mismatch",
|
|
);
|
|
|
|
const extraEntryArchive = makeStoredZip({
|
|
[MANIFEST_ARTIFACT_ENTRY]: JSON.stringify(manifest),
|
|
"unexpected.json": "{}",
|
|
});
|
|
writeFileSync(archivePath, extraEntryArchive);
|
|
expect(() =>
|
|
readManifestArtifactArchive(archivePath, artifactDigest(extraEntryArchive)),
|
|
).toThrow(`must contain only ${MANIFEST_ARTIFACT_ENTRY}`);
|
|
|
|
const oversizedManifestArchive = makeStoredZip({
|
|
[MANIFEST_ARTIFACT_ENTRY]: "x".repeat(MAX_RELEASE_ARTIFACT_BYTES + 1),
|
|
});
|
|
writeFileSync(archivePath, oversizedManifestArchive);
|
|
expect(() =>
|
|
readManifestArtifactArchive(archivePath, artifactDigest(oversizedManifestArchive)),
|
|
).toThrow("artifact entry size is invalid");
|
|
|
|
const oversizedArchive = Buffer.alloc(MAX_RELEASE_ARTIFACT_BYTES + 8 * 1024 + 1);
|
|
writeFileSync(archivePath, oversizedArchive);
|
|
expect(() =>
|
|
readManifestArtifactArchive(archivePath, artifactDigest(oversizedArchive)),
|
|
).toThrow("artifact compressed size is invalid");
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
},
|
|
);
|
|
|
|
it("bridges only attempt-one manifest v2 artifacts with the legacy stable name", async () => {
|
|
const legacyV2 = trustedMainPackageFixture();
|
|
legacyV2.artifact.name = `full-release-validation-${legacyV2.runId}`;
|
|
expect((await verifyFixture(legacyV2)).root.artifact.name).toBe(legacyV2.artifact.name);
|
|
|
|
const legacyV3 = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
workflowSha: "a".repeat(40),
|
|
});
|
|
legacyV3.artifact.name = `full-release-validation-${legacyV3.runId}`;
|
|
await expect(verifyFixture(legacyV3)).rejects.toThrow(
|
|
"legacy release validation manifest artifact is not compatible",
|
|
);
|
|
});
|
|
|
|
it("continues a failed npm producer through the real release evidence verifier", async () => {
|
|
const fixture = trustedMainNpmFixture();
|
|
const repository = "openclaw/openclaw";
|
|
const producer = {
|
|
...fixture.parentRun,
|
|
id: 81,
|
|
head_repository: { full_name: repository },
|
|
path: ".github/workflows/full-release-artifacts.yml",
|
|
display_title: `Full Release Artifacts full-release-validation-${fixture.runId}-1-artifacts-npm`,
|
|
conclusion: "failure",
|
|
};
|
|
const originalParent = {
|
|
...fixture.parentRun,
|
|
display_title: "Full Release Validation",
|
|
conclusion: "failure",
|
|
};
|
|
Object.assign(fixture.parentRun, originalParent);
|
|
const resolveId = 901;
|
|
const npmId = 902;
|
|
const sourceJobs = fixture.client.getParentJobs(fixture.runId);
|
|
const sourceLog = fixture.client.getJobLog;
|
|
const getJobLog = async (id: number) => {
|
|
if (id === resolveId) {
|
|
return `RERUN_GROUP: all\nFAIL_FAST: false\nTARGET_SHA: ${fixture.targetSha}`;
|
|
}
|
|
if (id === npmId) {
|
|
return `TARGET_SHA: ${fixture.targetSha}\nDispatched full-release-artifacts.yml: https://github.com/${repository}/actions/runs/81 (attempt 1)`;
|
|
}
|
|
return sourceLog(id);
|
|
};
|
|
const getRun = async (id: string) =>
|
|
structuredClone(id === "81" ? producer : fixture.client.getRun(id));
|
|
const rerunFailed = vi.fn(async (id: string) => {
|
|
expect(id).toBe("81");
|
|
producer.run_attempt = 2;
|
|
producer.conclusion = "success";
|
|
});
|
|
const rerunParent = vi.fn(async () => {
|
|
fixture.parentRun.run_attempt = 2;
|
|
fixture.parentRun.conclusion = "success";
|
|
fixture.parentView.attempt = 2;
|
|
fixture.manifest.runAttempt = "2";
|
|
fixture.artifact.name = `full-release-validation-${fixture.runId}-2`;
|
|
fixture.client.getParentJobs.mockReturnValue([
|
|
...sourceJobs,
|
|
...sourceJobs.map((job) => ({
|
|
...job,
|
|
id: job.id + 1000,
|
|
run_attempt: 2,
|
|
conclusion: "skipped",
|
|
started_at: "2026-07-10T02:00:00Z",
|
|
completed_at: "2026-07-10T02:01:00Z",
|
|
})),
|
|
]);
|
|
});
|
|
const verify = vi.fn(
|
|
async (
|
|
runId: string,
|
|
_plan: unknown,
|
|
_deadline?: number,
|
|
expectedRunAttempts?: Record<string, number>,
|
|
) =>
|
|
validateReleaseRunEvidence(
|
|
{
|
|
runId,
|
|
expectedRunAttempts,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
fixture.client,
|
|
),
|
|
);
|
|
await expect(
|
|
continueFailed(fixture.executionPlan, fixture.runId, {
|
|
repository,
|
|
getRun,
|
|
getRunAttempt: async (id: string) => (id === fixture.runId ? originalParent : getRun(id)),
|
|
getAttemptJobs: async (id: string) => fixture.client.getRunAttemptJobs(id),
|
|
getParentJobs: async () => [
|
|
...sourceJobs,
|
|
...[
|
|
[resolveId, "Resolve target ref"],
|
|
[npmId, "Prepare release npm artifacts"],
|
|
].map(([id, name]) => ({
|
|
id,
|
|
name,
|
|
run_attempt: 1,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
})),
|
|
],
|
|
getJobLog,
|
|
getReleaseEvidenceClient: () => ({
|
|
...createReleaseEvidenceClient(repository),
|
|
getWorkflowSource: () => "node scripts/full-release-artifacts.mjs resolve",
|
|
}),
|
|
rerunFailed,
|
|
rerunParent,
|
|
verify,
|
|
}),
|
|
).resolves.toMatchObject({ action: "reran-parent" });
|
|
expect(rerunFailed).toHaveBeenCalledExactlyOnceWith("81");
|
|
expect(rerunParent).toHaveBeenCalledExactlyOnceWith(fixture.runId);
|
|
expect((await verify.mock.results[0]!.value).children).toHaveLength(5);
|
|
});
|
|
|
|
it.each(["deleted-manifest", "deleted-plan", "changed-plan", "deleted-publication"])(
|
|
"verifies source admission against the immutable workflow and plan: %s",
|
|
async (mutation) => {
|
|
const fixture = trustedMainNpmFixture();
|
|
const inputs = fixture.manifest.validationInputs;
|
|
const sourceAdmission = publicationSourceFixture(fixture, true);
|
|
Object.assign(fixture.executionPlan, { sourceAdmissionContract: "1", sourceAdmission });
|
|
fixture.executionPlan.sha256 = releaseExecutionPlanSha256(fixture.executionPlan);
|
|
const manifest = Object.assign(fixture.manifest, {
|
|
sourceAdmissionContract: "1",
|
|
sourceAdmission,
|
|
trustedWorkflow: fixture.executionPlan.trustedWorkflow,
|
|
executionPlanSha256: fixture.executionPlan.sha256,
|
|
});
|
|
Object.assign(inputs, publicationIntentInputs(sourceAdmission));
|
|
const client = {
|
|
...fixture.client,
|
|
getWorkflowSource: vi.fn((sha: string) => {
|
|
expect(sha).toBe(fixture.workflowSha);
|
|
return (
|
|
'env:\n FULL_RELEASE_SOURCE_ADMISSION_CONTRACT: "1"\n' +
|
|
(mutation === "deleted-publication"
|
|
? ' FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT: "1"\n'
|
|
: "")
|
|
);
|
|
}),
|
|
};
|
|
if (mutation === "deleted-manifest") {
|
|
Reflect.deleteProperty(manifest, "sourceAdmission");
|
|
Reflect.deleteProperty(manifest, "sourceAdmissionContract");
|
|
Reflect.deleteProperty(inputs, "validationPurpose");
|
|
Reflect.deleteProperty(inputs, "publicationSelectionJson");
|
|
} else if (mutation === "deleted-plan") {
|
|
delete fixture.executionPlan.sourceAdmission;
|
|
delete fixture.executionPlan.sourceAdmissionContract;
|
|
fixture.executionPlan.sha256 = releaseExecutionPlanSha256(fixture.executionPlan);
|
|
manifest.executionPlanSha256 = fixture.executionPlan.sha256;
|
|
} else if (mutation === "changed-plan") {
|
|
const { digest: _digest, ...changed } = sourceAdmission;
|
|
changed.publicationSelection = {
|
|
...expectDefined(sourceAdmission.publicationSelection, "publication selection"),
|
|
route: "prepared",
|
|
};
|
|
fixture.executionPlan.sourceAdmission = {
|
|
...changed,
|
|
digest: createHash("sha256").update(publicationSourceJson(changed)).digest("hex"),
|
|
};
|
|
fixture.executionPlan.sha256 = releaseExecutionPlanSha256(fixture.executionPlan);
|
|
manifest.executionPlanSha256 = fixture.executionPlan.sha256;
|
|
}
|
|
const result = validateReleaseRunEvidence(
|
|
{
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
client,
|
|
);
|
|
await expect(result).rejects.toThrow(/source admission|publication/u);
|
|
},
|
|
);
|
|
|
|
it("verifies mixed fresh and independently reused children from an in-progress parent", async () => {
|
|
const fixture = trustedMainChildReuseFixture();
|
|
fixture.origin.status = "in_progress";
|
|
fixture.origin.conclusion = null;
|
|
const evidence = await verifyFixture(fixture);
|
|
expect(evidence.valid).toBe(true);
|
|
expect(
|
|
evidence.children.find((child: { role: string }) => child.role === "normalCi"),
|
|
).toMatchObject({
|
|
runId: String(fixture.run.id),
|
|
workflowSha: fixture.manifest.workflowSha,
|
|
parentJobId: "201",
|
|
sourceParentRunId: "77",
|
|
sourceParentAttempt: 1,
|
|
dispatchNonce: "full-release-validation-77-1-ci",
|
|
});
|
|
expect(
|
|
evidence.children
|
|
.filter((child: { role: string }) => child.role !== "normalCi")
|
|
.every((child: { sourceParentRunId: string }) => child.sourceParentRunId === fixture.runId),
|
|
).toBe(true);
|
|
expect(fixture.github).toHaveBeenCalledWith("actions/runs/77/attempts/1");
|
|
});
|
|
|
|
it.each([
|
|
["missing-adoption-witness", "reuse adoption witness mismatch"],
|
|
["changed-composite", "manifest child composite evidence mismatch"],
|
|
["unsealed-selection", "execution plan artifact digest"],
|
|
])("rejects independently reused final evidence with %s", async (fault, message) => {
|
|
const fixture = trustedMainChildReuseFixture();
|
|
if (fault === "missing-adoption-witness") {
|
|
const readLog = fixture.client.getJobLog.getMockImplementation()!;
|
|
fixture.client.getJobLog.mockImplementation((id: number) =>
|
|
readLog(id).replace(/FRV_CHILD_REUSE_SHA256=[a-f0-9]+/u, ""),
|
|
);
|
|
}
|
|
if (fault === "changed-composite") {
|
|
const evidence = expectDefined(fixture.manifest.childEvidence.normalCi, "CI evidence");
|
|
expectDefined(evidence.jobs[0], "CI job").completedAt = "2026-07-10T01:11:00Z";
|
|
evidence.compositeJobsSha256 = releaseCompositeJobsSha256(evidence);
|
|
}
|
|
if (fault === "unsealed-selection") {
|
|
fixture.selection.inputs.target_ref = "d".repeat(40);
|
|
}
|
|
await expect(verifyFixture(fixture)).rejects.toThrow(message);
|
|
});
|
|
|
|
it("requires passing product performance in sealed npm stable evidence", async () => {
|
|
const fixture = trustedMainNpmFixture("stable");
|
|
const options = {
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
};
|
|
const evidence = await validateReleaseRunEvidence(options, fixture.client);
|
|
expect(evidence.children.map((child: { role: string }) => child.role).toSorted()).toEqual([
|
|
"normalCi",
|
|
"pluginPrereleaseCandidate",
|
|
"pluginPrereleaseIndependent",
|
|
"productPerformance",
|
|
"releaseChecksCandidate",
|
|
"releaseChecksIndependent",
|
|
]);
|
|
expect(evidence.runReleaseSoak).toBe(true);
|
|
expect(fixture.client.getRunAttemptJobs).toHaveBeenCalledTimes(6);
|
|
const performance = expectDefined(
|
|
fixture.runs.find((run) => run.path === ".github/workflows/openclaw-performance.yml"),
|
|
"performance child",
|
|
);
|
|
performance.conclusion = "failure";
|
|
await expect(validateReleaseRunEvidence(options, fixture.client)).rejects.toThrow(
|
|
"does not pass release policy",
|
|
);
|
|
});
|
|
|
|
it.each(["carried-guard", "newer-guard-failure", "earlier-publisher"])(
|
|
"verifies effective artifact-only performance evidence after a targeted retry: %s",
|
|
async (scenario) => {
|
|
const fixture = trustedMainNpmFixture("stable");
|
|
const performance = expectDefined(
|
|
fixture.runs.find((run) => run.path === ".github/workflows/openclaw-performance.yml"),
|
|
"performance child",
|
|
);
|
|
const runId = String(performance.id);
|
|
const guard = { ...fixture.parentJob, name: "Verify artifact-only report mode" };
|
|
const benchmark = { ...fixture.parentJob, name: "Run performance benchmark" };
|
|
const originalJobs = [
|
|
guard,
|
|
{ ...benchmark, conclusion: "failure" },
|
|
{
|
|
...fixture.parentJob,
|
|
name: "Publish mock provider report",
|
|
conclusion: scenario === "earlier-publisher" ? "success" : "skipped",
|
|
},
|
|
];
|
|
const retryJobs = [
|
|
{ ...benchmark, run_attempt: 2 },
|
|
...(scenario === "newer-guard-failure"
|
|
? [{ ...guard, conclusion: "failure", run_attempt: 2 }]
|
|
: []),
|
|
];
|
|
const composite = composeReleaseAttemptJobs(
|
|
[
|
|
{ jobs: originalJobs, runAttempt: 1 },
|
|
{ jobs: retryJobs, runAttempt: 2 },
|
|
],
|
|
{ effectiveRunAttempt: 2, plannedRunAttempt: 1 },
|
|
);
|
|
Object.assign(performance, {
|
|
run_attempt: 2,
|
|
triggering_actor: { login: "release-maintainer" },
|
|
});
|
|
Object.assign(
|
|
expectDefined(fixture.manifest.childEvidence.productPerformance, "performance evidence"),
|
|
{
|
|
compositeJobsSha256: composite.sha256,
|
|
effectiveRunAttempt: 2,
|
|
jobs: composite.jobs,
|
|
observedRunAttempts: [1, 2],
|
|
triggeringActor: performance.triggering_actor.login,
|
|
},
|
|
);
|
|
const getOriginalJobs = fixture.client.getRunAttemptJobs;
|
|
const result = validateReleaseRunEvidence(
|
|
{
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
{
|
|
...fixture.client,
|
|
getRunAttemptJobs: (childRunId: string, runAttempt: number) =>
|
|
childRunId === runId
|
|
? runAttempt === 1
|
|
? originalJobs
|
|
: retryJobs
|
|
: getOriginalJobs(childRunId),
|
|
},
|
|
);
|
|
if (scenario === "carried-guard") {
|
|
expect((await result).children).toContainEqual(
|
|
expect.objectContaining({
|
|
reportPublication: "artifact-only",
|
|
role: "productPerformance",
|
|
runAttempt: 2,
|
|
}),
|
|
);
|
|
} else {
|
|
await expect(result).rejects.toThrow(
|
|
scenario === "newer-guard-failure"
|
|
? "manifest child run does not pass release policy: OpenClaw Performance"
|
|
: "performance report publisher was not skipped",
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"context",
|
|
"soak-control",
|
|
"soak",
|
|
"missing-plan",
|
|
"performance-run",
|
|
"performance-composite",
|
|
])("rejects incomplete npm stable qualification: %s", async (drift) => {
|
|
const fixture = trustedMainNpmFixture("stable");
|
|
if (drift === "context") {
|
|
fixture.manifest.validationInputs.targetContextRef = "";
|
|
} else if (drift === "soak-control") {
|
|
fixture.manifest.controls.stableSoakRequired = false;
|
|
} else if (drift === "soak") {
|
|
fixture.manifest.runReleaseSoak = "false";
|
|
} else if (drift === "missing-plan") {
|
|
fixture.client.loadExecutionPlan.mockReturnValue(undefined);
|
|
} else if (drift === "performance-run") {
|
|
delete fixture.manifest.childRuns.productPerformance;
|
|
} else {
|
|
delete fixture.manifest.childEvidence.productPerformance;
|
|
}
|
|
await expect(verifyFixture(fixture)).rejects.toThrow(
|
|
drift === "performance-run"
|
|
? "execution plan and manifest child identity differ: OpenClaw Performance"
|
|
: drift === "performance-composite"
|
|
? "release validation manifest composite child set is invalid"
|
|
: undefined,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"missing-plan",
|
|
"missing-marker",
|
|
"wrong-target-version",
|
|
"full-ci",
|
|
"deferred-run",
|
|
"package-telegram",
|
|
])("rejects npm beta coverage drift: %s", async (drift) => {
|
|
const fixture = trustedMainNpmFixture();
|
|
if (drift === "missing-plan") {
|
|
fixture.client.loadExecutionPlan.mockReturnValue(undefined);
|
|
} else if (drift === "missing-marker") {
|
|
delete fixture.manifest.validationInputs.coveragePolicy;
|
|
} else if (drift === "wrong-target-version") {
|
|
fixture.manifest.validationInputs.targetVersion = "2026.8.28-beta.2";
|
|
} else if (drift === "full-ci") {
|
|
fixture.client.getJobLog.mockImplementation(
|
|
(jobId: number) =>
|
|
`TARGET_SHA: ${fixture.targetSha}\nCI_RELEASE_SCOPE: full\nDispatched ci.yml: https://github.com/openclaw/openclaw/actions/runs/${jobId - 100} (attempt 1)`,
|
|
);
|
|
} else if (drift === "package-telegram") {
|
|
fixture.manifest.validationInputs.skipPackageTelegramE2e = "false";
|
|
} else {
|
|
fixture.manifest.childRuns.productPerformance = {
|
|
blocking: false,
|
|
conclusion: "success",
|
|
runId: "106",
|
|
};
|
|
}
|
|
await expect(verifyFixture(fixture)).rejects.toThrow();
|
|
});
|
|
|
|
it.each(["inputs", "target"])(
|
|
"rejects an ineligible reuse %s before fetching child evidence",
|
|
async (mismatch) => {
|
|
const fixture = trustedMainFullFixture();
|
|
const reuseRequest = {
|
|
releaseProfile: "full",
|
|
runReleaseSoak: "true",
|
|
targetSha: fixture.targetSha,
|
|
validationInputs: { ...fixture.manifest.validationInputs },
|
|
};
|
|
if (mismatch === "inputs") {
|
|
reuseRequest.validationInputs.provider = "anthropic";
|
|
} else {
|
|
reuseRequest.targetSha = "7".repeat(40);
|
|
fixture.client.compareCommits = () => ({
|
|
files: [{ filename: "src/index.ts", status: "modified" }],
|
|
merge_base_commit: { sha: fixture.targetSha },
|
|
status: "ahead",
|
|
});
|
|
}
|
|
await expect(
|
|
validateReleaseRunEvidence(
|
|
{
|
|
reuseRequest,
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
fixture.client,
|
|
),
|
|
).rejects.toThrow(
|
|
mismatch === "target" ? "failed commit comparison" : "ineligible reuse candidate",
|
|
);
|
|
expect(fixture.client.getRun).toHaveBeenCalledExactlyOnceWith(fixture.runId);
|
|
expect(fixture.client.loadExecutionPlan).not.toHaveBeenCalled();
|
|
expect(fixture.client.getParentJobs).not.toHaveBeenCalled();
|
|
expect(fixture.client.getJobLog).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("collects independent child evidence concurrently and drains reads after failure", async () => {
|
|
const fixture = trustedMainFullFixture();
|
|
let active = 0;
|
|
let peak = 0;
|
|
let completed = 0;
|
|
const client = {
|
|
...fixture.client,
|
|
async getJobLog(jobId: number) {
|
|
active += 1;
|
|
peak = Math.max(peak, active);
|
|
await new Promise<void>((complete) => {
|
|
setImmediate(complete);
|
|
});
|
|
active -= 1;
|
|
completed += 1;
|
|
if (jobId === 201) {
|
|
throw new Error("dispatch log unavailable");
|
|
}
|
|
return fixture.client.getJobLog(jobId);
|
|
},
|
|
};
|
|
const validation = Promise.resolve().then(() =>
|
|
validateReleaseRunEvidence(
|
|
{
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
client,
|
|
),
|
|
);
|
|
await expect(validation).rejects.toThrow("dispatch log unavailable");
|
|
expect(peak).toBeGreaterThan(1);
|
|
expect(peak).toBeLessThanOrEqual(7);
|
|
expect(completed).toBe(6);
|
|
expect(active).toBe(0);
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"retains full child proof for eligible reuse (changelog=%s)",
|
|
async (changelog) => {
|
|
const fixture = trustedMainFullFixture();
|
|
fixture.client.compareCommits = () => ({
|
|
files: [{ filename: "CHANGELOG.md", status: "modified" }],
|
|
merge_base_commit: { sha: fixture.targetSha },
|
|
status: "ahead",
|
|
});
|
|
const options = {
|
|
reuseRequest: {
|
|
releaseProfile: fixture.manifest.releaseProfile,
|
|
runReleaseSoak: fixture.manifest.runReleaseSoak,
|
|
targetSha: changelog ? "7".repeat(40) : fixture.targetSha,
|
|
validationInputs: { ...fixture.manifest.validationInputs },
|
|
},
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
};
|
|
const evidence = await validateReleaseRunEvidence(options, fixture.client);
|
|
expect(evidence.valid).toBe(true);
|
|
expect(evidence.children).toHaveLength(6);
|
|
expect(fixture.client.getJobLog).toHaveBeenCalledTimes(6);
|
|
|
|
expectDefined(fixture.runs[0], "CI run").head_sha = "f".repeat(40);
|
|
await expect(validateReleaseRunEvidence(options, fixture.client)).rejects.toThrow(
|
|
"manifest child dispatch tuple mismatch",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("recomputes mixed-attempt evidence with an authenticated Telegram waiver", async () => {
|
|
const version = "2026.9.1";
|
|
|
|
const fixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
workflowSha: "a".repeat(40),
|
|
});
|
|
const manifest = fixture.manifest as typeof fixture.manifest & {
|
|
childEvidence: Record<
|
|
string,
|
|
{
|
|
compositeJobsSha256: string;
|
|
effectiveRunAttempt: number;
|
|
jobs: Array<{
|
|
acceptedRunAttempt: number;
|
|
completedAt: string;
|
|
conclusion: string;
|
|
name: string;
|
|
startedAt: string;
|
|
status: string;
|
|
url: string;
|
|
}>;
|
|
observedRunAttempts: number[];
|
|
plannedRunAttempt: number;
|
|
runId: string;
|
|
}
|
|
>;
|
|
executionPlanSha256: string;
|
|
sourceParentRunAttempt: number;
|
|
};
|
|
const client = fixture.client as typeof fixture.client & {
|
|
getRunAttemptJobs: (runId: string, runAttempt: number) => Array<Record<string, unknown>>;
|
|
loadExecutionPlan: () => Record<string, unknown>;
|
|
};
|
|
const waiver = version
|
|
? { telegramWaiver: `${version}-owner-approved`, targetVersion: version }
|
|
: {};
|
|
Object.assign(manifest.validationInputs, waiver);
|
|
const plannedChild = {
|
|
dispatchName: "Dispatch release checks",
|
|
displayTitle: fixture.childRun.display_title,
|
|
key: "releaseChecks",
|
|
required: true,
|
|
result: "success",
|
|
runAttempt: 1,
|
|
runId: String(fixture.childRun.id),
|
|
selected: true,
|
|
source: "fresh",
|
|
url: fixture.childRun.html_url,
|
|
workflow: "openclaw-release-checks.yml",
|
|
workflowRef: fixture.childRun.head_branch,
|
|
workflowSha: fixture.childRun.head_sha,
|
|
};
|
|
const executionPlan = buildReleaseExecutionPlanArtifact({
|
|
...waiver,
|
|
attemptEvidenceVersion: 2,
|
|
candidate: null,
|
|
children: [plannedChild],
|
|
evidenceReuse: { requested: false },
|
|
expected: {
|
|
candidateRequest: buildFullReleaseCandidateRequest({
|
|
repository: "openclaw/openclaw",
|
|
targetSha: fixture.targetSha,
|
|
toolingSha: fixture.workflowSha,
|
|
releaseProfile: "full",
|
|
releaseSoak: true,
|
|
upgradeSurvivorBaseline: "openclaw@latest",
|
|
upgradeSurvivorBaselines: "",
|
|
upgradeSurvivorScenarios: "reported-issues",
|
|
allowFrozenTargetScenarioOmissions: false,
|
|
allowUnreleasedChangelog: false,
|
|
packagePublished: false,
|
|
sharedImagePolicy: "no-push-artifact",
|
|
}),
|
|
parentRunAttempt: 1,
|
|
parentRunId: fixture.runId,
|
|
repository: "openclaw/openclaw",
|
|
targetSha: fixture.targetSha,
|
|
workflowRef: fixture.parentRun.head_branch,
|
|
workflowSha: fixture.workflowSha,
|
|
},
|
|
gates: [{ name: "Resolve target ref", required: true, result: "success" }],
|
|
releaseProfile: "full",
|
|
rerunGroup: "package",
|
|
trustedWorkflow: {
|
|
fullRef: "refs/heads/main",
|
|
ref: "main",
|
|
sha: fixture.workflowSha,
|
|
},
|
|
});
|
|
expect(executionPlan.candidate).toBeNull();
|
|
fixture.childRun.run_attempt = 2;
|
|
fixture.childRun.triggering_actor = { login: "release-operator" };
|
|
const firstAttemptJob = {
|
|
completed_at: "2026-08-22T00:01:00Z",
|
|
conclusion: "failure",
|
|
html_url: "https://example.invalid/jobs/test",
|
|
name: "test",
|
|
started_at: "2026-08-22T00:00:00Z",
|
|
status: "completed",
|
|
};
|
|
const secondAttemptJob = { ...firstAttemptJob, conclusion: "success" };
|
|
const compositeJobs = [
|
|
{
|
|
acceptedRunAttempt: 2,
|
|
completedAt: secondAttemptJob.completed_at,
|
|
conclusion: "success",
|
|
name: "test",
|
|
startedAt: secondAttemptJob.started_at,
|
|
status: "completed",
|
|
url: secondAttemptJob.html_url,
|
|
},
|
|
];
|
|
const releaseChecksEvidence = {
|
|
compositeJobsSha256: releaseCompositeJobsSha256({
|
|
effectiveRunAttempt: 2,
|
|
jobs: compositeJobs,
|
|
plannedRunAttempt: 1,
|
|
}),
|
|
dispatchActor: "github-actions[bot]",
|
|
effectiveRunAttempt: 2,
|
|
jobs: compositeJobs,
|
|
observedRunAttempts: [1, 2],
|
|
plannedRunAttempt: 1,
|
|
repository: "openclaw/openclaw",
|
|
runId: String(fixture.childRun.id),
|
|
triggeringActor: "release-operator",
|
|
};
|
|
manifest.executionPlanSha256 = executionPlan.sha256;
|
|
manifest.sourceParentRunAttempt = 1;
|
|
manifest.runAttempt = "2";
|
|
manifest.childEvidence = {
|
|
releaseChecks: releaseChecksEvidence,
|
|
};
|
|
fixture.parentRun.run_attempt = 2;
|
|
fixture.parentView.attempt = 2;
|
|
fixture.artifact.name = `full-release-validation-${fixture.runId}-2`;
|
|
const skippedParentJob = {
|
|
completed_at: "2026-08-22T00:02:00Z",
|
|
conclusion: "skipped",
|
|
id: fixture.parentJob.id + 1,
|
|
name: fixture.parentJob.name,
|
|
run_attempt: 2,
|
|
started_at: "2026-08-22T00:02:00Z",
|
|
status: "completed",
|
|
steps: [],
|
|
};
|
|
client.loadExecutionPlan = () => executionPlan;
|
|
client.loadManifest = (requestedRunId: string, requestedRunAttempt: number) => {
|
|
expect(requestedRunId).toBe(fixture.runId);
|
|
expect(requestedRunAttempt).toBe(2);
|
|
return { artifact: fixture.artifact, manifest };
|
|
};
|
|
client.getParentJobs = (requestedRunId: string) => {
|
|
expect(requestedRunId).toBe(fixture.runId);
|
|
return [fixture.parentJob, skippedParentJob];
|
|
};
|
|
client.getRunAttemptJobs = (_runId: string, attempt: number) =>
|
|
attempt === 1 ? [firstAttemptJob] : [secondAttemptJob];
|
|
fixture.client.getJobLog = (jobId: number) => {
|
|
expect(jobId).toBe(fixture.parentJob.id);
|
|
return [
|
|
`TARGET_SHA: ${fixture.targetSha}`,
|
|
`Dispatched openclaw-release-checks.yml: ${fixture.childRun.html_url} (attempt 1)`,
|
|
].join("\n");
|
|
};
|
|
|
|
const validate = (expectedRunAttempts?: Record<string, number>) =>
|
|
validateReleaseRunEvidence(
|
|
{
|
|
expectedRunAttempts,
|
|
repository: "openclaw/openclaw",
|
|
runId: fixture.runId,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
fixture.client,
|
|
);
|
|
const evidence = await validate();
|
|
expect(evidence.children).toEqual([
|
|
expect.objectContaining({
|
|
compositeJobsSha256: releaseChecksEvidence.compositeJobsSha256,
|
|
plannedRunAttempt: 1,
|
|
runAttempt: 2,
|
|
}),
|
|
]);
|
|
if (version) {
|
|
delete manifest.validationInputs.telegramWaiver;
|
|
await expect(validate()).rejects.toThrow(/Telegram waiver/u);
|
|
Object.assign(manifest.validationInputs, waiver);
|
|
client.loadExecutionPlan = () => undefined as never;
|
|
await expect(validate()).rejects.toThrow(/Telegram waiver/u);
|
|
client.loadExecutionPlan = () => executionPlan;
|
|
}
|
|
|
|
for (const [expectedRunAttempts, message] of [
|
|
[{ [fixture.runId]: 1, [String(fixture.childRun.id)]: 2 }, "parent run attempt changed"],
|
|
[{ [fixture.runId]: 2, [String(fixture.childRun.id)]: 1 }, "child run attempt changed"],
|
|
[{ [fixture.runId]: 2 }, "expected run attempts omitted"],
|
|
[{ [fixture.runId]: 2, [String(fixture.childRun.id)]: 2, "999": 1 }, "unvalidated run IDs"],
|
|
] as const) {
|
|
await expect(validate(expectedRunAttempts)).rejects.toThrow(message);
|
|
}
|
|
|
|
const staleJobs = [
|
|
{
|
|
acceptedRunAttempt: 1,
|
|
completedAt: firstAttemptJob.completed_at,
|
|
conclusion: firstAttemptJob.conclusion,
|
|
name: firstAttemptJob.name,
|
|
startedAt: firstAttemptJob.started_at,
|
|
status: firstAttemptJob.status,
|
|
url: firstAttemptJob.html_url,
|
|
},
|
|
];
|
|
const staleEvidence = {
|
|
compositeJobsSha256: releaseCompositeJobsSha256({
|
|
effectiveRunAttempt: 1,
|
|
jobs: staleJobs,
|
|
plannedRunAttempt: 1,
|
|
}),
|
|
dispatchActor: "github-actions[bot]",
|
|
effectiveRunAttempt: 1,
|
|
jobs: staleJobs,
|
|
observedRunAttempts: [1],
|
|
plannedRunAttempt: 1,
|
|
repository: "openclaw/openclaw",
|
|
runId: String(fixture.childRun.id),
|
|
triggeringActor: "github-actions[bot]",
|
|
};
|
|
manifest.childEvidence.releaseChecks = staleEvidence;
|
|
await expect(
|
|
validate({ [fixture.runId]: 2, [String(fixture.childRun.id)]: 2 }),
|
|
).rejects.toThrowError(
|
|
expect.objectContaining({
|
|
message: "successful parent manifest predates OpenClaw Release Checks attempt 2",
|
|
refreshable: true,
|
|
}),
|
|
);
|
|
|
|
manifest.childEvidence.releaseChecks = releaseChecksEvidence;
|
|
const loadManifest = client.loadManifest.bind(client);
|
|
client.loadManifest = () => undefined as never;
|
|
await expect(
|
|
validate({ [fixture.runId]: 2, [String(fixture.childRun.id)]: 2 }),
|
|
).rejects.toThrowError(
|
|
expect.objectContaining({
|
|
message: `successful parent run is missing its release validation manifest: ${fixture.runId}`,
|
|
refreshable: true,
|
|
}),
|
|
);
|
|
client.loadManifest = loadManifest;
|
|
|
|
releaseChecksEvidence.jobs[0]!.conclusion = "failure";
|
|
let malformedError: unknown;
|
|
try {
|
|
await validate();
|
|
} catch (error) {
|
|
malformedError = error;
|
|
}
|
|
expect(malformedError).toMatchObject({
|
|
message: expect.stringContaining("digest is invalid"),
|
|
});
|
|
expect(malformedError).not.toHaveProperty("refreshable");
|
|
|
|
releaseChecksEvidence.jobs[0]!.conclusion = "success";
|
|
fixture.childRun.actor = { login: "release-operator" };
|
|
await expect(validate()).rejects.toThrow("execution plan child dispatch tuple mismatch");
|
|
});
|
|
|
|
it("rejects a parent recovery that reruns a sealed child dispatch slot", () => {
|
|
const child = expectedChildDispatches("28717729503", 2, "main").find(
|
|
(entry) => entry.manifestKey === "releaseChecks",
|
|
);
|
|
if (!child) {
|
|
throw new Error("missing release checks fixture");
|
|
}
|
|
const parentManifest = { runAttempt: 2, runId: "28717729503" };
|
|
const parentJobs = [
|
|
{
|
|
completed_at: "2026-08-22T00:01:00Z",
|
|
conclusion: "success",
|
|
id: 900,
|
|
name: child.parentJobName,
|
|
run_attempt: 1,
|
|
started_at: "2026-08-22T00:00:00Z",
|
|
status: "completed",
|
|
steps: [],
|
|
},
|
|
{
|
|
completed_at: "2026-08-22T00:02:00Z",
|
|
conclusion: "success",
|
|
id: 901,
|
|
name: child.parentJobName,
|
|
run_attempt: 2,
|
|
started_at: "2026-08-22T00:01:00Z",
|
|
status: "completed",
|
|
steps: [],
|
|
},
|
|
];
|
|
|
|
expect(() =>
|
|
selectManifestParentJob(parentJobs, child, parentManifest, 1, {
|
|
requireSkippedCarryForward: true,
|
|
}),
|
|
).toThrow("manifest parent job was redispatched during recovery");
|
|
});
|
|
|
|
it("blocks selected cross-OS failures through canonical policy", async () => {
|
|
const releaseProfile = "stable";
|
|
const jobName = "cross_os_release_checks / Windows / packaged fresh";
|
|
|
|
const fixture = trustedMainPackageFixture();
|
|
fixture.manifest.releaseProfile = releaseProfile;
|
|
fixture.childRun.conclusion = "failure";
|
|
const originalClient = { ...fixture.client };
|
|
fixture.client.getParentJobs = (requestedRunId: string) =>
|
|
requestedRunId === String(fixture.childRun.id)
|
|
? [
|
|
{
|
|
completed_at: "2026-07-10T01:10:00Z",
|
|
conclusion: "failure",
|
|
id: 86293408711,
|
|
name: jobName,
|
|
run_attempt: 1,
|
|
started_at: "2026-07-10T01:00:00Z",
|
|
status: "completed",
|
|
steps: [],
|
|
},
|
|
{
|
|
completed_at: "2026-07-10T01:10:00Z",
|
|
conclusion: "success",
|
|
id: 86293408712,
|
|
name: "Verify release checks",
|
|
run_attempt: 1,
|
|
started_at: "2026-07-10T01:00:00Z",
|
|
status: "completed",
|
|
steps: [],
|
|
},
|
|
]
|
|
: originalClient.getParentJobs(requestedRunId);
|
|
|
|
await expect(verifyFixture(fixture)).rejects.toThrow("does not pass release policy");
|
|
});
|
|
|
|
it.each(["valid", "macos-failure", "cancelled-run", "forged-advisory", "omitted-advisory"])(
|
|
"authenticates Windows Node CI advisory evidence: %s",
|
|
async (scenario) => {
|
|
const fixture = trustedMainNpmFixture();
|
|
const selected = expectDefined(
|
|
fixture.executionPlan.children.find((child) => child.key === "normalCi"),
|
|
"normal CI child",
|
|
);
|
|
const run = expectDefined(
|
|
fixture.runs.find((candidate) => String(candidate.id) === selected.runId),
|
|
"normal CI run",
|
|
);
|
|
run.conclusion = scenario === "cancelled-run" ? "cancelled" : "failure";
|
|
const windowsJob = {
|
|
...fixture.parentJob,
|
|
name: "checks-windows-node-test-2",
|
|
conclusion: "failure",
|
|
html_url: `https://github.com/openclaw/openclaw/actions/runs/${selected.runId}/job/501`,
|
|
};
|
|
const jobs = [
|
|
windowsJob,
|
|
{ ...fixture.parentJob, name: "openclaw/ci-gate" },
|
|
...(scenario === "macos-failure"
|
|
? [{ ...fixture.parentJob, name: "macos-node-2", conclusion: "failure" }]
|
|
: []),
|
|
];
|
|
const composite = composeReleaseAttemptJobs([{ jobs, runAttempt: 1 }], {
|
|
effectiveRunAttempt: 1,
|
|
plannedRunAttempt: 1,
|
|
});
|
|
Object.assign(expectDefined(fixture.manifest.childEvidence.normalCi, "normal CI evidence"), {
|
|
jobs: composite.jobs,
|
|
compositeJobsSha256: composite.sha256,
|
|
});
|
|
const originalJobs = expectDefined(
|
|
fixture.client.getRunAttemptJobs.getMockImplementation(),
|
|
"live job reader",
|
|
);
|
|
fixture.client.getRunAttemptJobs.mockImplementation((runId) =>
|
|
runId === selected.runId ? jobs : originalJobs(runId),
|
|
);
|
|
const advisory = {
|
|
class: "windows-node-ci",
|
|
child: "normalCi",
|
|
job: windowsJob.name,
|
|
conclusion: "failure",
|
|
runId: selected.runId,
|
|
url: windowsJob.html_url,
|
|
};
|
|
if (scenario !== "omitted-advisory") {
|
|
Object.assign(fixture.manifest, {
|
|
advisoryJobs: [
|
|
scenario === "forged-advisory"
|
|
? { ...advisory, child: "releaseChecksCandidate" }
|
|
: advisory,
|
|
],
|
|
});
|
|
}
|
|
const validation = verifyFixture(fixture);
|
|
if (scenario === "valid") {
|
|
const evidence = await validation;
|
|
expect(evidence.valid).toBe(true);
|
|
expect(evidence.conclusions.allRequiredSucceeded).toBe(true);
|
|
expect(evidence.children).toContainEqual(
|
|
expect.objectContaining({
|
|
role: "normalCi",
|
|
conclusion: "failure",
|
|
policyPassed: true,
|
|
advisoryJobs: [advisory],
|
|
}),
|
|
);
|
|
expect(evidence.current.manifest).toMatchObject({
|
|
advisoryJobs: [advisory],
|
|
childEvidence: {
|
|
normalCi: {
|
|
jobs: expect.arrayContaining([
|
|
expect.objectContaining({ name: windowsJob.name, conclusion: "failure" }),
|
|
]),
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
await expect(validation).rejects.toThrow(
|
|
scenario === "forged-advisory" || scenario === "omitted-advisory"
|
|
? /advisory jobs differ/u
|
|
: /does not pass release policy/u,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(["", "ship"])(
|
|
"reads published empty retry metadata without granting a waiver (%s)",
|
|
async (laneWaiver) => {
|
|
const fixture = trustedMainNpmFixture();
|
|
const selected = expectDefined(
|
|
fixture.executionPlan.children.find((child) => child.key === "releaseChecksCandidate"),
|
|
"release checks child",
|
|
);
|
|
const run = expectDefined(
|
|
fixture.runs.find((candidateRun) => String(candidateRun.id) === selected.runId),
|
|
"release checks run",
|
|
);
|
|
run.conclusion = "failure";
|
|
const jobs = [
|
|
{
|
|
name: "cross_os_release_checks / Windows / packaged upgrade",
|
|
status: "completed",
|
|
conclusion: "failure",
|
|
},
|
|
{
|
|
name: "cross_os_release_checks / macOS / packaged fresh",
|
|
status: "completed",
|
|
conclusion: "success",
|
|
},
|
|
{ name: "Verify release checks", status: "completed", conclusion: "success" },
|
|
].map((job) => Object.assign({}, fixture.parentJob, job));
|
|
const composite = composeReleaseAttemptJobs([{ jobs, runAttempt: 1 }], {
|
|
effectiveRunAttempt: 1,
|
|
plannedRunAttempt: 1,
|
|
});
|
|
Object.assign(
|
|
expectDefined(
|
|
fixture.manifest.childEvidence.releaseChecksCandidate,
|
|
"release checks evidence",
|
|
),
|
|
{
|
|
jobs: composite.jobs,
|
|
compositeJobsSha256: composite.sha256,
|
|
},
|
|
);
|
|
const originalJobs = fixture.client.getRunAttemptJobs.getMockImplementation()!;
|
|
fixture.client.getRunAttemptJobs.mockImplementation((runId) =>
|
|
runId === selected.runId ? jobs : originalJobs(runId),
|
|
);
|
|
Object.assign(fixture.executionPlan, {
|
|
knownFlakyJobs: [],
|
|
...(laneWaiver ? { laneWaiver } : {}),
|
|
});
|
|
fixture.executionPlan.sha256 = releaseExecutionPlanSha256(fixture.executionPlan);
|
|
Object.assign(fixture.manifest.validationInputs, {
|
|
knownFlakyJobsJson: "[]",
|
|
...(laneWaiver ? { laneWaiver } : {}),
|
|
});
|
|
const manifest = Object.assign(fixture.manifest, {
|
|
knownFlakyJobs: [],
|
|
automaticRetries: [],
|
|
executionPlanSha256: fixture.executionPlan.sha256,
|
|
advisoryJobs: jobs.slice(0, 2).map(({ name, status, conclusion }) => ({
|
|
child: selected.key,
|
|
job: name,
|
|
status,
|
|
conclusion,
|
|
policy: "advisory",
|
|
})),
|
|
});
|
|
const before = JSON.stringify(manifest);
|
|
expect(() => validateParentManifest(manifest, { runId: fixture.runId })).toThrow(
|
|
laneWaiver ? "no longer accepted" : "advisory jobs differ",
|
|
);
|
|
await expect(verifyFixture(fixture)).rejects.toThrow();
|
|
expect(JSON.stringify(manifest)).toBe(before);
|
|
},
|
|
);
|
|
|
|
it("rejects a v3 producer dispatched from a tag named main", async () => {
|
|
const fixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
workflowFullRef: "refs/tags/main",
|
|
workflowRefType: "tag",
|
|
workflowSha: "a".repeat(40),
|
|
});
|
|
await expect(verifyFixture(fixture)).rejects.toThrow(
|
|
"producer workflow full ref is not trusted",
|
|
);
|
|
});
|
|
|
|
it("rejects a legacy producer outside the trusted main verifier lineage", async () => {
|
|
const fixture = trustedMainPackageFixture({ workflowSha: "a".repeat(40) });
|
|
fixture.client.compareCommitLineage = () => ({
|
|
merge_base_commit: { sha: "d".repeat(40) },
|
|
status: "diverged",
|
|
});
|
|
await expect(verifyFixture(fixture)).rejects.toThrow(
|
|
"producer is not on the trusted main verifier lineage",
|
|
);
|
|
});
|
|
|
|
it("rejects a candidate branch producer even when its SHA differs from the target", async () => {
|
|
const fixture = trustedMainPackageFixture({
|
|
targetSha: "8".repeat(40),
|
|
workflowRef: "release/2026.7.1",
|
|
workflowSha: "7".repeat(40),
|
|
});
|
|
await expect(
|
|
validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: fixture.runId,
|
|
trustedWorkflowRef: "main",
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
fixture.client,
|
|
),
|
|
).rejects.toThrow("producer must run from trusted workflow ref: main");
|
|
});
|
|
|
|
it("accepts canonical SHA-pinned v3 evidence on the trusted main lineage", async () => {
|
|
const workflowSha = "7".repeat(40);
|
|
const workflowRef = `release-ci/${workflowSha.slice(0, 12)}-1783705000000`;
|
|
const fixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
targetSha: "8".repeat(40),
|
|
workflowFullRef: `refs/heads/${workflowRef}`,
|
|
workflowRef,
|
|
workflowSha,
|
|
});
|
|
fixture.manifest.targetRef = fixture.targetSha;
|
|
|
|
expect((await verifyFixture(fixture)).root).toMatchObject({
|
|
workflowFullRef: `refs/heads/${workflowRef}`,
|
|
workflowRef,
|
|
workflowRefProof: "manifest-v3-sha-pinned-main-ancestry",
|
|
workflowSha,
|
|
});
|
|
});
|
|
|
|
it("accepts canonical SHA-pinned v3 evidence exactly bound to a protected tooling tag", async () => {
|
|
const workflowSha = "7".repeat(40);
|
|
const workflowRef = `release-ci/${workflowSha.slice(0, 12)}-1783705000000`;
|
|
const trustedWorkflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
const fixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
targetSha: "8".repeat(40),
|
|
workflowFullRef: `refs/heads/${workflowRef}`,
|
|
workflowRef,
|
|
workflowSha,
|
|
});
|
|
fixture.manifest.targetRef = fixture.targetSha;
|
|
|
|
expect(
|
|
await validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: fixture.runId,
|
|
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
|
|
trustedWorkflowRef,
|
|
trustedWorkflowSha: workflowSha,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
fixture.client,
|
|
),
|
|
).toMatchObject({
|
|
producerOnTrustedMainLineage: false,
|
|
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
|
|
trustedWorkflowRef,
|
|
root: {
|
|
workflowRef,
|
|
workflowRefProof: "manifest-v3-protected-tag-exact-sha",
|
|
workflowSha,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("accepts protected-tag evidence from an older trusted tooling ancestor", async () => {
|
|
const trustedWorkflowSha = "7".repeat(40);
|
|
const trustedWorkflowRef = `release-publish/${trustedWorkflowSha.slice(0, 12)}-123`;
|
|
const olderWorkflowSha = "6".repeat(40);
|
|
const olderWorkflowRef = `release-ci/${olderWorkflowSha.slice(0, 12)}-1783705000000`;
|
|
const olderFixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
targetSha: "8".repeat(40),
|
|
workflowFullRef: `refs/heads/${olderWorkflowRef}`,
|
|
workflowRef: olderWorkflowRef,
|
|
workflowSha: olderWorkflowSha,
|
|
});
|
|
olderFixture.manifest.targetRef = olderFixture.targetSha;
|
|
olderFixture.client.getRef = (fullRef: string) => ({
|
|
object: { sha: trustedWorkflowSha },
|
|
ref: fullRef,
|
|
});
|
|
olderFixture.client.compareCommitLineage = (base: string, head: string) => {
|
|
expect(base).toBe(olderWorkflowSha);
|
|
expect(head).toBe(trustedWorkflowSha);
|
|
return {
|
|
merge_base_commit: { sha: olderWorkflowSha },
|
|
status: "ahead",
|
|
};
|
|
};
|
|
|
|
expect(
|
|
await validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: olderFixture.runId,
|
|
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
|
|
trustedWorkflowRef,
|
|
trustedWorkflowSha,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
olderFixture.client,
|
|
),
|
|
).toMatchObject({
|
|
root: {
|
|
workflowRef: olderWorkflowRef,
|
|
workflowRefProof: "manifest-v3-protected-tag-tooling-lineage",
|
|
workflowSha: olderWorkflowSha,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects protected-tag evidence from a same-name branch or unrelated producer", async () => {
|
|
const trustedWorkflowSha = "7".repeat(40);
|
|
const trustedWorkflowRef = `release-publish/${trustedWorkflowSha.slice(0, 12)}-123`;
|
|
const validFixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
workflowSha: trustedWorkflowSha,
|
|
});
|
|
|
|
await expect(
|
|
validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: validFixture.runId,
|
|
trustedWorkflowFullRef: `refs/heads/${trustedWorkflowRef}`,
|
|
trustedWorkflowRef,
|
|
trustedWorkflowSha,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
validFixture.client,
|
|
),
|
|
).rejects.toThrow("must be a protected tag");
|
|
|
|
const unrelatedWorkflowSha = "6".repeat(40);
|
|
const unrelatedWorkflowRef = `release-ci/${unrelatedWorkflowSha.slice(0, 12)}-1783705000000`;
|
|
const unrelatedFixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
targetSha: "8".repeat(40),
|
|
workflowFullRef: `refs/heads/${unrelatedWorkflowRef}`,
|
|
workflowRef: unrelatedWorkflowRef,
|
|
workflowSha: unrelatedWorkflowSha,
|
|
});
|
|
unrelatedFixture.manifest.targetRef = unrelatedFixture.targetSha;
|
|
unrelatedFixture.client.getRef = (fullRef: string) => ({
|
|
object: { sha: trustedWorkflowSha },
|
|
ref: fullRef,
|
|
});
|
|
unrelatedFixture.client.compareCommitLineage = () => ({
|
|
merge_base_commit: { sha: "5".repeat(40) },
|
|
status: "diverged",
|
|
});
|
|
await expect(
|
|
validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: unrelatedFixture.runId,
|
|
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
|
|
trustedWorkflowRef,
|
|
trustedWorkflowSha,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
unrelatedFixture.client,
|
|
),
|
|
).rejects.toThrow("not on the trusted tooling lineage");
|
|
|
|
const sameNameFixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
workflowFullRef: `refs/heads/${trustedWorkflowRef}`,
|
|
workflowRef: trustedWorkflowRef,
|
|
workflowSha: trustedWorkflowSha,
|
|
});
|
|
await expect(
|
|
validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: sameNameFixture.runId,
|
|
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
|
|
trustedWorkflowRef,
|
|
trustedWorkflowSha,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
sameNameFixture.client,
|
|
),
|
|
).rejects.toThrow("canonical release-ci branch");
|
|
});
|
|
|
|
it("rejects a protected tooling tag that moved or disappeared after sealing", async () => {
|
|
const workflowSha = "7".repeat(40);
|
|
const workflowRef = `release-ci/${workflowSha.slice(0, 12)}-1783705000000`;
|
|
const trustedWorkflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
const fixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
targetSha: "8".repeat(40),
|
|
workflowFullRef: `refs/heads/${workflowRef}`,
|
|
workflowRef,
|
|
workflowSha,
|
|
});
|
|
fixture.manifest.targetRef = fixture.targetSha;
|
|
const options = {
|
|
repository: "openclaw/openclaw",
|
|
runId: fixture.runId,
|
|
trustedWorkflowFullRef: `refs/tags/${trustedWorkflowRef}`,
|
|
trustedWorkflowRef,
|
|
trustedWorkflowSha: workflowSha,
|
|
verifierSourceContent: readFileSync(SCRIPT),
|
|
verifierSourceSha: "c".repeat(40),
|
|
};
|
|
|
|
fixture.client.getRef = (fullRef: string) => ({
|
|
object: { sha: "6".repeat(40) },
|
|
ref: fullRef,
|
|
});
|
|
await expect(validateReleaseRunEvidence(options, fixture.client)).rejects.toThrow(
|
|
"protected tooling tag moved",
|
|
);
|
|
|
|
fixture.client.getRef = () => {
|
|
throw new Error("HTTP 404");
|
|
};
|
|
await expect(validateReleaseRunEvidence(options, fixture.client)).rejects.toThrow(
|
|
"protected tooling tag is unavailable",
|
|
);
|
|
});
|
|
|
|
it.each(["main", "refs/heads/main"])(
|
|
"accepts a REST workflow path qualified with %s",
|
|
async (qualifiedRef) => {
|
|
const fixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
parentPath: `.github/workflows/full-release-validation.yml@${qualifiedRef}`,
|
|
workflowSha: "7".repeat(40),
|
|
});
|
|
|
|
expect((await verifyFixture(fixture)).root).toMatchObject({
|
|
workflowFullRef: "refs/heads/main",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("rejects a SHA-pinned evidenceReuse field even when false", async () => {
|
|
const workflowSha = "7".repeat(40);
|
|
const workflowRef = `release-ci/${workflowSha.slice(0, 12)}-1783705000000`;
|
|
const fixture = trustedMainPackageFixture({
|
|
manifestVersion: 3,
|
|
workflowFullRef: `refs/heads/${workflowRef}`,
|
|
workflowRef,
|
|
workflowSha,
|
|
});
|
|
fixture.manifest.targetRef = fixture.targetSha;
|
|
fixture.manifest.evidenceReuse = false;
|
|
|
|
await expect(verifyFixture(fixture)).rejects.toThrow("evidence reuse is invalid");
|
|
});
|
|
|
|
it("rejects dirty verifier bytes and a forged verifier source SHA", async () => {
|
|
const fixture = trustedMainPackageFixture();
|
|
await expect(
|
|
validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: fixture.runId,
|
|
verifierSourceContent: "different verifier bytes",
|
|
verifierSourceSha: "c".repeat(40),
|
|
},
|
|
fixture.client,
|
|
),
|
|
).rejects.toThrow("verifier script differs from its source SHA");
|
|
await expect(
|
|
validateReleaseRunEvidence(
|
|
{
|
|
repository: "openclaw/openclaw",
|
|
runId: fixture.runId,
|
|
verifierSourceSha: "f".repeat(40),
|
|
},
|
|
fixture.client,
|
|
),
|
|
).rejects.toThrow("verifier source blob is unavailable");
|
|
});
|
|
|
|
it("binds verifier bytes from the repository root even outside the caller cwd", () => {
|
|
const repositoryRoot = mkdtempSync(join(tmpdir(), "release-verifier-repo-"));
|
|
const outsideCwd = mkdtempSync(join(tmpdir(), "release-verifier-cwd-"));
|
|
try {
|
|
const scriptPath = join(repositoryRoot, SCRIPT);
|
|
mkdirSync(dirname(scriptPath), { recursive: true });
|
|
writeFileSync(scriptPath, readFileSync(SCRIPT));
|
|
execFileSync("git", ["init", "-q"], { cwd: repositoryRoot });
|
|
execFileSync("git", ["add", SCRIPT], { cwd: repositoryRoot });
|
|
execFileSync(
|
|
"git",
|
|
[
|
|
"-c",
|
|
"user.name=Release Test",
|
|
"-c",
|
|
"user.email=release-test@example.invalid",
|
|
"-c",
|
|
"commit.gpgSign=false",
|
|
"commit",
|
|
"-qm",
|
|
"test verifier",
|
|
],
|
|
{ cwd: repositoryRoot },
|
|
);
|
|
const sourceSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
cwd: repositoryRoot,
|
|
encoding: "utf8",
|
|
}).trim();
|
|
|
|
const moduleUrl = pathToFileURL(resolve(SCRIPT)).href;
|
|
const output = execFileSync(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"--eval",
|
|
`import { resolveVerifierIdentity } from ${JSON.stringify(moduleUrl)};
|
|
process.stdout.write(JSON.stringify(resolveVerifierIdentity(
|
|
process.env.SOURCE_SHA,
|
|
undefined,
|
|
process.env.REPOSITORY_ROOT,
|
|
)));`,
|
|
],
|
|
{
|
|
cwd: outsideCwd,
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
REPOSITORY_ROOT: repositoryRoot,
|
|
SOURCE_SHA: sourceSha,
|
|
},
|
|
},
|
|
);
|
|
expect(JSON.parse(output)).toMatchObject({
|
|
script: SCRIPT,
|
|
sourceSha,
|
|
});
|
|
} finally {
|
|
rmSync(repositoryRoot, { force: true, recursive: true });
|
|
rmSync(outsideCwd, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("binds the parent to the exact Full Release Validation REST run", () => {
|
|
const parentView = {
|
|
attempt: 2,
|
|
headBranch: "main",
|
|
headSha: "a".repeat(40),
|
|
};
|
|
const parentRest = {
|
|
event: "workflow_dispatch",
|
|
head_branch: parentView.headBranch,
|
|
head_sha: parentView.headSha,
|
|
id: 29090000000,
|
|
path: ".github/workflows/full-release-validation.yml@refs/heads/main",
|
|
run_attempt: parentView.attempt,
|
|
};
|
|
|
|
expect(validateParentRunBinding(parentView, parentRest, "29090000000")).toBe(parentRest);
|
|
expect(() =>
|
|
validateParentRunBinding(
|
|
parentView,
|
|
{ ...parentRest, path: ".github/workflows/openclaw-release-checks.yml" },
|
|
"29090000000",
|
|
),
|
|
).toThrow("full release parent run binding mismatch");
|
|
});
|
|
|
|
it("ignores same-SHA and nearby-name runs without the exact parent dispatch binding", () => {
|
|
const expected = "OpenClaw Performance full-release-validation-29090000000-3";
|
|
const exact = {
|
|
display_title: expected,
|
|
event: "workflow_dispatch",
|
|
head_branch: "main",
|
|
head_sha: "a".repeat(40),
|
|
id: 303,
|
|
};
|
|
expect(
|
|
selectExactChildRun(
|
|
[
|
|
{
|
|
display_title: "OpenClaw Performance",
|
|
event: "workflow_dispatch",
|
|
head_branch: "main",
|
|
head_sha: exact.head_sha,
|
|
id: 101,
|
|
},
|
|
{ ...exact, event: "push", id: 202 },
|
|
exact,
|
|
],
|
|
expected,
|
|
"main",
|
|
),
|
|
).toBe(exact);
|
|
});
|
|
|
|
it("fails closed on duplicate exact dispatch bindings and ignores branch collisions", () => {
|
|
const expected = "CI full-release-validation-29090000000-3-ci";
|
|
const exact = {
|
|
display_title: expected,
|
|
event: "workflow_dispatch",
|
|
head_branch: "main",
|
|
id: 1,
|
|
};
|
|
expect(
|
|
selectExactChildRun(
|
|
[{ ...exact, head_branch: "release/2026.7.1", id: 0 }, exact],
|
|
expected,
|
|
"main",
|
|
),
|
|
).toBe(exact);
|
|
expect(() => selectExactChildRun([exact, { ...exact, id: 2 }], expected, "main")).toThrow(
|
|
"multiple child runs have exact dispatch title and branch",
|
|
);
|
|
});
|
|
|
|
it("returns one exact child after a full bounded pagination scan", () => {
|
|
const expected = "OpenClaw Performance full-release-validation-29090000000-3";
|
|
const exact = {
|
|
display_title: expected,
|
|
event: "workflow_dispatch",
|
|
head_branch: "main",
|
|
id: 999,
|
|
};
|
|
const pages = Array.from({ length: 10 }, (_, pageIndex) =>
|
|
Array.from({ length: 100 }, (_unused, runIndex) => ({
|
|
display_title: `decoy-${pageIndex}-${runIndex}`,
|
|
event: "workflow_dispatch",
|
|
head_branch: "main",
|
|
id: pageIndex * 100 + runIndex,
|
|
})),
|
|
);
|
|
expectDefined(pages[9], "last child run page")[99] = exact;
|
|
|
|
expect(selectExactChildRunFromPages(pages, expected, "main")).toBe(exact);
|
|
expectDefined(pages[0], "first child run page")[0] = { ...exact, id: 1001 };
|
|
expect(() => selectExactChildRunFromPages(pages, expected, "main")).toThrow(
|
|
"multiple child runs have exact dispatch title and branch",
|
|
);
|
|
});
|
|
|
|
it("validates mixed-case composite job ordering using the producer contract", () => {
|
|
const composite = composeReleaseAttemptJobs(
|
|
[
|
|
{
|
|
jobs: [
|
|
{ conclusion: "success", name: "qa smoke ci", status: "completed" },
|
|
{ conclusion: "success", name: "QA Smoke CI", status: "completed" },
|
|
],
|
|
runAttempt: 1,
|
|
},
|
|
],
|
|
{ effectiveRunAttempt: 1, plannedRunAttempt: 1 },
|
|
);
|
|
const releaseChecksEvidence = {
|
|
compositeJobsSha256: composite.sha256,
|
|
dispatchActor: "github-actions[bot]",
|
|
effectiveRunAttempt: composite.effectiveRunAttempt,
|
|
jobs: composite.jobs,
|
|
observedRunAttempts: [1],
|
|
plannedRunAttempt: composite.plannedRunAttempt,
|
|
repository: "openclaw/openclaw",
|
|
runId: "404",
|
|
triggeringActor: "github-actions[bot]",
|
|
};
|
|
const raw = Object.assign(rawManifest({}), {
|
|
childEvidence: {
|
|
releaseChecks: releaseChecksEvidence,
|
|
},
|
|
});
|
|
|
|
expect(() =>
|
|
validateParentManifest(raw, {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
}),
|
|
).not.toThrow();
|
|
|
|
const reversedJobs = composite.jobs.toReversed();
|
|
const reversedCompositeJobsSha256 = releaseCompositeJobsSha256({
|
|
effectiveRunAttempt: composite.effectiveRunAttempt,
|
|
jobs: reversedJobs,
|
|
plannedRunAttempt: composite.plannedRunAttempt,
|
|
});
|
|
releaseChecksEvidence.compositeJobsSha256 = reversedCompositeJobsSha256;
|
|
releaseChecksEvidence.jobs = reversedJobs;
|
|
|
|
expect(() =>
|
|
validateParentManifest(raw, {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
}),
|
|
).toThrow("release validation child job identity is duplicated: releaseChecks");
|
|
});
|
|
|
|
it("requires the npm Telegram child for all-validation with an effective package spec", () => {
|
|
const raw = rawManifest({});
|
|
raw.childRuns.npmTelegram = "505";
|
|
raw.validationInputs.npmTelegramPackageSpec = "openclaw@beta";
|
|
raw.validationInputs.skipPackageTelegramE2e = "true";
|
|
const manifest = validateParentManifest(raw, {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
});
|
|
const selected = requiredChildKeysForRerunGroup(manifest.rerunGroup, manifest.validationInputs);
|
|
expect([...selected].toSorted((left, right) => left.localeCompare(right))).toEqual([
|
|
"normalCi",
|
|
"npmTelegram",
|
|
"pluginPrerelease",
|
|
"productPerformance",
|
|
"releaseChecks",
|
|
]);
|
|
const missing = {
|
|
...manifest,
|
|
childRunIds: { ...manifest.childRunIds, npmTelegram: "" },
|
|
};
|
|
expect(() =>
|
|
manifestChildEntries(
|
|
missing,
|
|
expectedChildDispatches(manifest.runId, manifest.runAttempt, "main"),
|
|
selected,
|
|
),
|
|
).toThrow("selected child is missing from manifest: NPM Telegram Beta E2E");
|
|
});
|
|
|
|
it("validates the Telegram waiver before changing package child coverage", () => {
|
|
const version = "2026.9.5";
|
|
|
|
const raw = rawManifest({});
|
|
raw.releaseProfile = "stable";
|
|
Object.assign(raw.validationInputs, {
|
|
telegramWaiver: `${version}-owner-approved`,
|
|
targetVersion: version,
|
|
releasePackageSpec: `openclaw@${version}`,
|
|
});
|
|
const expected = { runAttempt: 2, runId: "29090000000" };
|
|
const manifest = validateParentManifest(raw, expected);
|
|
expect(
|
|
requiredChildKeysForRerunGroup(manifest.rerunGroup, manifest.validationInputs),
|
|
).not.toContain("npmTelegram");
|
|
raw.validationInputs.targetVersion = "2026.8.2";
|
|
expect(() => validateParentManifest(raw, expected)).toThrow(/Telegram waiver/u);
|
|
});
|
|
|
|
it("rejects an unreviewed self-declared Telegram waiver", () => {
|
|
const raw = rawManifest({});
|
|
raw.releaseProfile = "stable";
|
|
Object.assign(raw.validationInputs, {
|
|
telegramWaiver: "2026.10.1-owner-approved",
|
|
targetVersion: "2026.10.1",
|
|
releasePackageSpec: "openclaw@2026.10.1",
|
|
});
|
|
expect(() => validateParentManifest(raw, { runAttempt: 2, runId: "29090000000" })).toThrow(
|
|
/Telegram waiver/u,
|
|
);
|
|
});
|
|
|
|
it("keeps historical non-reuse v2 manifests readable without validation inputs", () => {
|
|
const legacy = rawManifest({});
|
|
delete (legacy as { validationInputs?: unknown }).validationInputs;
|
|
const manifest = validateParentManifest(legacy, {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
});
|
|
|
|
expect(manifest.validationInputs).toBeUndefined();
|
|
expect(manifest.rerunGroup).toBe("all");
|
|
});
|
|
|
|
it("keeps historical QA manifests readable", () => {
|
|
const version = 3;
|
|
const rerunGroup = "qa";
|
|
|
|
const workflowSha = version === 3 ? "b".repeat(40) : undefined;
|
|
const manifest = validateParentManifest(rawManifest({ rerunGroup, version, workflowSha }), {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowSha,
|
|
});
|
|
|
|
expect(manifest.rerunGroup).toBe(rerunGroup);
|
|
expect(manifest.version).toBe(version);
|
|
});
|
|
|
|
it("binds v3 manifests to their immutable producer workflow SHA", () => {
|
|
const workflowSha = "b".repeat(40);
|
|
const manifest = validateParentManifest(rawManifest({ version: 3, workflowSha }), {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowRef: "main",
|
|
workflowSha,
|
|
});
|
|
expect(manifest).toMatchObject({
|
|
version: 3,
|
|
workflowSha,
|
|
});
|
|
expect(() =>
|
|
validateParentManifest(rawManifest({ version: 3, workflowSha }), {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowSha: "c".repeat(40),
|
|
}),
|
|
).toThrow("release validation manifest workflow SHA mismatch");
|
|
});
|
|
|
|
it("binds v3 manifests to the candidate sealed by the execution plan", () => {
|
|
const workflowSha = "b".repeat(40);
|
|
const candidateBinding = fullReleaseCandidateBindingFixture({
|
|
releaseProfile: "beta",
|
|
releaseSoak: false,
|
|
targetSha: "a".repeat(40),
|
|
toolingSha: workflowSha,
|
|
upgradeSurvivorScenarios: "",
|
|
});
|
|
const manifest = validateParentManifest(
|
|
rawManifest({ candidateBinding, version: 3, workflowSha }),
|
|
{
|
|
candidateBinding,
|
|
repository: "openclaw/openclaw",
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowSha,
|
|
},
|
|
);
|
|
expect(manifest.candidateBinding).toEqual(candidateBinding);
|
|
expect(() =>
|
|
validateParentManifest(rawManifest({ candidateBinding: null, version: 3, workflowSha }), {
|
|
candidateBinding,
|
|
repository: "openclaw/openclaw",
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowSha,
|
|
}),
|
|
).toThrow("candidate differs from the immutable plan");
|
|
});
|
|
|
|
it("requires v3 manifests to record artifact-only performance publication", () => {
|
|
const workflowSha = "b".repeat(40);
|
|
const missing = rawManifest({ version: 3, workflowSha });
|
|
delete (
|
|
missing.controls as {
|
|
performanceReportPublication?: string;
|
|
}
|
|
).performanceReportPublication;
|
|
expect(() =>
|
|
validateParentManifest(missing, {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowSha,
|
|
}),
|
|
).toThrow("release validation manifest performance report publication mode is invalid");
|
|
|
|
const publishing = rawManifest({ version: 3, workflowSha });
|
|
publishing.controls.performanceReportPublication = "publish";
|
|
expect(() =>
|
|
validateParentManifest(publishing, {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowSha,
|
|
}),
|
|
).toThrow("release validation manifest performance report publication mode is invalid");
|
|
});
|
|
|
|
it("requires a successful artifact-only performance guard and skipped publishers", () => {
|
|
const guard = {
|
|
conclusion: "success",
|
|
name: "Verify artifact-only report mode",
|
|
status: "completed",
|
|
};
|
|
const skippedPublisher = {
|
|
conclusion: "skipped",
|
|
name: "Publish mock provider report",
|
|
status: "completed",
|
|
};
|
|
expect(validatePerformanceArtifactOnlyJobs([guard, skippedPublisher])).toBe(guard);
|
|
expect(() => validatePerformanceArtifactOnlyJobs([skippedPublisher])).toThrow(
|
|
"performance artifact-only guard is missing or unsuccessful",
|
|
);
|
|
expect(() =>
|
|
validatePerformanceArtifactOnlyJobs([{ ...guard, conclusion: "failure" }]),
|
|
).toThrow("performance artifact-only guard is missing or unsuccessful");
|
|
expect(() =>
|
|
validatePerformanceArtifactOnlyJobs([guard, { ...skippedPublisher, conclusion: "success" }]),
|
|
).toThrow("performance report publisher was not skipped");
|
|
});
|
|
|
|
it("requires the child mapped by rerunGroup and scans only selected in-progress workflows", () => {
|
|
expect(() => requiredChildKeysForRerunGroup("release-checks")).toThrow(
|
|
"release validation manifest rerun group is invalid: release-checks",
|
|
);
|
|
expect(() => requiredChildKeysForRerunGroup("qa")).toThrow(
|
|
"release validation manifest rerun group is invalid: qa",
|
|
);
|
|
const focused = validateParentManifest(
|
|
{
|
|
...rawManifest({ rerunGroup: "npm-telegram" }),
|
|
childRuns: {
|
|
normalCi: "",
|
|
npmTelegram: "",
|
|
pluginPrerelease: "",
|
|
productPerformance: { runId: "" },
|
|
releaseChecks: "",
|
|
},
|
|
},
|
|
{ runAttempt: 2, runId: "29090000000" },
|
|
);
|
|
const selected = requiredChildKeysForRerunGroup(focused.rerunGroup);
|
|
const children = expectedSelectedChildDispatches(
|
|
focused.runId,
|
|
focused.runAttempt,
|
|
focused.workflowRef,
|
|
selected,
|
|
);
|
|
expect(children.map((child) => child.manifestKey)).toEqual(["npmTelegram"]);
|
|
expect(() => manifestChildEntries(focused, children, selected)).toThrow(
|
|
"selected child is missing from manifest: NPM Telegram Beta E2E",
|
|
);
|
|
|
|
const inProgress = selectedChildKeys([
|
|
{ conclusion: "skipped", name: "Run normal full CI" },
|
|
{ conclusion: "skipped", name: "Run plugin prerelease validation" },
|
|
{ conclusion: undefined, name: "Run product performance evidence" },
|
|
{ conclusion: "skipped", name: "Run release/live/Docker/QA validation" },
|
|
]);
|
|
expect(
|
|
expectedSelectedChildDispatches("29090000000", 2, "main", inProgress).map(
|
|
(child) => child.manifestKey,
|
|
),
|
|
).toEqual(["productPerformance"]);
|
|
});
|
|
|
|
it("rejects unverified changed paths and cross-SHA exact-target reuse", () => {
|
|
const root = validateParentManifest(rawManifest({}), {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
});
|
|
const changedPaths = validateParentManifest(
|
|
rawManifest({
|
|
evidenceReuse: {
|
|
changedPaths: ["CHANGELOG.md"],
|
|
evidenceSha: root.targetSha,
|
|
policy: "changelog-only-release-v1",
|
|
runId: root.runId,
|
|
selectedRunId: root.runId,
|
|
},
|
|
runId: "29090000001",
|
|
targetSha: "b".repeat(40),
|
|
}),
|
|
{ runAttempt: 2, runId: "29090000001" },
|
|
);
|
|
expect(() =>
|
|
validateEvidenceReuseChain(changedPaths, root, root, (base: string) => ({
|
|
files: [{ filename: "src/index.ts" }],
|
|
merge_base_commit: { sha: base },
|
|
status: "ahead",
|
|
})),
|
|
).toThrow("failed commit comparison");
|
|
|
|
expect(() =>
|
|
validateEvidenceReuseChain(changedPaths, root, root, (base: string) => ({
|
|
files: [
|
|
{
|
|
filename: "CHANGELOG.md",
|
|
previous_filename: "src/index.ts",
|
|
status: "renamed",
|
|
},
|
|
],
|
|
merge_base_commit: { sha: base },
|
|
status: "ahead",
|
|
})),
|
|
).toThrow("failed commit comparison");
|
|
|
|
const changedTarget = validateParentManifest(
|
|
rawManifest({
|
|
evidenceReuse: {
|
|
changedPaths: [],
|
|
evidenceSha: root.targetSha,
|
|
policy: "exact-target-full-validation-v1",
|
|
runId: root.runId,
|
|
selectedRunId: root.runId,
|
|
},
|
|
runId: "29090000001",
|
|
targetSha: "b".repeat(40),
|
|
}),
|
|
{ runAttempt: 2, runId: "29090000001" },
|
|
);
|
|
expect(() => validateEvidenceReuseChain(changedTarget, root, root)).toThrow(
|
|
"exact-target release evidence reuse requires no changed paths",
|
|
);
|
|
});
|
|
|
|
it("binds split changelog reuse to the selected release entry and matching record", () => {
|
|
const targetVersion = "2026.7.1-beta.1";
|
|
|
|
const inputs = { ...rawManifest({}).validationInputs, targetVersion };
|
|
const root = validateParentManifest(
|
|
{ ...rawManifest({}), validationInputs: inputs },
|
|
{
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
},
|
|
);
|
|
const paths = ["CHANGELOG.md", "CHANGELOG/2026.7.1.md", "CHANGELOG/records/2026.7.1.md"];
|
|
const makeCurrent = (changedPaths: string[]) =>
|
|
validateParentManifest(
|
|
{
|
|
...rawManifest({
|
|
evidenceReuse: {
|
|
changedPaths,
|
|
evidenceSha: root.targetSha,
|
|
policy: "split-changelog-release-v1",
|
|
runId: root.runId,
|
|
selectedRunId: root.runId,
|
|
},
|
|
runId: "29090000001",
|
|
targetSha: "b".repeat(40),
|
|
}),
|
|
validationInputs: inputs,
|
|
},
|
|
{ runAttempt: 2, runId: "29090000001" },
|
|
);
|
|
const compare = (changedPaths: string[]) => (base: string) => ({
|
|
files: changedPaths.map((filename) => ({ filename, status: "modified" })),
|
|
merge_base_commit: { sha: base },
|
|
status: "ahead",
|
|
});
|
|
expect(validateEvidenceReuseChain(makeCurrent(paths), root, root, compare(paths))).toBe(
|
|
root.targetSha,
|
|
);
|
|
for (const unrelated of [
|
|
"CHANGELOG/2026.8.1.md",
|
|
"CHANGELOG/records/2026.8.1.md",
|
|
"src/index.ts",
|
|
]) {
|
|
const changedPaths = [...paths, unrelated];
|
|
expect(() =>
|
|
validateEvidenceReuseChain(makeCurrent(changedPaths), root, root, compare(changedPaths)),
|
|
).toThrow("invalid target delta");
|
|
expect(() =>
|
|
validateEvidenceReuseChain(makeCurrent(paths), root, root, compare(changedPaths)),
|
|
).toThrow("failed commit comparison");
|
|
}
|
|
for (const filename of paths.slice(1)) {
|
|
for (const status of ["removed", "renamed"]) {
|
|
expect(() =>
|
|
validateEvidenceReuseChain(makeCurrent(paths), root, root, (base: string) => ({
|
|
...compare(paths)(base),
|
|
files: paths.map((path) => ({
|
|
filename: path,
|
|
status: path === filename ? status : "modified",
|
|
previous_filename:
|
|
path === filename && status === "renamed" ? "src/index.ts" : undefined,
|
|
})),
|
|
})),
|
|
).toThrow("failed commit comparison");
|
|
}
|
|
}
|
|
if (targetVersion.endsWith("-beta.1")) {
|
|
const betaPaths = [
|
|
"CHANGELOG.md",
|
|
`CHANGELOG/${targetVersion}.md`,
|
|
`CHANGELOG/records/${targetVersion}.md`,
|
|
];
|
|
expect(() =>
|
|
validateEvidenceReuseChain(makeCurrent(betaPaths), root, root, compare(betaPaths)),
|
|
).toThrow("invalid target delta");
|
|
}
|
|
expect(() =>
|
|
validateEvidenceReuseChain(
|
|
makeCurrent(["CHANGELOG.md"]),
|
|
root,
|
|
root,
|
|
compare(["CHANGELOG.md"]),
|
|
),
|
|
).toThrow("invalid target delta");
|
|
});
|
|
|
|
it("rejects exact-target reuse without matching root policy and authorization", () => {
|
|
const root = validateParentManifest(rawManifest({}), {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
});
|
|
const current = validateParentManifest(
|
|
rawManifest({
|
|
evidenceReuse: {
|
|
changedPaths: [],
|
|
evidenceSha: root.targetSha,
|
|
policy: "exact-target-full-validation-v1",
|
|
runId: root.runId,
|
|
selectedRunId: root.runId,
|
|
},
|
|
runId: "29090000001",
|
|
targetSha: root.targetSha,
|
|
}),
|
|
{ runAttempt: 2, runId: "29090000001" },
|
|
);
|
|
const mismatchedRoot = {
|
|
...root,
|
|
validationInputs: {
|
|
...root.validationInputs,
|
|
npmTelegramScenario: "telegram-status-command",
|
|
},
|
|
};
|
|
|
|
expect(() => validateEvidenceReuseChain(current, mismatchedRoot, mismatchedRoot)).toThrow(
|
|
"evidence reuse current manifest policy differs from the chain root",
|
|
);
|
|
expect(() =>
|
|
validateEvidenceReuseChain({ ...current, evidenceReuse: undefined }, root, root),
|
|
).toThrow("does not authorize evidence reuse");
|
|
});
|
|
|
|
it("rejects any selected manifest that itself reuses evidence", () => {
|
|
const root = validateParentManifest(rawManifest({}), {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
});
|
|
const intermediate = validateParentManifest(
|
|
rawManifest({
|
|
evidenceReuse: {
|
|
changedPaths: [],
|
|
evidenceSha: root.targetSha,
|
|
policy: "exact-target-full-validation-v1",
|
|
runId: root.runId,
|
|
selectedRunId: root.runId,
|
|
},
|
|
runId: "29090000001",
|
|
targetSha: root.targetSha,
|
|
}),
|
|
{ runAttempt: 2, runId: "29090000001" },
|
|
);
|
|
const current = validateParentManifest(
|
|
rawManifest({
|
|
evidenceReuse: {
|
|
changedPaths: [],
|
|
evidenceSha: intermediate.targetSha,
|
|
policy: "exact-target-full-validation-v1",
|
|
runId: root.runId,
|
|
selectedRunId: intermediate.runId,
|
|
},
|
|
runId: "29090000002",
|
|
targetSha: intermediate.targetSha,
|
|
}),
|
|
{ runAttempt: 2, runId: "29090000002" },
|
|
);
|
|
|
|
expect(() => validateEvidenceReuseChain(current, intermediate, root)).toThrow(
|
|
"evidence reuse must select a root execution manifest",
|
|
);
|
|
});
|
|
|
|
it("binds each manifest workflow ref to the fetched parent branch", () => {
|
|
expect(() =>
|
|
validateParentManifest(rawManifest({}), {
|
|
runAttempt: 2,
|
|
runId: "29090000000",
|
|
workflowRef: "release/2026.7.1",
|
|
}),
|
|
).toThrow("release validation manifest workflow ref mismatch");
|
|
});
|
|
|
|
it("validates manifest child workflow, dispatch tuple, branch, and attempt", () => {
|
|
const child = expectDefined(
|
|
expectedChildDispatches("29090000000", 3, "main")[0],
|
|
"expected CI child dispatch",
|
|
);
|
|
const parentManifest = {
|
|
runAttempt: 3,
|
|
runId: "29090000000",
|
|
targetSha: "a".repeat(40),
|
|
workflowSha: "b".repeat(40),
|
|
};
|
|
const parentJobs = [
|
|
{
|
|
completed_at: "2026-07-10T01:10:00Z",
|
|
conclusion: "success",
|
|
id: 901,
|
|
name: child.parentJobName,
|
|
run_attempt: 3,
|
|
started_at: "2026-07-10T01:00:00Z",
|
|
status: "completed",
|
|
steps: [],
|
|
},
|
|
];
|
|
const parentLog = [
|
|
`TARGET_SHA: ${parentManifest.targetSha}`,
|
|
"Dispatched ci.yml: https://github.com/openclaw/openclaw/actions/runs/101 (attempt 1)",
|
|
].join("\n");
|
|
const run = {
|
|
actor: { login: "github-actions[bot]" },
|
|
display_title: child.displayTitle,
|
|
event: "workflow_dispatch",
|
|
head_branch: child.headBranch,
|
|
head_sha: parentManifest.workflowSha,
|
|
id: 101,
|
|
path: ".github/workflows/ci.yml@refs/heads/main",
|
|
run_attempt: 1,
|
|
triggering_actor: { login: "github-actions[bot]" },
|
|
};
|
|
expect(validateManifestChildRun(run, child, "101", parentManifest, parentJobs, parentLog)).toBe(
|
|
run,
|
|
);
|
|
expect(() =>
|
|
validateManifestChildRun(
|
|
{ ...run, head_branch: "release/2026.7.1" },
|
|
child,
|
|
"101",
|
|
parentManifest,
|
|
parentJobs,
|
|
parentLog,
|
|
),
|
|
).toThrow("manifest child dispatch tuple mismatch");
|
|
expect(() =>
|
|
validateParentManifest(rawManifest({}), { runAttempt: 3, runId: "29090000000" }),
|
|
).toThrow("release validation manifest run attempt mismatch");
|
|
});
|
|
|
|
it("accepts strongly bound legacy and correlated children across parent attempts", () => {
|
|
const parentManifest = {
|
|
runAttempt: 2,
|
|
runId: "28717729503",
|
|
targetSha: "a".repeat(40),
|
|
workflowSha: "b".repeat(40),
|
|
};
|
|
const children = expectedChildDispatches(
|
|
parentManifest.runId,
|
|
parentManifest.runAttempt,
|
|
"main",
|
|
);
|
|
const fixtures = new Map([
|
|
["normalCi", { originAttempt: 2, runId: 28718903263, title: "CI" }],
|
|
["pluginPrerelease", { originAttempt: 1, runId: 28717802268, title: "Plugin Prerelease" }],
|
|
[
|
|
"productPerformance",
|
|
{
|
|
originAttempt: 1,
|
|
runId: 28717802171,
|
|
title: "OpenClaw Performance full-release-validation-28717729503-1",
|
|
},
|
|
],
|
|
["releaseChecks", { originAttempt: 1, runId: 28717802397, title: "OpenClaw Release Checks" }],
|
|
]);
|
|
const fingerprint = {
|
|
completed_at: "2026-07-04T20:29:21Z",
|
|
conclusion: "success",
|
|
started_at: "2026-07-04T19:53:02Z",
|
|
status: "completed",
|
|
steps: [
|
|
{
|
|
completed_at: "2026-07-04T20:29:20Z",
|
|
conclusion: "success",
|
|
name: "Dispatch and monitor child",
|
|
number: 1,
|
|
started_at: "2026-07-04T19:53:03Z",
|
|
status: "completed",
|
|
},
|
|
],
|
|
};
|
|
|
|
for (const child of children.filter((entry) => fixtures.has(entry.manifestKey))) {
|
|
const fixture = fixtures.get(child.manifestKey);
|
|
if (!fixture) {
|
|
throw new Error(`missing fixture for ${child.manifestKey}`);
|
|
}
|
|
const { originAttempt, runId, title } = fixture;
|
|
const parentJobs = [
|
|
...(originAttempt === 1
|
|
? [
|
|
{
|
|
...fingerprint,
|
|
id: 900,
|
|
name: child.parentJobName,
|
|
run_attempt: 1,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
...fingerprint,
|
|
id: 901,
|
|
name: child.parentJobName,
|
|
run_attempt: 2,
|
|
},
|
|
];
|
|
const run = {
|
|
actor: { login: "github-actions[bot]" },
|
|
display_title: title,
|
|
event: "workflow_dispatch",
|
|
head_branch: child.headBranch,
|
|
head_sha: parentManifest.workflowSha,
|
|
id: runId,
|
|
path: `.github/workflows/${child.workflow}@refs/heads/${child.headBranch}`,
|
|
run_attempt: 1,
|
|
triggering_actor: { login: "github-actions[bot]" },
|
|
};
|
|
const parentLog = [
|
|
`TARGET_SHA: ${parentManifest.targetSha}`,
|
|
...(child.manifestKey === "productPerformance" ? ["-f publish_reports=false"] : []),
|
|
`Dispatched ${child.workflow}: https://github.com/openclaw/openclaw/actions/runs/${runId} (attempt ${run.run_attempt})`,
|
|
].join("\n");
|
|
expect(resolveManifestChildOriginAttempt(run, child, parentManifest, parentJobs)).toBe(
|
|
originAttempt,
|
|
);
|
|
expect(
|
|
validateManifestChildRun(run, child, String(runId), parentManifest, parentJobs, parentLog),
|
|
).toBe(run);
|
|
expect(
|
|
validateManifestChildRun(
|
|
run,
|
|
child,
|
|
String(runId),
|
|
parentManifest,
|
|
parentJobs,
|
|
parentLog.replace(` (attempt ${run.run_attempt})`, ""),
|
|
),
|
|
).toBe(run);
|
|
if (child.manifestKey === "productPerformance") {
|
|
expect(() =>
|
|
validateManifestChildRun(
|
|
run,
|
|
child,
|
|
String(runId),
|
|
parentManifest,
|
|
parentJobs,
|
|
parentLog.replace("-f publish_reports=false\n", ""),
|
|
),
|
|
).toThrow("release performance child is not dispatched in artifact-only mode");
|
|
}
|
|
}
|
|
|
|
const ci = children.find((child) => child.manifestKey === "normalCi");
|
|
if (!ci) {
|
|
throw new Error("missing CI child fixture");
|
|
}
|
|
const wrongParent = {
|
|
display_title: `CI full-release-validation-28717729504-1-ci`,
|
|
event: "workflow_dispatch",
|
|
head_branch: "main",
|
|
id: 101,
|
|
path: ".github/workflows/ci.yml@refs/heads/main",
|
|
};
|
|
const ciJobs = [
|
|
{
|
|
...fingerprint,
|
|
id: 901,
|
|
name: ci.parentJobName,
|
|
run_attempt: 2,
|
|
},
|
|
];
|
|
const ciLog = [
|
|
`TARGET_SHA: ${parentManifest.targetSha}`,
|
|
"Dispatched ci.yml: https://github.com/openclaw/openclaw/actions/runs/101 (attempt 1)",
|
|
].join("\n");
|
|
expect(() =>
|
|
validateManifestChildRun(wrongParent, ci, "101", parentManifest, ciJobs, ciLog),
|
|
).toThrow("manifest child dispatch tuple mismatch");
|
|
expect(() =>
|
|
validateManifestChildRun(
|
|
{
|
|
...wrongParent,
|
|
display_title: `CI full-release-validation-${parentManifest.runId}-3-ci`,
|
|
},
|
|
ci,
|
|
"101",
|
|
parentManifest,
|
|
ciJobs,
|
|
ciLog,
|
|
),
|
|
).toThrow("manifest child dispatch tuple mismatch");
|
|
expect(
|
|
resolveManifestChildOriginAttempt({ display_title: "CI nearby" }, ci, parentManifest, ciJobs),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("keeps requested changelog reuse target and evidence SHAs separate", () => {
|
|
const evidenceSha = "a".repeat(40);
|
|
const targetSha = "b".repeat(40);
|
|
expect(() =>
|
|
validateRequestedEvidenceReuse(
|
|
{
|
|
evidenceReuse: {
|
|
changedPaths: ["CHANGELOG.md"],
|
|
evidenceSha,
|
|
policy: "changelog-only-release-v1",
|
|
runId: "101",
|
|
selectedRunId: "101",
|
|
},
|
|
runId: "101",
|
|
targetSha,
|
|
},
|
|
{ runId: "101", targetSha: evidenceSha },
|
|
{ runId: "101", targetSha: evidenceSha },
|
|
{
|
|
expectedChangedPaths: ["CHANGELOG.md"],
|
|
expectedEvidencePolicy: "changelog-only-release-v1",
|
|
expectedEvidenceSha: evidenceSha,
|
|
expectedRootRunId: "101",
|
|
expectedSelectedRunId: "101",
|
|
expectedTargetSha: targetSha,
|
|
},
|
|
),
|
|
).not.toThrow();
|
|
expect(() =>
|
|
validateRequestedEvidenceReuse(
|
|
{
|
|
evidenceReuse: {
|
|
changedPaths: ["CHANGELOG.md"],
|
|
evidenceSha,
|
|
policy: "changelog-only-release-v1",
|
|
runId: "101",
|
|
selectedRunId: "101",
|
|
},
|
|
runId: "101",
|
|
targetSha,
|
|
},
|
|
{ runId: "101", targetSha },
|
|
{ runId: "101", targetSha },
|
|
{
|
|
expectedChangedPaths: ["CHANGELOG.md"],
|
|
expectedEvidencePolicy: "changelog-only-release-v1",
|
|
expectedEvidenceSha: evidenceSha,
|
|
expectedRootRunId: "101",
|
|
expectedSelectedRunId: "101",
|
|
expectedTargetSha: targetSha,
|
|
},
|
|
),
|
|
).toThrow("no longer matches");
|
|
});
|
|
|
|
it("rejects carried parent jobs whose selected-attempt execution fingerprint changed", () => {
|
|
const child = expectedChildDispatches("28717729503", 2, "main").find(
|
|
(entry) => entry.manifestKey === "pluginPrerelease",
|
|
);
|
|
if (!child) {
|
|
throw new Error("missing plugin prerelease fixture");
|
|
}
|
|
const parentManifest = { runAttempt: 2, runId: "28717729503" };
|
|
const parentJobs = [
|
|
{
|
|
completed_at: "2026-07-04T20:29:21Z",
|
|
conclusion: "success",
|
|
id: 900,
|
|
name: child.parentJobName,
|
|
run_attempt: 1,
|
|
started_at: "2026-07-04T19:53:02Z",
|
|
status: "completed",
|
|
steps: [],
|
|
},
|
|
{
|
|
completed_at: "2026-07-04T20:30:21Z",
|
|
conclusion: "success",
|
|
id: 901,
|
|
name: child.parentJobName,
|
|
run_attempt: 2,
|
|
started_at: "2026-07-04T19:53:02Z",
|
|
status: "completed",
|
|
steps: [],
|
|
},
|
|
];
|
|
|
|
expect(() => selectManifestParentJob(parentJobs, child, parentManifest, 1)).toThrow(
|
|
"manifest parent job carry-forward fingerprint mismatch",
|
|
);
|
|
});
|
|
});
|