perf(release): prebuild immutable candidate artifacts

This commit is contained in:
Vincent Koc
2026-08-21 03:45:12 -07:00
parent 570054eb50
commit bcba4fc2d1
9 changed files with 1660 additions and 0 deletions
@@ -0,0 +1,354 @@
name: Release Candidate Artifacts
run-name: Release Candidate Artifacts ${{ inputs.dispatch_id }}
on:
workflow_dispatch:
inputs:
dispatch_id:
description: Unique caller nonce used to locate this exact producer run
required: true
type: string
release_plan_lock_base64:
description: Base64-encoded canonical ReleasePlanLock bytes
required: true
type: string
permissions:
actions: read
contents: read
packages: read
pull-requests: read
concurrency:
group: release-candidate-artifacts-${{ inputs.dispatch_id }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
validate_release_plan:
name: Validate immutable release plan
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
allow_frozen_target_scenario_omissions: ${{ steps.plan.outputs.allow_frozen_target_scenario_omissions }}
allow_unreleased_changelog: ${{ steps.plan.outputs.allow_unreleased_changelog }}
candidate_sha: ${{ steps.plan.outputs.candidate_sha }}
release_plan_digest: ${{ steps.plan.outputs.release_plan_digest }}
release_profile: ${{ steps.plan.outputs.release_profile }}
steps:
- name: Checkout trusted receipt tooling
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Validate ReleasePlanLock
id: plan
env:
DISPATCH_ID: ${{ inputs.dispatch_id }}
RELEASE_PLAN_LOCK_BASE64: ${{ inputs.release_plan_lock_base64 }}
WORKFLOW_FULL_REF: ${{ github.ref }}
WORKFLOW_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
node --input-type=module <<'NODE'
import fs from "node:fs";
import { parseReleasePlanLockJson } from "./scripts/release-plan-contract.mjs";
const dispatchId = process.env.DISPATCH_ID ?? "";
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/u.test(dispatchId)) {
throw new Error("dispatch_id must be one safe unique caller nonce");
}
const encoded = process.env.RELEASE_PLAN_LOCK_BASE64 ?? "";
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded)) {
throw new Error("release_plan_lock_base64 must contain one canonical base64 payload");
}
const bytes = Buffer.from(encoded, "base64");
if (bytes.toString("base64") !== encoded) {
throw new Error("release_plan_lock_base64 is not canonical base64");
}
const lock = parseReleasePlanLockJson(bytes.toString("utf8"));
if (lock.plan.tooling.sha !== process.env.WORKFLOW_SHA) {
throw new Error("ReleasePlan tooling SHA must equal the candidate producer workflow SHA");
}
if (lock.plan.tooling.ref !== process.env.WORKFLOW_FULL_REF) {
throw new Error("ReleasePlan tooling ref must equal the candidate producer workflow ref");
}
const outputPath = process.env.GITHUB_OUTPUT;
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");
}
const frozenTarget = lock.plan.target_context_ref !== lock.plan.candidate_sha;
fs.appendFileSync(
outputPath,
[
`allow_frozen_target_scenario_omissions=${frozenTarget}`,
`allow_unreleased_changelog=${lock.plan.purpose === "main-qualification"}`,
`candidate_sha=${lock.plan.candidate_sha}`,
`release_plan_digest=${lock.digest}`,
`release_profile=${lock.plan.validation.profile}`,
"",
].join("\n"),
);
NODE
candidate_artifacts:
name: Produce package, plugin registry, and Docker image
needs: validate_release_plan
permissions:
actions: read
contents: read
packages: read
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ needs.validate_release_plan.outputs.candidate_sha }}
prepare_only: true
include_repo_e2e: false
include_release_path_suites: false
include_openwebui: false
include_live_suites: false
enable_prepublish_plugin_registry: true
allow_frozen_target_scenario_omissions: ${{ needs.validate_release_plan.outputs.allow_frozen_target_scenario_omissions == 'true' }}
allow_unreleased_changelog: ${{ needs.validate_release_plan.outputs.allow_unreleased_changelog == 'true' }}
release_test_profile: ${{ needs.validate_release_plan.outputs.release_profile }}
shared_image_artifact_namespace: release-candidate
shared_image_policy: no-push-artifact
root_image:
name: Produce root Dockerfile image
needs: validate_release_plan
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
packages: read
outputs:
archive_sha256: ${{ steps.image_artifact.outputs.archive_sha256 }}
artifact_digest: ${{ steps.upload.outputs.artifact-digest }}
artifact_id: ${{ steps.upload.outputs.artifact-id }}
artifact_name: ${{ steps.image_artifact.outputs.artifact_name }}
env:
DOCKER_BUILD_RECORD_UPLOAD: "false"
DOCKER_BUILD_SUMMARY: "false"
steps:
- name: Checkout candidate
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.validate_release_plan.outputs.candidate_sha }}
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Checkout trusted image artifact helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ${{ github.repository }}
ref: ${{ github.sha }}
path: .release-harness
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@6ff44f8e5255f9d8aa31ef22f7e57a2d926b7da0 # v1
with:
max-cache-size-mb: 800000
- name: Build root Dockerfile image
env:
IMAGE_REF: openclaw-release-candidate-root:${{ needs.validate_release_plan.outputs.candidate_sha }}
shell: bash
run: |
set -euo pipefail
timeout --kill-after=30s 45m docker buildx build \
--progress=plain \
--load \
--build-arg OPENCLAW_EXTENSIONS=matrix \
--tag "$IMAGE_REF" \
--file ./Dockerfile \
.
- name: Pack root Dockerfile image artifact
id: image_artifact
env:
IMAGE_REF: openclaw-release-candidate-root:${{ needs.validate_release_plan.outputs.candidate_sha }}
TARGET_SHA: ${{ needs.validate_release_plan.outputs.candidate_sha }}
WORKFLOW_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
artifact_dir="${RUNNER_TEMP}/release-candidate-root-image"
artifact_name="release-candidate-root-image-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
bash .release-harness/scripts/docker/shared-image-artifact.sh \
pack "$artifact_dir" release-candidate-root "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF"
archive_sha256="$(
jq -er '.archive.sha256 | select(type == "string" and test("^[a-f0-9]{64}$"))' \
"$artifact_dir/shared-image-artifact.json"
)"
{
echo "archive_sha256=$archive_sha256"
echo "artifact_name=$artifact_name"
echo "artifact_path=$artifact_dir"
} >> "$GITHUB_OUTPUT"
- name: Upload root Dockerfile image artifact
id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ steps.image_artifact.outputs.artifact_name }}
path: ${{ steps.image_artifact.outputs.artifact_path }}
if-no-files-found: error
compression-level: 0
retention-days: 7
candidate_receipt:
name: Emit immutable candidate receipt
needs: [validate_release_plan, candidate_artifacts, root_image]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
actions: read
contents: read
steps:
- name: Checkout trusted receipt tooling
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.sha }}
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Verify exact producer workflow attempt
id: producer
env:
GH_TOKEN: ${{ github.token }}
EXPECTED_RUN_TITLE: Release Candidate Artifacts ${{ inputs.dispatch_id }}
EXPECTED_WORKFLOW_PATH: .github/workflows/release-candidate-artifacts.yml
EXPECTED_WORKFLOW_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
run_json="$(
gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}"
)"
workflow_id="$(
jq -er \
--arg attempt "$GITHUB_RUN_ATTEMPT" \
--arg path "$EXPECTED_WORKFLOW_PATH" \
--arg run_id "$GITHUB_RUN_ID" \
--arg sha "$EXPECTED_WORKFLOW_SHA" \
--arg title "$EXPECTED_RUN_TITLE" \
'
select(
(.id | tostring) == $run_id and
(.run_attempt | tostring) == $attempt and
.event == "workflow_dispatch" and
.display_title == $title and
.path == $path and
.head_sha == $sha
)
| .workflow_id
' <<< "$run_json"
)"
workflow_json="$(
gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow_id}"
)"
jq -e \
--arg id "$workflow_id" \
--arg path "$EXPECTED_WORKFLOW_PATH" \
'(.id | tostring) == $id and .path == $path and .state == "active"' \
<<< "$workflow_json" >/dev/null
echo "workflow_id=$workflow_id" >> "$GITHUB_OUTPUT"
- name: Create canonical CandidateReceiptLock
env:
DOCKER_IMAGE_ARCHIVE_SHA256: ${{ needs.candidate_artifacts.outputs.shared_image_archive_sha256 }}
DOCKER_IMAGE_ARTIFACT_DIGEST: ${{ needs.candidate_artifacts.outputs.shared_image_artifact_digest }}
DOCKER_IMAGE_ARTIFACT_ID: ${{ needs.candidate_artifacts.outputs.shared_image_artifact_id }}
DOCKER_IMAGE_ARTIFACT_NAME: ${{ needs.candidate_artifacts.outputs.shared_image_artifact_name }}
PACKAGE_ARTIFACT_DIGEST: ${{ needs.candidate_artifacts.outputs.package_artifact_digest }}
PACKAGE_ARTIFACT_ID: ${{ needs.candidate_artifacts.outputs.package_artifact_id }}
PACKAGE_ARTIFACT_NAME: ${{ needs.candidate_artifacts.outputs.package_artifact_name }}
PACKAGE_SHA256: ${{ needs.candidate_artifacts.outputs.package_sha256 }}
PLUGIN_REGISTRY_ARTIFACT_DIGEST: ${{ needs.candidate_artifacts.outputs.prepublish_plugin_registry_artifact_digest }}
PLUGIN_REGISTRY_ARTIFACT_ID: ${{ needs.candidate_artifacts.outputs.prepublish_plugin_registry_artifact_id }}
PLUGIN_REGISTRY_ARTIFACT_NAME: ${{ needs.candidate_artifacts.outputs.prepublish_plugin_registry_artifact_name }}
PLUGIN_REGISTRY_MANIFEST_SHA256: ${{ needs.candidate_artifacts.outputs.prepublish_plugin_registry_manifest_sha256 }}
RELEASE_PLAN_DIGEST: ${{ needs.validate_release_plan.outputs.release_plan_digest }}
ROOT_IMAGE_ARCHIVE_SHA256: ${{ needs.root_image.outputs.archive_sha256 }}
ROOT_IMAGE_ARTIFACT_DIGEST: ${{ needs.root_image.outputs.artifact_digest }}
ROOT_IMAGE_ARTIFACT_ID: ${{ needs.root_image.outputs.artifact_id }}
ROOT_IMAGE_ARTIFACT_NAME: ${{ needs.root_image.outputs.artifact_name }}
WORKFLOW_ID: ${{ steps.producer.outputs.workflow_id }}
WORKFLOW_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
mkdir -p .artifacts/candidate-receipt
node --input-type=module <<'NODE'
import fs from "node:fs";
import {
canonicalCandidateReceiptLockJson,
createCandidateReceiptLock,
} from "./scripts/release-candidate-receipt-contract.mjs";
const required = (name) => {
const value = process.env[name] ?? "";
if (!value) {
throw new Error(`${name} is required`);
}
return value;
};
const prefixedDigest = (name) => {
const value = required(name);
return value.startsWith("sha256:") ? value : `sha256:${value}`;
};
const artifact = (prefix, contentDigestName) => ({
artifact_digest: prefixedDigest(`${prefix}_ARTIFACT_DIGEST`),
artifact_id: required(`${prefix}_ARTIFACT_ID`),
artifact_name: required(`${prefix}_ARTIFACT_NAME`),
content_digest: prefixedDigest(contentDigestName),
});
const lock = createCandidateReceiptLock({
schema: "openclaw.candidate-receipt.v1",
release_plan_digest: required("RELEASE_PLAN_DIGEST"),
producer: {
repository: required("GITHUB_REPOSITORY"),
workflow_path: ".github/workflows/release-candidate-artifacts.yml",
workflow_id: required("WORKFLOW_ID"),
workflow_sha: required("WORKFLOW_SHA"),
run_id: required("GITHUB_RUN_ID"),
run_attempt: required("GITHUB_RUN_ATTEMPT"),
},
artifacts: {
docker_image: artifact("DOCKER_IMAGE", "DOCKER_IMAGE_ARCHIVE_SHA256"),
package: artifact("PACKAGE", "PACKAGE_SHA256"),
plugin_registry: artifact(
"PLUGIN_REGISTRY",
"PLUGIN_REGISTRY_MANIFEST_SHA256",
),
root_image: artifact("ROOT_IMAGE", "ROOT_IMAGE_ARCHIVE_SHA256"),
},
});
fs.writeFileSync(
".artifacts/candidate-receipt/candidate-receipt-lock.json",
canonicalCandidateReceiptLockJson(lock),
"ascii",
);
NODE
- name: Upload CandidateReceiptLock
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-candidate-receipt-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/candidate-receipt/candidate-receipt-lock.json
if-no-files-found: error
retention-days: 7
@@ -0,0 +1,44 @@
export type CandidateReceiptArtifact = {
artifact_digest: string;
artifact_id: string;
artifact_name: string;
content_digest: string;
};
export type CandidateReceipt = {
schema: "openclaw.candidate-receipt.v1";
release_plan_digest: string;
producer: {
repository: "openclaw/openclaw";
workflow_path: ".github/workflows/release-candidate-artifacts.yml";
workflow_id: string;
workflow_sha: string;
run_id: string;
run_attempt: string;
};
artifacts: {
docker_image: CandidateReceiptArtifact;
package: CandidateReceiptArtifact;
plugin_registry: CandidateReceiptArtifact;
root_image: CandidateReceiptArtifact;
};
};
export type CandidateReceiptLock = {
schema: "openclaw.candidate-receipt-lock.v1";
digest: string;
receipt: CandidateReceipt;
};
export const CANDIDATE_RECEIPT_SCHEMA: "openclaw.candidate-receipt.v1";
export const CANDIDATE_RECEIPT_LOCK_SCHEMA: "openclaw.candidate-receipt-lock.v1";
export const CANDIDATE_RECEIPT_CANONICALIZATION: "ascii-sorted-compact-json-trailing-newline-v1";
export const CANDIDATE_RECEIPT_MAX_BYTES: number;
export const CANDIDATE_RECEIPT_WORKFLOW_PATH: ".github/workflows/release-candidate-artifacts.yml";
export function validateCandidateReceipt(value: unknown): CandidateReceipt;
export function canonicalCandidateReceiptJson(value: unknown): string;
export function candidateReceiptDigest(value: unknown): string;
export function createCandidateReceiptLock(value: unknown): CandidateReceiptLock;
export function validateCandidateReceiptLock(value: unknown): CandidateReceiptLock;
export function canonicalCandidateReceiptLockJson(value: unknown): string;
export function parseCandidateReceiptLockJson(text: string): CandidateReceiptLock;
@@ -0,0 +1,257 @@
import { createHash } from "node:crypto";
import { parseDocument } from "yaml";
import { isRecord } from "./lib/record-shared.mjs";
export const CANDIDATE_RECEIPT_SCHEMA = "openclaw.candidate-receipt.v1";
export const CANDIDATE_RECEIPT_LOCK_SCHEMA = "openclaw.candidate-receipt-lock.v1";
export const CANDIDATE_RECEIPT_CANONICALIZATION = "ascii-sorted-compact-json-trailing-newline-v1";
export const CANDIDATE_RECEIPT_MAX_BYTES = 16 * 1024;
export const CANDIDATE_RECEIPT_WORKFLOW_PATH = ".github/workflows/release-candidate-artifacts.yml";
const REPOSITORY = "openclaw/openclaw";
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const POSITIVE_DECIMAL_PATTERN = /^[1-9][0-9]*$/u;
const ASCII_PATTERN = /^[\x20-\x7e]+$/u;
const ARTIFACT_KEYS = ["docker_image", "package", "plugin_registry", "root_image"];
const compareAscii = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
function fail(message) {
throw new Error(message);
}
function exactKeys(value, keys, label) {
const actual = Object.keys(value).toSorted(compareAscii);
const expected = [...keys].toSorted(compareAscii);
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
fail(`${label} keys must be exactly: ${expected.join(", ")}`);
}
}
function asciiString(value, label) {
if (typeof value !== "string" || !ASCII_PATTERN.test(value)) {
fail(`${label} must be a non-empty printable ASCII string`);
}
return value;
}
function positiveDecimal(value, label) {
const normalized = asciiString(value, label);
if (!POSITIVE_DECIMAL_PATTERN.test(normalized)) {
fail(`${label} must be a positive decimal integer string`);
}
return normalized;
}
function sha(value, label) {
const normalized = asciiString(value, label);
if (!SHA_PATTERN.test(normalized)) {
fail(`${label} must be a lowercase 40-character commit SHA`);
}
return normalized;
}
function digest(value, label) {
if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) {
fail(`${label} must be sha256:<64 lowercase hex characters>`);
}
return value;
}
function canonicalize(value) {
if (Array.isArray(value)) {
return value.map(canonicalize);
}
if (isRecord(value)) {
return Object.fromEntries(
Object.keys(value)
.toSorted(compareAscii)
.map((key) => [key, canonicalize(value[key])]),
);
}
return value;
}
function canonicalAsciiJson(value) {
const json = `${JSON.stringify(canonicalize(value))}\n`;
if (!/^[\x20-\x7e]+\n$/u.test(json)) {
fail("canonical JSON must be printable ASCII with exactly one trailing newline");
}
return json;
}
function validateArtifact(value, label) {
if (!isRecord(value)) {
fail(`${label} must be an object`);
}
exactKeys(value, ["artifact_digest", "artifact_id", "artifact_name", "content_digest"], label);
return {
artifact_digest: digest(value.artifact_digest, `${label} artifact_digest`),
artifact_id: positiveDecimal(value.artifact_id, `${label} artifact_id`),
artifact_name: asciiString(value.artifact_name, `${label} artifact_name`),
content_digest: digest(value.content_digest, `${label} content_digest`),
};
}
export function validateCandidateReceipt(value) {
if (!isRecord(value)) {
fail("candidate receipt must be an object");
}
exactKeys(value, ["artifacts", "producer", "release_plan_digest", "schema"], "candidate receipt");
if (value.schema !== CANDIDATE_RECEIPT_SCHEMA) {
fail(`candidate receipt schema must be ${CANDIDATE_RECEIPT_SCHEMA}`);
}
if (!isRecord(value.producer)) {
fail("candidate receipt producer must be an object");
}
exactKeys(
value.producer,
["repository", "run_attempt", "run_id", "workflow_id", "workflow_path", "workflow_sha"],
"candidate receipt producer",
);
if (!isRecord(value.artifacts)) {
fail("candidate receipt artifacts must be an object");
}
exactKeys(value.artifacts, ARTIFACT_KEYS, "candidate receipt artifacts");
const receipt = {
schema: CANDIDATE_RECEIPT_SCHEMA,
release_plan_digest: digest(value.release_plan_digest, "candidate receipt release_plan_digest"),
producer: {
repository: asciiString(value.producer.repository, "candidate receipt producer repository"),
workflow_path: asciiString(
value.producer.workflow_path,
"candidate receipt producer workflow_path",
),
workflow_id: positiveDecimal(
value.producer.workflow_id,
"candidate receipt producer workflow_id",
),
workflow_sha: sha(value.producer.workflow_sha, "candidate receipt producer workflow_sha"),
run_id: positiveDecimal(value.producer.run_id, "candidate receipt producer run_id"),
run_attempt: positiveDecimal(
value.producer.run_attempt,
"candidate receipt producer run_attempt",
),
},
artifacts: Object.fromEntries(
ARTIFACT_KEYS.map((key) => [
key,
validateArtifact(value.artifacts[key], `candidate receipt artifacts.${key}`),
]),
),
};
if (receipt.producer.repository !== REPOSITORY) {
fail(`candidate receipt producer repository must be ${REPOSITORY}`);
}
if (receipt.producer.workflow_path !== CANDIDATE_RECEIPT_WORKFLOW_PATH) {
fail(`candidate receipt producer workflow_path must be ${CANDIDATE_RECEIPT_WORKFLOW_PATH}`);
}
const artifactIds = ARTIFACT_KEYS.map((key) => receipt.artifacts[key].artifact_id);
if (new Set(artifactIds).size !== artifactIds.length) {
fail("candidate receipt artifact IDs must be unique");
}
const expectedNameSuffix = `-${receipt.producer.run_id}-${receipt.producer.run_attempt}`;
for (const key of ARTIFACT_KEYS) {
if (!receipt.artifacts[key].artifact_name.endsWith(expectedNameSuffix)) {
fail(`candidate receipt artifacts.${key} name must bind the producer run attempt`);
}
}
const exactArtifactNames = {
package: `docker-e2e-package${expectedNameSuffix}`,
plugin_registry: `docker-e2e-prepublish-plugin-registry${expectedNameSuffix}`,
root_image: `release-candidate-root-image${expectedNameSuffix}`,
};
for (const [key, expectedName] of Object.entries(exactArtifactNames)) {
if (receipt.artifacts[key].artifact_name !== expectedName) {
fail(`candidate receipt artifacts.${key} name does not match its artifact kind`);
}
}
if (
!new RegExp(
`^docker-e2e-shared-images-release-candidate-[a-f0-9]{12}${expectedNameSuffix}$`,
"u",
).test(receipt.artifacts.docker_image.artifact_name)
) {
fail("candidate receipt artifacts.docker_image name does not match its artifact kind");
}
if (Buffer.byteLength(canonicalAsciiJson(receipt), "ascii") > CANDIDATE_RECEIPT_MAX_BYTES) {
fail(`candidate receipt exceeds ${CANDIDATE_RECEIPT_MAX_BYTES} bytes`);
}
return receipt;
}
export function canonicalCandidateReceiptJson(value) {
return canonicalAsciiJson(validateCandidateReceipt(value));
}
export function candidateReceiptDigest(value) {
return `sha256:${createHash("sha256")
.update(canonicalCandidateReceiptJson(value), "ascii")
.digest("hex")}`;
}
export function createCandidateReceiptLock(value) {
const receipt = validateCandidateReceipt(value);
return {
schema: CANDIDATE_RECEIPT_LOCK_SCHEMA,
digest: candidateReceiptDigest(receipt),
receipt,
};
}
export function validateCandidateReceiptLock(value) {
if (!isRecord(value)) {
fail("candidate receipt lock must be an object");
}
exactKeys(value, ["digest", "receipt", "schema"], "candidate receipt lock");
if (value.schema !== CANDIDATE_RECEIPT_LOCK_SCHEMA) {
fail(`candidate receipt lock schema must be ${CANDIDATE_RECEIPT_LOCK_SCHEMA}`);
}
const receipt = validateCandidateReceipt(value.receipt);
const receiptDigest = digest(value.digest, "candidate receipt lock digest");
if (receiptDigest !== candidateReceiptDigest(receipt)) {
fail("candidate receipt lock digest does not match its canonical receipt");
}
return { schema: CANDIDATE_RECEIPT_LOCK_SCHEMA, digest: receiptDigest, receipt };
}
export function canonicalCandidateReceiptLockJson(value) {
return canonicalAsciiJson(validateCandidateReceiptLock(value));
}
export function parseCandidateReceiptLockJson(text) {
if (
typeof text !== "string" ||
Buffer.byteLength(text, "utf8") > CANDIDATE_RECEIPT_MAX_BYTES + 4096
) {
fail("candidate receipt lock JSON is missing or too large");
}
if (!/^[\x20-\x7e]+\n$/u.test(text)) {
fail(
"candidate receipt lock JSON must be compact printable ASCII with exactly one trailing LF",
);
}
const document = parseDocument(text, { strict: true, uniqueKeys: true });
if (document.errors.length > 0) {
const duplicate = document.errors.find((error) =>
error.message.includes("keys must be unique"),
);
fail(
duplicate
? "candidate receipt JSON contains a duplicate key"
: `candidate receipt lock JSON is invalid: ${document.errors[0].message}`,
);
}
let value;
try {
value = JSON.parse(text);
} catch (error) {
throw new Error("candidate receipt lock JSON is invalid JSON", { cause: error });
}
const lock = validateCandidateReceiptLock(value);
if (text !== canonicalCandidateReceiptLockJson(lock)) {
fail("candidate receipt lock JSON does not use canonical bytes");
}
return lock;
}
@@ -0,0 +1,47 @@
import type { CandidateReceiptLock } from "./release-candidate-receipt-contract.mjs";
type RunGh = (args: string[]) => string;
export type CandidateReceiptLocatorOptions = {
dispatchId: string;
releasePlanDigest: string;
repo: string;
runAttempt?: string;
runGh?: RunGh;
runId?: string;
sleep?: (milliseconds: number) => Promise<void>;
timeoutMs?: number;
workflowId: string;
workflowSha: string;
};
export function validateCandidateReceiptProvenance(params: {
artifacts: unknown;
expectedDispatchId: string;
expectedReleasePlanDigest: string;
expectedRunAttempt: string;
expectedRunId: string;
expectedWorkflowId: string;
expectedWorkflowSha: string;
lock: CandidateReceiptLock;
run: unknown;
workflow: unknown;
}): CandidateReceiptLock;
export function runCandidateReceiptGh(
args: string[],
params?: {
execFileSyncImpl?: (
command: string,
args: string[],
options: {
encoding: "utf8";
killSignal: "SIGKILL";
maxBuffer: number;
timeout: number;
},
) => string;
},
): string;
export function locateCandidateReceipt(
options: CandidateReceiptLocatorOptions,
): Promise<CandidateReceiptLock>;
@@ -0,0 +1,476 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { isRecord } from "./lib/record-shared.mjs";
import {
canonicalCandidateReceiptLockJson,
CANDIDATE_RECEIPT_WORKFLOW_PATH,
parseCandidateReceiptLockJson,
validateCandidateReceiptLock,
type CandidateReceiptLock,
} from "./release-candidate-receipt-contract.mjs";
type JsonRecord = Record<string, unknown>;
type RunGh = (args: string[]) => string;
export type CandidateReceiptLocatorOptions = {
dispatchId: string;
releasePlanDigest: string;
repo: string;
runAttempt?: string;
runGh?: RunGh;
runId?: string;
sleep?: (milliseconds: number) => Promise<void>;
timeoutMs?: number;
workflowId: string;
workflowSha: string;
};
const REPOSITORY = "openclaw/openclaw";
const RUN_NAME_PREFIX = "Release Candidate Artifacts";
const RECEIPT_FILE_NAME = "candidate-receipt-lock.json";
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const POSITIVE_DECIMAL_PATTERN = /^[1-9][0-9]*$/u;
const DISPATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/u;
const GH_COMMAND_TIMEOUT_MS = 60_000;
const DEFAULT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
const POLL_INTERVAL_MS = 15_000;
function fail(message: string): never {
throw new Error(message);
}
function parseJson(raw: string, label: string): unknown {
try {
return JSON.parse(raw) as unknown;
} catch (error) {
throw new Error(`${label} returned invalid JSON`, { cause: error });
}
}
function record(value: unknown, label: string): JsonRecord {
if (!isRecord(value)) {
fail(`${label} must be an object`);
}
return value;
}
function requiredString(value: unknown, label: string): string {
if (typeof value !== "string" || value.length === 0) {
fail(`${label} is missing`);
}
return value;
}
function positiveDecimal(value: unknown, label: string): string {
const normalized =
typeof value === "number" && Number.isSafeInteger(value) ? String(value) : value;
if (typeof normalized !== "string" || !POSITIVE_DECIMAL_PATTERN.test(normalized)) {
fail(`${label} must be a positive decimal integer`);
}
return normalized;
}
function sha(value: unknown, label: string): string {
const normalized = requiredString(value, label);
if (!SHA_PATTERN.test(normalized)) {
fail(`${label} must be a lowercase full commit SHA`);
}
return normalized;
}
function digest(value: unknown, label: string): string {
const normalized = requiredString(value, label);
if (!DIGEST_PATTERN.test(normalized)) {
fail(`${label} must be a prefixed lowercase SHA-256 digest`);
}
return normalized;
}
function requireOptions(options: CandidateReceiptLocatorOptions) {
if (options.repo !== REPOSITORY) {
fail(`candidate receipt repository must be ${REPOSITORY}`);
}
if (!DISPATCH_ID_PATTERN.test(options.dispatchId)) {
fail("candidate receipt dispatch id is invalid");
}
const runPairCount =
Number(options.runId !== undefined) + Number(options.runAttempt !== undefined);
if (runPairCount === 1) {
fail("candidate receipt exact run id and attempt must be supplied together");
}
return {
...options,
releasePlanDigest: digest(options.releasePlanDigest, "release plan digest"),
runAttempt:
options.runAttempt === undefined
? undefined
: positiveDecimal(options.runAttempt, "candidate receipt run attempt"),
runId:
options.runId === undefined
? undefined
: positiveDecimal(options.runId, "candidate receipt run id"),
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
workflowId: positiveDecimal(options.workflowId, "candidate receipt workflow id"),
workflowSha: sha(options.workflowSha, "candidate receipt workflow SHA"),
};
}
function artifactRecords(value: unknown): JsonRecord[] {
const root = record(value, "candidate receipt artifact response");
if (!Array.isArray(root.artifacts)) {
fail("candidate receipt artifact response must contain artifacts");
}
return root.artifacts.map((entry, index) =>
record(entry, `candidate receipt artifact response artifacts[${index}]`),
);
}
function validateArtifactMetadata(
artifact: JsonRecord,
expected: {
digest: string;
id: string;
name: string;
runId: string;
},
) {
const workflowRun = record(artifact.workflow_run, `${expected.name} workflow_run`);
if (
positiveDecimal(artifact.id, `${expected.name} artifact id`) !== expected.id ||
requiredString(artifact.name, `${expected.name} artifact name`) !== expected.name ||
digest(artifact.digest, `${expected.name} artifact digest`) !== expected.digest ||
artifact.expired !== false ||
positiveDecimal(workflowRun.id, `${expected.name} workflow run id`) !== expected.runId
) {
fail(`${expected.name} metadata does not match the candidate receipt`);
}
}
export function validateCandidateReceiptProvenance(params: {
artifacts: unknown;
expectedDispatchId: string;
expectedReleasePlanDigest: string;
expectedRunAttempt: string;
expectedRunId: string;
expectedWorkflowId: string;
expectedWorkflowSha: string;
lock: CandidateReceiptLock;
run: unknown;
workflow: unknown;
}) {
const lock = validateCandidateReceiptLock(params.lock);
const run = record(params.run, "candidate receipt run");
const workflow = record(params.workflow, "candidate receipt workflow");
const expectedTitle = `${RUN_NAME_PREFIX} ${params.expectedDispatchId}`;
if (
positiveDecimal(run.id, "candidate receipt run id") !== params.expectedRunId ||
positiveDecimal(run.run_attempt, "candidate receipt run attempt") !==
params.expectedRunAttempt ||
positiveDecimal(run.workflow_id, "candidate receipt run workflow id") !==
params.expectedWorkflowId ||
sha(run.head_sha, "candidate receipt run head SHA") !== params.expectedWorkflowSha ||
requiredString(run.path, "candidate receipt run path") !== CANDIDATE_RECEIPT_WORKFLOW_PATH ||
requiredString(run.display_title, "candidate receipt run title") !== expectedTitle ||
run.event !== "workflow_dispatch" ||
run.status !== "completed" ||
run.conclusion !== "success"
) {
fail("candidate receipt run does not match the exact successful producer attempt");
}
if (
positiveDecimal(workflow.id, "candidate receipt workflow id") !== params.expectedWorkflowId ||
requiredString(workflow.path, "candidate receipt workflow path") !==
CANDIDATE_RECEIPT_WORKFLOW_PATH ||
workflow.state !== "active"
) {
fail("candidate receipt workflow identity does not match the canonical active workflow");
}
const receipt = lock.receipt;
if (
receipt.release_plan_digest !== params.expectedReleasePlanDigest ||
receipt.producer.repository !== REPOSITORY ||
receipt.producer.workflow_path !== CANDIDATE_RECEIPT_WORKFLOW_PATH ||
receipt.producer.workflow_id !== params.expectedWorkflowId ||
receipt.producer.workflow_sha !== params.expectedWorkflowSha ||
receipt.producer.run_id !== params.expectedRunId ||
receipt.producer.run_attempt !== params.expectedRunAttempt
) {
fail("candidate receipt payload does not match the requested producer provenance");
}
const artifacts = artifactRecords(params.artifacts);
for (const artifact of Object.values(receipt.artifacts)) {
const metadata = artifacts.find(
(entry) =>
positiveDecimal(entry.id, "candidate receipt artifact id") === artifact.artifact_id,
);
if (!metadata) {
fail(`candidate receipt artifact ${artifact.artifact_id} is missing from the producer run`);
}
validateArtifactMetadata(metadata, {
digest: artifact.artifact_digest,
id: artifact.artifact_id,
name: artifact.artifact_name,
runId: params.expectedRunId,
});
}
return lock;
}
export function runCandidateReceiptGh(
args: string[],
params: { execFileSyncImpl?: typeof runGhCommand } = {},
): string {
const execFileSyncImpl = params.execFileSyncImpl ?? runGhCommand;
return execFileSyncImpl("gh", args, {
encoding: "utf8",
killSignal: "SIGKILL",
maxBuffer: 32 * 1024 * 1024,
timeout: GH_COMMAND_TIMEOUT_MS,
});
}
function runGhCommand(
command: string,
args: string[],
options: {
encoding: "utf8";
killSignal: "SIGKILL";
maxBuffer: number;
timeout: number;
},
) {
return execFileSync(command, args, options);
}
async function pollUntil<T>(
deadline: number,
poll: () => T | undefined,
sleep: (milliseconds: number) => Promise<void>,
timeoutMessage: string,
): Promise<T> {
while (Date.now() <= deadline) {
const result = poll();
if (result !== undefined) {
return result;
}
await sleep(Math.min(POLL_INTERVAL_MS, Math.max(1, deadline - Date.now())));
}
fail(timeoutMessage);
}
function discoverRun(
api: (endpoint: string) => unknown,
params: {
dispatchId: string;
workflowId: string;
workflowSha: string;
},
): { runAttempt: string; runId: string } | undefined {
const response = record(
api(`actions/workflows/${params.workflowId}/runs?event=workflow_dispatch&per_page=100`),
"candidate receipt workflow runs response",
);
if (!Array.isArray(response.workflow_runs)) {
fail("candidate receipt workflow runs response must contain workflow_runs");
}
const expectedTitle = `${RUN_NAME_PREFIX} ${params.dispatchId}`;
const matches = response.workflow_runs
.map((entry, index) => record(entry, `candidate receipt workflow_runs[${index}]`))
.filter(
(run) =>
run.display_title === expectedTitle &&
run.head_sha === params.workflowSha &&
run.workflow_id !== undefined &&
positiveDecimal(run.workflow_id, "candidate receipt discovered workflow id") ===
params.workflowId,
);
if (matches.length === 0) {
return undefined;
}
if (matches.length !== 1) {
fail("candidate receipt dispatch id matched multiple workflow runs");
}
return {
runAttempt: positiveDecimal(matches[0]!.run_attempt, "candidate receipt run attempt"),
runId: positiveDecimal(matches[0]!.id, "candidate receipt run id"),
};
}
function requireCurrentAttempt(
api: (endpoint: string) => unknown,
runId: string,
runAttempt: string,
) {
const latestRun = record(api(`actions/runs/${runId}`), "candidate receipt latest run");
if (
positiveDecimal(latestRun.run_attempt, "candidate receipt latest run attempt") !== runAttempt
) {
fail("candidate receipt producer attempt was superseded by a rerun");
}
}
export async function locateCandidateReceipt(
rawOptions: CandidateReceiptLocatorOptions,
): Promise<CandidateReceiptLock> {
const options = requireOptions(rawOptions);
if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0) {
fail("candidate receipt timeout must be a positive integer");
}
const runGh = options.runGh ?? runCandidateReceiptGh;
const sleep =
options.sleep ??
((milliseconds: number) =>
new Promise((resolve) => {
setTimeout(resolve, milliseconds);
}));
const api = (endpoint: string): unknown =>
parseJson(runGh(["api", `repos/${options.repo}/${endpoint}`, "--method", "GET"]), endpoint);
const deadline = Date.now() + options.timeoutMs;
const workflow = api(`actions/workflows/${options.workflowId}`);
const workflowRecord = record(workflow, "candidate receipt workflow");
if (
positiveDecimal(workflowRecord.id, "candidate receipt workflow id") !== options.workflowId ||
workflowRecord.path !== CANDIDATE_RECEIPT_WORKFLOW_PATH ||
workflowRecord.state !== "active"
) {
fail("candidate receipt workflow identity does not match the canonical active workflow");
}
const exactRun =
options.runId && options.runAttempt
? { runAttempt: options.runAttempt, runId: options.runId }
: await pollUntil(
deadline,
() =>
discoverRun(api, {
dispatchId: options.dispatchId,
workflowId: options.workflowId,
workflowSha: options.workflowSha,
}),
sleep,
"timed out locating the candidate receipt producer run",
);
const run = await pollUntil(
deadline,
() => {
const current = record(
api(`actions/runs/${exactRun.runId}/attempts/${exactRun.runAttempt}`),
"candidate receipt run attempt",
);
if (current.status !== "completed") {
return undefined;
}
if (current.conclusion !== "success") {
fail(`candidate receipt producer concluded ${String(current.conclusion)}`);
}
return current;
},
sleep,
"timed out waiting for the candidate receipt producer",
);
requireCurrentAttempt(api, exactRun.runId, exactRun.runAttempt);
const artifacts = api(`actions/runs/${exactRun.runId}/artifacts?per_page=100`);
const receiptArtifactName = `release-candidate-receipt-${exactRun.runId}-${exactRun.runAttempt}`;
const receiptArtifact = artifactRecords(artifacts).find(
(entry) => entry.name === receiptArtifactName,
);
if (!receiptArtifact) {
fail("candidate receipt lock artifact is missing from the producer run");
}
validateArtifactMetadata(receiptArtifact, {
digest: digest(receiptArtifact.digest, "candidate receipt lock artifact digest"),
id: positiveDecimal(receiptArtifact.id, "candidate receipt lock artifact id"),
name: receiptArtifactName,
runId: exactRun.runId,
});
const downloadDir = mkdtempSync(join(tmpdir(), "openclaw-candidate-receipt-"));
try {
runGh([
"run",
"download",
exactRun.runId,
"--repo",
options.repo,
"--name",
receiptArtifactName,
"--dir",
downloadDir,
]);
const parsedLock = parseCandidateReceiptLockJson(
readFileSync(join(downloadDir, RECEIPT_FILE_NAME), "utf8"),
);
const validatedLock = validateCandidateReceiptProvenance({
artifacts,
expectedDispatchId: options.dispatchId,
expectedReleasePlanDigest: options.releasePlanDigest,
expectedRunAttempt: exactRun.runAttempt,
expectedRunId: exactRun.runId,
expectedWorkflowId: options.workflowId,
expectedWorkflowSha: options.workflowSha,
lock: parsedLock,
run,
workflow,
});
// A rerun invalidates the just-read artifact namespace even if it starts
// between the first attempt check and the final receipt read.
requireCurrentAttempt(api, exactRun.runId, exactRun.runAttempt);
return validatedLock;
} finally {
rmSync(downloadDir, { force: true, recursive: true });
}
}
function parseArgs(argv: string[]): CandidateReceiptLocatorOptions {
const options: Record<string, string> = {};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (
arg === "--dispatch-id" ||
arg === "--release-plan-digest" ||
arg === "--repo" ||
arg === "--run-attempt" ||
arg === "--run-id" ||
arg === "--timeout-seconds" ||
arg === "--workflow-id" ||
arg === "--workflow-sha"
) {
options[arg] = argv[(index += 1)] ?? "";
} else {
fail(`unknown argument: ${arg}`);
}
}
const timeoutSeconds = options["--timeout-seconds"];
return {
dispatchId: options["--dispatch-id"] ?? "",
releasePlanDigest: options["--release-plan-digest"] ?? "",
repo: options["--repo"] ?? "",
...(options["--run-attempt"] ? { runAttempt: options["--run-attempt"] } : {}),
...(options["--run-id"] ? { runId: options["--run-id"] } : {}),
...(timeoutSeconds ? { timeoutMs: Number.parseInt(timeoutSeconds, 10) * 1000 } : {}),
workflowId: options["--workflow-id"] ?? "",
workflowSha: options["--workflow-sha"] ?? "",
};
}
async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
const lock = await locateCandidateReceipt(parseArgs(argv));
process.stdout.write(canonicalCandidateReceiptLockJson(lock));
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
void main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
console.error("[release-candidate-receipt-locator] FAILED (exit 1)");
process.exitCode = 1;
});
}
+5
View File
@@ -2273,6 +2273,7 @@ const SEMANTIC_TOOLING_TARGET_PATTERNS: Array<[RegExp, string[]]> = [
/^\.github\/workflows\/full-release-validation\.yml$/u,
["src/dockerfile.test.ts", packageAcceptance, pluginPrerelease],
],
[/^\.github\/workflows\/release-candidate-artifacts\.yml$/u, ["release-candidate-receipt"]],
[
/^\.github\/workflows\/openclaw-release-checks\.yml$/u,
[packageAcceptance, crossOsReleaseChecks, pluginPrerelease, installDocker],
@@ -2424,6 +2425,10 @@ const SEMANTIC_TOOLING_TARGET_PATTERNS: Array<[RegExp, string[]]> = [
[/^apps\/ios\/fastlane\/Fastfile$/u, ["ios-release-fastlane-gates"]],
[/^scripts\/ios-release-cut\.(?:sh|ts)$/u, ["ios-release-plan"]],
[/^scripts\/ios-release-prepare\.sh$/u, ["ios-release-prepare", "ios-release-wrapper-args"]],
[
/^scripts\/release-candidate-receipt-(?:contract|locator)\.(?:d\.mts|mjs|mts)$/u,
["release-candidate-receipt"],
],
[
/^scripts\/lib\/bundled-runtime-sidecar-paths\.json$/u,
[
@@ -0,0 +1 @@
{"digest":"sha256:2bc324c84b0aeee94c65c144f53580bfa4f74178e0f632560b790e7213685915","receipt":{"artifacts":{"docker_image":{"artifact_digest":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","artifact_id":"103","artifact_name":"docker-e2e-shared-images-release-candidate-aaaaaaaaaaaa-12345-2","content_digest":"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"},"package":{"artifact_digest":"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","artifact_id":"101","artifact_name":"docker-e2e-package-12345-2","content_digest":"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"},"plugin_registry":{"artifact_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111","artifact_id":"102","artifact_name":"docker-e2e-prepublish-plugin-registry-12345-2","content_digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222"},"root_image":{"artifact_digest":"sha256:3333333333333333333333333333333333333333333333333333333333333333","artifact_id":"104","artifact_name":"release-candidate-root-image-12345-2","content_digest":"sha256:4444444444444444444444444444444444444444444444444444444444444444"}},"producer":{"repository":"openclaw/openclaw","run_attempt":"2","run_id":"12345","workflow_id":"987","workflow_path":".github/workflows/release-candidate-artifacts.yml","workflow_sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},"release_plan_digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","schema":"openclaw.candidate-receipt.v1"},"schema":"openclaw.candidate-receipt-lock.v1"}
+1
View File
@@ -0,0 +1 @@
{"artifacts":{"docker_image":{"artifact_digest":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","artifact_id":"103","artifact_name":"docker-e2e-shared-images-release-candidate-aaaaaaaaaaaa-12345-2","content_digest":"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"},"package":{"artifact_digest":"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","artifact_id":"101","artifact_name":"docker-e2e-package-12345-2","content_digest":"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"},"plugin_registry":{"artifact_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111","artifact_id":"102","artifact_name":"docker-e2e-prepublish-plugin-registry-12345-2","content_digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222"},"root_image":{"artifact_digest":"sha256:3333333333333333333333333333333333333333333333333333333333333333","artifact_id":"104","artifact_name":"release-candidate-root-image-12345-2","content_digest":"sha256:4444444444444444444444444444444444444444444444444444444444444444"}},"producer":{"repository":"openclaw/openclaw","run_attempt":"2","run_id":"12345","workflow_id":"987","workflow_path":".github/workflows/release-candidate-artifacts.yml","workflow_sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},"release_plan_digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","schema":"openclaw.candidate-receipt.v1"}
@@ -0,0 +1,475 @@
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { parse } from "yaml";
import {
canonicalCandidateReceiptJson,
canonicalCandidateReceiptLockJson,
CANDIDATE_RECEIPT_CANONICALIZATION,
createCandidateReceiptLock,
parseCandidateReceiptLockJson,
validateCandidateReceipt,
} from "../../scripts/release-candidate-receipt-contract.mjs";
import {
locateCandidateReceipt,
runCandidateReceiptGh,
validateCandidateReceiptProvenance,
} from "../../scripts/release-candidate-receipt-locator.mts";
const fixtureDir = resolve("test/fixtures");
const sourceText = readFileSync(resolve(fixtureDir, "candidate-receipt-v1.source.json"), "utf8");
const lockText = readFileSync(
resolve(fixtureDir, "candidate-receipt-lock-v1.compatibility.json"),
"utf8",
);
const sourceFixture = JSON.parse(sourceText) as Record<string, unknown>;
const lockFixture = parseCandidateReceiptLockJson(lockText);
const runId = lockFixture.receipt.producer.run_id;
const runAttempt = lockFixture.receipt.producer.run_attempt;
const workflowId = lockFixture.receipt.producer.workflow_id;
const workflowSha = lockFixture.receipt.producer.workflow_sha;
const dispatchId = "candidate-2026.8.1-beta.3";
const runTitle = `Release Candidate Artifacts ${dispatchId}`;
function runFixture(overrides: Record<string, unknown> = {}) {
return {
conclusion: "success",
display_title: runTitle,
event: "workflow_dispatch",
head_sha: workflowSha,
id: Number(runId),
path: ".github/workflows/release-candidate-artifacts.yml",
run_attempt: Number(runAttempt),
status: "completed",
workflow_id: Number(workflowId),
...overrides,
};
}
function workflowFixture(overrides: Record<string, unknown> = {}) {
return {
id: Number(workflowId),
path: ".github/workflows/release-candidate-artifacts.yml",
state: "active",
...overrides,
};
}
function artifactFixture(
artifact: (typeof lockFixture.receipt.artifacts)[keyof typeof lockFixture.receipt.artifacts],
) {
return {
digest: artifact.artifact_digest,
expired: false,
id: Number(artifact.artifact_id),
name: artifact.artifact_name,
workflow_run: { id: Number(runId) },
};
}
function artifactsFixture() {
return {
artifacts: Object.values(lockFixture.receipt.artifacts).map(artifactFixture),
total_count: 4,
};
}
describe("candidate receipt contract", () => {
it("pins canonical source and lock bytes as the cross-workflow golden fixture", () => {
expect(CANDIDATE_RECEIPT_CANONICALIZATION).toBe(
"ascii-sorted-compact-json-trailing-newline-v1",
);
expect(sourceText).toBe(canonicalCandidateReceiptJson(sourceFixture));
expect(lockText).toBe(canonicalCandidateReceiptLockJson(lockFixture));
expect(createCandidateReceiptLock(sourceFixture)).toEqual(lockFixture);
expect(lockText.endsWith("\n")).toBe(true);
});
it("rejects duplicate, reordered, pretty, CRLF, and non-ASCII lock bytes", () => {
const duplicate = lockText.replace('{"digest":', `{"digest":"${lockFixture.digest}","digest":`);
expect(() => parseCandidateReceiptLockJson(duplicate)).toThrow("duplicate key");
expect(() =>
parseCandidateReceiptLockJson(
`${JSON.stringify({
schema: lockFixture.schema,
receipt: lockFixture.receipt,
digest: lockFixture.digest,
})}\n`,
),
).toThrow("canonical bytes");
expect(() =>
parseCandidateReceiptLockJson(`${JSON.stringify(lockFixture, null, 2)}\n`),
).toThrow("compact printable ASCII");
expect(() => parseCandidateReceiptLockJson(lockText.replace(/\n$/u, "\r\n"))).toThrow(
"exactly one trailing LF",
);
expect(() =>
parseCandidateReceiptLockJson(lockText.replace("openclaw/openclaw", "opénclaw")),
).toThrow("printable ASCII");
});
it("rejects digest drift, duplicate artifact IDs, and names from another attempt", () => {
expect(() =>
parseCandidateReceiptLockJson(
lockText.replace(lockFixture.digest, `sha256:${"9".repeat(64)}`),
),
).toThrow("does not match");
expect(() =>
validateCandidateReceipt({
...sourceFixture,
artifacts: {
...(sourceFixture.artifacts as Record<string, unknown>),
root_image: {
...(sourceFixture.artifacts as Record<string, Record<string, unknown>>).root_image,
artifact_id: "103",
},
},
}),
).toThrow("artifact IDs must be unique");
expect(() =>
validateCandidateReceipt({
...sourceFixture,
artifacts: {
...(sourceFixture.artifacts as Record<string, unknown>),
package: {
...(sourceFixture.artifacts as Record<string, Record<string, unknown>>).package,
artifact_name: "docker-e2e-package-12345-1",
},
},
}),
).toThrow("name must bind the producer run attempt");
expect(() =>
validateCandidateReceipt({
...sourceFixture,
artifacts: {
...(sourceFixture.artifacts as Record<string, unknown>),
package: {
...(sourceFixture.artifacts as Record<string, Record<string, unknown>>).package,
artifact_name: "release-candidate-root-image-12345-2",
},
},
}),
).toThrow("name does not match its artifact kind");
});
it("references ReleasePlan only by digest", () => {
const receipt = validateCandidateReceipt(sourceFixture);
expect(receipt).not.toHaveProperty("candidate_sha");
expect(receipt).not.toHaveProperty("version");
expect(receipt).not.toHaveProperty("validation");
expect(() =>
validateCandidateReceipt({ ...sourceFixture, candidate_sha: "a".repeat(40) }),
).toThrow("candidate receipt keys must be exactly");
});
});
describe("candidate receipt locator", () => {
it("validates the exact workflow, run attempt, ReleasePlan, and artifact service digests", () => {
expect(
validateCandidateReceiptProvenance({
artifacts: artifactsFixture(),
expectedDispatchId: dispatchId,
expectedReleasePlanDigest: lockFixture.receipt.release_plan_digest,
expectedRunAttempt: runAttempt,
expectedRunId: runId,
expectedWorkflowId: workflowId,
expectedWorkflowSha: workflowSha,
lock: lockFixture,
run: runFixture(),
workflow: workflowFixture(),
}),
).toEqual(lockFixture);
});
it.each([
["workflow id", { run: runFixture({ workflow_id: 999 }) }],
["workflow path", { run: runFixture({ path: ".github/workflows/ci.yml" }) }],
["workflow SHA", { run: runFixture({ head_sha: "c".repeat(40) }) }],
["run attempt", { run: runFixture({ run_attempt: 1 }) }],
["dispatch title", { run: runFixture({ display_title: "other" }) }],
["event", { run: runFixture({ event: "push" }) }],
["conclusion", { run: runFixture({ conclusion: "failure" }) }],
])("rejects mismatched %s provenance", (_label, overrides) => {
expect(() =>
validateCandidateReceiptProvenance({
artifacts: artifactsFixture(),
expectedDispatchId: dispatchId,
expectedReleasePlanDigest: lockFixture.receipt.release_plan_digest,
expectedRunAttempt: runAttempt,
expectedRunId: runId,
expectedWorkflowId: workflowId,
expectedWorkflowSha: workflowSha,
lock: lockFixture,
run: overrides.run,
workflow: workflowFixture(),
}),
).toThrow("exact successful producer attempt");
});
it("rejects missing, expired, moved, or digest-mismatched artifacts", () => {
const artifacts = artifactsFixture();
const firstArtifact = artifacts.artifacts[0]!;
artifacts.artifacts[0] = { ...firstArtifact, digest: `sha256:${"8".repeat(64)}` };
expect(() =>
validateCandidateReceiptProvenance({
artifacts,
expectedDispatchId: dispatchId,
expectedReleasePlanDigest: lockFixture.receipt.release_plan_digest,
expectedRunAttempt: runAttempt,
expectedRunId: runId,
expectedWorkflowId: workflowId,
expectedWorkflowSha: workflowSha,
lock: lockFixture,
run: runFixture(),
workflow: workflowFixture(),
}),
).toThrow("metadata does not match");
});
it("bounds each gh lookup", () => {
const execFileSyncImpl = vi.fn(() => "result");
expect(
runCandidateReceiptGh(["api", "repos/openclaw/openclaw/actions/runs/12345"], {
execFileSyncImpl,
}),
).toBe("result");
expect(execFileSyncImpl).toHaveBeenCalledWith(
"gh",
["api", "repos/openclaw/openclaw/actions/runs/12345"],
{
encoding: "utf8",
killSignal: "SIGKILL",
maxBuffer: 32 * 1024 * 1024,
timeout: 60_000,
},
);
});
it("discovers one nonce-bound run, polls its exact attempt, and reads its receipt artifact", async () => {
const receiptArtifactName = `release-candidate-receipt-${runId}-${runAttempt}`;
const artifactResponse = artifactsFixture();
artifactResponse.artifacts.push({
digest: `sha256:${"5".repeat(64)}`,
expired: false,
id: 105,
name: receiptArtifactName,
workflow_run: { id: Number(runId) },
});
artifactResponse.total_count = 5;
const responses = new Map<string, unknown>([
[
`api repos/openclaw/openclaw/actions/workflows/${workflowId} --method GET`,
workflowFixture(),
],
[
`api repos/openclaw/openclaw/actions/workflows/${workflowId}/runs?event=workflow_dispatch&per_page=100 --method GET`,
{ workflow_runs: [runFixture()] },
],
[
`api repos/openclaw/openclaw/actions/runs/${runId}/attempts/${runAttempt} --method GET`,
runFixture(),
],
[`api repos/openclaw/openclaw/actions/runs/${runId} --method GET`, runFixture()],
[
`api repos/openclaw/openclaw/actions/runs/${runId}/artifacts?per_page=100 --method GET`,
artifactResponse,
],
]);
const runGh = vi.fn((args: string[]) => {
if (args[0] === "run" && args[1] === "download") {
const dir = args[args.indexOf("--dir") + 1];
if (!dir) {
throw new Error("missing download dir");
}
writeFileSync(resolve(dir, "candidate-receipt-lock.json"), lockText);
return "";
}
const response = responses.get(args.join(" "));
if (!response) {
throw new Error(`unexpected gh invocation: ${args.join(" ")}`);
}
return JSON.stringify(response);
});
await expect(
locateCandidateReceipt({
dispatchId,
releasePlanDigest: lockFixture.receipt.release_plan_digest,
repo: "openclaw/openclaw",
runGh,
sleep: async () => {},
timeoutMs: 1000,
workflowId,
workflowSha,
}),
).resolves.toEqual(lockFixture);
expect(runGh).toHaveBeenCalledWith([
"run",
"download",
runId,
"--repo",
"openclaw/openclaw",
"--name",
receiptArtifactName,
"--dir",
expect.any(String),
]);
});
it("rejects a superseded exact attempt", async () => {
const runGh = vi.fn((args: string[]) => {
const key = args.join(" ");
if (key.includes(`actions/workflows/${workflowId} --method GET`)) {
return JSON.stringify(workflowFixture());
}
if (key.includes(`actions/runs/${runId}/attempts/${runAttempt}`)) {
return JSON.stringify(runFixture());
}
if (key.includes(`actions/runs/${runId} --method GET`)) {
return JSON.stringify(runFixture({ run_attempt: 3 }));
}
throw new Error(`unexpected gh invocation: ${key}`);
});
await expect(
locateCandidateReceipt({
dispatchId,
releasePlanDigest: lockFixture.receipt.release_plan_digest,
repo: "openclaw/openclaw",
runAttempt,
runGh,
runId,
sleep: async () => {},
timeoutMs: 1000,
workflowId,
workflowSha,
}),
).rejects.toThrow("superseded by a rerun");
});
});
type WorkflowStep = {
env?: Record<string, string>;
id?: string;
name?: string;
run?: string;
uses?: string;
with?: Record<string, unknown>;
};
type WorkflowJob = {
"runs-on"?: string;
needs?: string | string[];
outputs?: Record<string, string>;
permissions?: Record<string, string>;
steps?: WorkflowStep[];
uses?: string;
with?: Record<string, unknown>;
};
type Workflow = {
jobs: Record<string, WorkflowJob>;
on?: {
workflow_dispatch?: { inputs?: Record<string, Record<string, unknown>> };
workflow_call?: unknown;
};
permissions?: Record<string, string>;
"run-name"?: string;
};
function workflowJob(workflow: Workflow, name: string): WorkflowJob {
const found = workflow.jobs[name];
expect(found, name).toBeDefined();
return found!;
}
function workflowStep(job: WorkflowJob, name: string): WorkflowStep {
const found = job.steps?.find((step) => step.name === name);
expect(found, name).toBeDefined();
return found!;
}
describe("release candidate artifact producer workflow", () => {
const path = ".github/workflows/release-candidate-artifacts.yml";
const text = readFileSync(path, "utf8");
const workflow = parse(text) as Workflow;
it("is one read-only standalone producer keyed by a caller nonce", () => {
expect(workflow["run-name"]).toBe("Release Candidate Artifacts ${{ inputs.dispatch_id }}");
expect(workflow.on?.workflow_call).toBeUndefined();
expect(workflow.on?.workflow_dispatch?.inputs).toMatchObject({
dispatch_id: { required: true, type: "string" },
release_plan_lock_base64: { required: true, type: "string" },
});
expect(workflow.permissions).toEqual({
actions: "read",
contents: "read",
packages: "read",
"pull-requests": "read",
});
expect(text).not.toContain("contents: write");
expect(text).not.toContain("packages: write");
expect(text).not.toContain("--push");
});
it("validates canonical ReleasePlan bytes and derives candidate inputs without copying plan fields", () => {
const validate = workflowJob(workflow, "validate_release_plan");
const step = workflowStep(validate, "Validate ReleasePlanLock");
expect(step.run).toContain("parseReleasePlanLockJson");
expect(step.run).toContain("dispatch_id must be one safe unique caller nonce");
expect(step.run).toContain("lock.plan.tooling.sha !== process.env.WORKFLOW_SHA");
expect(step.run).toContain("lock.plan.tooling.ref !== process.env.WORKFLOW_FULL_REF");
expect(step.run).toContain("candidate_sha=${lock.plan.candidate_sha}");
expect(step.run).toContain("release_plan_digest=${lock.digest}");
expect(step.run).toContain("release_profile=${lock.plan.validation.profile}");
});
it("runs the existing candidate producer and root-image producer in parallel", () => {
const candidate = workflowJob(workflow, "candidate_artifacts");
const root = workflowJob(workflow, "root_image");
expect(candidate.needs).toBe("validate_release_plan");
expect(root.needs).toBe("validate_release_plan");
expect(candidate.uses).toBe("./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml");
expect(candidate.with).toMatchObject({
prepare_only: true,
include_repo_e2e: false,
include_release_path_suites: false,
include_openwebui: false,
include_live_suites: false,
enable_prepublish_plugin_registry: true,
shared_image_artifact_namespace: "release-candidate",
shared_image_policy: "no-push-artifact",
});
expect(root["runs-on"]).toBe("blacksmith-32vcpu-ubuntu-2404");
expect(workflowStep(root, "Pack root Dockerfile image artifact").run).toContain(
"scripts/docker/shared-image-artifact.sh",
);
expect(workflowStep(root, "Upload root Dockerfile image artifact").with).toMatchObject({
"compression-level": 0,
"if-no-files-found": "error",
"retention-days": 7,
});
});
it("emits one receipt only after all four immutable artifacts exist", () => {
const receipt = workflowJob(workflow, "candidate_receipt");
expect(receipt.needs).toEqual(["validate_release_plan", "candidate_artifacts", "root_image"]);
const provenance = workflowStep(receipt, "Verify exact producer workflow attempt");
expect(provenance.run).toContain(
"actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}",
);
expect(provenance.run).toContain(".display_title == $title");
expect(provenance.run).toContain("actions/workflows/${workflow_id}");
const create = workflowStep(receipt, "Create canonical CandidateReceiptLock");
expect(create.run).toContain("createCandidateReceiptLock");
expect(create.run).toContain('docker_image: artifact("DOCKER_IMAGE"');
expect(create.run).toContain('package: artifact("PACKAGE"');
expect(create.run).toContain("plugin_registry: artifact(");
expect(create.run).toContain('root_image: artifact("ROOT_IMAGE"');
expect(workflowStep(receipt, "Upload CandidateReceiptLock").with).toMatchObject({
name: "release-candidate-receipt-${{ github.run_id }}-${{ github.run_attempt }}",
path: ".artifacts/candidate-receipt/candidate-receipt-lock.json",
"if-no-files-found": "error",
"retention-days": 7,
});
});
});