Files
openclaw/test/scripts/release-no-push-workflow.test.ts
Peter Steinberger fe261b0f59 chore(tooling): typecheck root test/** with a dedicated tsgo lane (#104475)
* chore(types): add declaration files for scripts/lib and scripts/e2e modules

* chore(types): add declaration files for top-level script modules (a-m)

* chore(types): add declaration files for top-level script modules (n-z)

* test: use a non-secret-shaped gateway token fixture

* test: type ci workflow guard helpers for the root test lane

* chore(tooling): typecheck root test/** with a dedicated tsgo lane

- test/tsconfig/tsconfig.test.root.json: root-test program (strict unused checks,
  fixtures excluded; two Docker E2E clients that import built dist/** stay out,
  same rationale as the scripts/e2e exclusion in tsconfig.scripts.json)
- tsgo:test:root wired into tsgo:test, check:test-types, scripts/check.mjs, and
  the ci.yml test-types shard, mirroring the tsgo:scripts lane (#104348)
- changed-lane routing: test/**/*.ts (excluding fixtures) and the lane tsconfig
  now trigger 'typecheck test root' in check:changed; previously test/ paths ran
  lint only, so harness type errors surfaced first in CI (#104287 envDir case)
- burn down all 1071 latent type errors in the program: precise param/local
  types across test/scripts, test/vitest, test/e2e, and transitive scripts/e2e
  program members; 205 sibling .d.mts declaration files for imported .mjs
  modules (committed separately); zero any, zero ts-expect-error
- resolve the pre-existing testing star-export ambiguity in
  scripts/e2e/parallels/common.ts with an explicit re-export

Closes #104388

* chore(types): correct declaration fidelity per structured review

- re-derive 51 .d.mts files from implementation data flow instead of
  initializers: fix a wrong never return (runTestProjectsDelegation returns
  the child), add encoding-sensitive exec/spawn overloads (plain-gh), restore
  the full release profile union, make parsed paths string | null, add missing
  parseArgs fields via help/non-help unions, add a missing sibling declaration
  (budget-number-args), drop 15 unused lint directives
- precise install-record/tuple typing removes the type-aware oxlint
  regressions the first declarations caused in scripts/e2e implementations
- route .mts declaration edits under test/ to the testRoot lane and reference
  the test-root project from tsconfig.projects.json so tsgo:all covers it
  (closes both review findings against the lane wiring)

* chore(scripts): keep telegram runner dist typing structural for the boundary guard

* chore(types): declare runtime pack and gateway readiness exports added on main

* test: pin the importTargetPlan form of the plugin-contract plan import

The guard expectation still referenced the raw await import( form that
7ae5996bb3 (#103975) replaced with the importTargetPlan fallback helper;
the assertion fails on current main.
2026-07-11 06:15:41 -07:00

999 lines
44 KiB
TypeScript

import { spawnSync } from "node:child_process";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
const FULL_RELEASE = ".github/workflows/full-release-validation.yml";
const RELEASE_CHECKS = ".github/workflows/openclaw-release-checks.yml";
const PACKAGE_ACCEPTANCE = ".github/workflows/package-acceptance.yml";
const PLUGIN_PRERELEASE = ".github/workflows/plugin-prerelease.yml";
const LIVE_E2E = ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml";
const INSTALL_SMOKE = ".github/workflows/install-smoke.yml";
const SHARED_IMAGE_PUBLISHER = ".github/workflows/openclaw-shared-image-publish-reusable.yml";
const SCHEDULED_LIVE = ".github/workflows/openclaw-scheduled-live-checks.yml";
const DOCKER_RELEASE = ".github/workflows/docker-release.yml";
const UPDATE_MIGRATION = ".github/workflows/update-migration.yml";
const PERFORMANCE = ".github/workflows/openclaw-performance.yml";
const LIVE_BUILD = "scripts/test-live-build-docker.sh";
const DOCKER_E2E_IMAGE_HELPER = "scripts/lib/docker-e2e-image.sh";
type WorkflowInput = {
default?: boolean | number | string;
options?: string[];
type?: string;
};
type WorkflowStep = {
env?: Record<string, string>;
id?: string;
if?: string;
name?: string;
run?: string;
uses?: string;
with?: Record<string, boolean | number | string>;
};
type WorkflowJob = {
env?: Record<string, string>;
if?: string;
needs?: string | string[];
outputs?: Record<string, string>;
permissions?: PermissionMap;
steps?: WorkflowStep[];
uses?: string;
with?: Record<string, boolean | number | string>;
};
type Workflow = {
jobs?: Record<string, WorkflowJob>;
on?: {
workflow_call?: {
inputs?: Record<string, WorkflowInput>;
outputs?: Record<string, { description?: string; value?: string }>;
};
workflow_dispatch?: { inputs?: Record<string, WorkflowInput> };
};
permissions?: PermissionMap;
};
type PermissionLevel = "none" | "read" | "write";
type PermissionMap = "read-all" | "write-all" | Record<string, PermissionLevel>;
const PERMISSION_RANK: Record<PermissionLevel, number> = { none: 0, read: 1, write: 2 };
function permissionAt(
permissions: PermissionMap | undefined,
scope: string,
inherited: PermissionLevel,
): PermissionLevel {
if (permissions === undefined) {
return inherited;
}
if (permissions === "read-all") {
return "read";
}
if (permissions === "write-all") {
return "write";
}
return permissions[scope] ?? "none";
}
function permissionScopes(...permissions: Array<PermissionMap | undefined>): string[] {
const scopes = new Set(["actions", "contents", "packages", "pull-requests"]);
for (const value of permissions) {
if (value && typeof value === "object") {
for (const scope of Object.keys(value)) {
scopes.add(scope);
}
}
}
return [...scopes].toSorted();
}
function reusablePermissionViolations(
callerPath: string,
callerJobName: string,
seen = new Set<string>(),
): string[] {
const caller = readWorkflow(callerPath);
const callerJob = job(caller, callerJobName);
if (!callerJob.uses?.startsWith("./.github/workflows/")) {
throw new Error(`${callerPath}:${callerJobName} is not a local reusable-workflow call`);
}
const ceiling = callerJob.permissions ?? caller.permissions;
return workflowPermissionViolations(
callerJob.uses.slice(2),
Object.fromEntries(
permissionScopes(ceiling).map((scope) => [scope, permissionAt(ceiling, scope, "none")]),
),
`${callerPath}:${callerJobName}`,
seen,
);
}
function workflowPermissionViolations(
workflowPath: string,
ceiling: Record<string, PermissionLevel>,
chain: string,
seen: Set<string>,
): string[] {
const visitKey = `${chain}->${workflowPath}`;
if (seen.has(visitKey)) {
return [];
}
seen.add(visitKey);
const workflow = readWorkflow(workflowPath);
const violations: string[] = [];
for (const [jobName, workflowJob] of Object.entries(workflow.jobs ?? {})) {
const requested = workflowJob.permissions ?? workflow.permissions;
const scopes = permissionScopes(requested, ceiling);
const effective: Record<string, PermissionLevel> = {};
for (const scope of scopes) {
const cap = ceiling[scope] ?? "none";
const level = permissionAt(requested, scope, cap);
effective[scope] = level;
if (PERMISSION_RANK[level] > PERMISSION_RANK[cap]) {
violations.push(
`${chain} -> ${workflowPath}:${jobName} requests ${scope}:${level} above caller ${scope}:${cap}`,
);
}
}
if (workflowJob.uses?.startsWith("./.github/workflows/")) {
violations.push(
...workflowPermissionViolations(
workflowJob.uses.slice(2),
effective,
`${chain} -> ${workflowPath}:${jobName}`,
seen,
),
);
}
}
return violations;
}
function readWorkflow(path: string): Workflow {
return parse(readFileSync(path, "utf8")) as Workflow;
}
function job(workflow: Workflow, name: string): WorkflowJob {
const value = workflow.jobs?.[name];
if (!value) {
throw new Error(`missing workflow job ${name}`);
}
return value;
}
function step(workflowJob: WorkflowJob, name: string): WorkflowStep {
const value = workflowJob.steps?.find((candidate) => candidate.name === name);
if (!value) {
throw new Error(`missing workflow step ${name}`);
}
return value;
}
function expectReadOnlyPackagePermission(workflowJob: WorkflowJob): void {
expect(permissionAt(workflowJob.permissions, "packages", "none")).toBe("read");
}
describe("release validation no-push transport", () => {
it("keeps every local reusable-workflow permission request within its caller ceiling", () => {
const readOnlyCalls = [
[PLUGIN_PRERELEASE, "plugin-prerelease-docker-suite"],
[RELEASE_CHECKS, "live_repo_e2e_release_checks"],
[RELEASE_CHECKS, "docker_e2e_release_checks"],
[RELEASE_CHECKS, "package_acceptance_release_checks"],
[RELEASE_CHECKS, "install_smoke_release_checks"],
[PACKAGE_ACCEPTANCE, "docker_acceptance"],
[PACKAGE_ACCEPTANCE, "docker_acceptance_registry"],
[INSTALL_SMOKE, "install_smoke"],
[SCHEDULED_LIVE, "live_and_openwebui_checks"],
[UPDATE_MIGRATION, "update_migration"],
] as const;
for (const [workflowPath, jobName] of readOnlyCalls) {
const callerWorkflow = readWorkflow(workflowPath);
const caller = job(callerWorkflow, jobName);
const callerPermissions = caller.permissions ?? callerWorkflow.permissions;
expect(
permissionAt(callerPermissions, "packages", "none"),
`${workflowPath}:${jobName}`,
).toBe("read");
expect(
reusablePermissionViolations(workflowPath, jobName),
`${workflowPath}:${jobName}`,
).toEqual([]);
}
});
it("models conditional reusable jobs as permission requests before scheduling", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-permission-graph-"));
const fixture = join(root, "callee.yml");
try {
writeFileSync(
fixture,
`permissions:\n packages: read\njobs:\n safe:\n runs-on: ubuntu-latest\n skippedWriter:\n if: false\n runs-on: ubuntu-latest\n permissions:\n packages: write\n`,
);
const violations = workflowPermissionViolations(
fixture,
{ actions: "none", contents: "none", packages: "read", "pull-requests": "none" },
"fixture:caller",
new Set(),
);
expect(violations).toEqual([
`fixture:caller -> ${fixture}:skippedWriter requests packages:write above caller packages:read`,
]);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("does not persist Git credentials in validation checkouts", () => {
for (const workflowPath of [PLUGIN_PRERELEASE, RELEASE_CHECKS]) {
const workflow = readWorkflow(workflowPath);
const checkoutSteps = Object.values(workflow.jobs ?? {}).flatMap(
(workflowJob) =>
workflowJob.steps?.filter((candidate) =>
candidate.uses?.startsWith("actions/checkout@"),
) ?? [],
);
expect(checkoutSteps, workflowPath).not.toHaveLength(0);
for (const checkout of checkoutSteps) {
expect(checkout.with?.["persist-credentials"], `${workflowPath}:${checkout.name}`).toBe(
false,
);
}
}
});
it("runs evidence reuse from an immutable trusted-main workflow checkout", () => {
const full = readWorkflow(FULL_RELEASE);
for (const jobName of ["resolve_target", "evidence_reuse"]) {
const checkout = step(job(full, jobName), "Checkout trusted workflow helper");
expect(checkout.with?.ref, jobName).toBe("${{ github.sha }}");
expect(checkout.with?.ref, jobName).not.toBe("${{ github.ref_name }}");
expect(checkout.with?.["persist-credentials"], jobName).toBe(false);
}
const evidenceReuse = job(full, "evidence_reuse");
expect(step(evidenceReuse, "Checkout target SHA").with?.["persist-credentials"]).toBe(false);
const dockerAssets = job(full, "docker_runtime_assets_preflight");
expect(step(dockerAssets, "Checkout target SHA").with?.["persist-credentials"]).toBe(false);
expect(evidenceReuse.if).toContain("github.ref == 'refs/heads/main'");
expect(evidenceReuse.if).toContain("startsWith(github.ref, 'refs/heads/release-ci/')");
expect(
evidenceReuse.steps?.find(
(candidate) => candidate.name === "Require trusted main workflow ref",
),
).toBeUndefined();
const releaseChecks = readWorkflow(RELEASE_CHECKS);
const releaseHelper = step(
job(releaseChecks, "resolve_target"),
"Checkout trusted workflow helper",
);
expect(releaseHelper.with?.ref).toBe("${{ github.sha }}");
expect(releaseHelper.with?.ref).not.toBe("${{ github.ref_name }}");
expect(releaseHelper.with?.["persist-credentials"]).toBe(false);
});
it("rejects every child whose workflow SHA differs from the parent workflow SHA", () => {
const full = readWorkflow(FULL_RELEASE);
for (const [jobName, stepName] of [
["normal_ci", "Dispatch and monitor CI"],
["plugin_prerelease", "Dispatch and monitor plugin prerelease"],
["release_checks", "Dispatch and monitor release checks"],
["npm_telegram", "Dispatch and monitor npm Telegram E2E"],
["performance", "Dispatch and monitor OpenClaw Performance"],
] as const) {
const dispatch = step(job(full, jobName), stepName);
expect(dispatch.env?.PARENT_WORKFLOW_SHA, jobName).toBe("${{ github.sha }}");
expect(dispatch.run, jobName).toContain('"$child_head_sha" != "$PARENT_WORKFLOW_SHA"');
expect(dispatch.run, jobName).toContain("expected parent workflow SHA");
}
const verify = step(job(full, "summary"), "Verify child workflow results");
expect(verify.env?.PARENT_WORKFLOW_SHA).toBe("${{ github.sha }}");
expect(verify.run).toContain('"$head_sha" != "$PARENT_WORKFLOW_SHA"');
expect(verify.run).not.toContain('"$head_sha" != "$TARGET_SHA"');
});
it("publishes an attempt-qualified canonical manifest plus a temporary legacy alias", () => {
const summary = job(readWorkflow(FULL_RELEASE), "summary");
expect(step(summary, "Upload release validation manifest").with).toMatchObject({
name: "full-release-validation-${{ github.run_id }}-${{ github.run_attempt }}",
});
expect(step(summary, "Upload legacy release validation manifest alias").with).toMatchObject({
name: "full-release-validation-${{ github.run_id }}",
overwrite: true,
});
});
it("pins every Full Release Docker caller to artifact-only transport", () => {
const fullText = readFileSync(FULL_RELEASE, "utf8");
const release = readWorkflow(RELEASE_CHECKS);
const packageAcceptance = readWorkflow(PACKAGE_ACCEPTANCE);
const pluginPrerelease = readWorkflow(PLUGIN_PRERELEASE);
expect(fullText).toContain("dispatch_and_wait plugin-prerelease.yml");
expect(fullText).toContain("dispatch_and_wait openclaw-release-checks.yml");
expect(fullText).toContain("gh workflow run openclaw-performance.yml");
const preparePackage = job(release, "prepare_release_package");
const live = job(release, "live_repo_e2e_release_checks");
const docker = job(release, "docker_e2e_release_checks");
const acceptance = job(release, "package_acceptance_release_checks");
expectReadOnlyPackagePermission(preparePackage);
expectReadOnlyPackagePermission(live);
expectReadOnlyPackagePermission(docker);
expectReadOnlyPackagePermission(acceptance);
expect(step(preparePackage, "Resolve release package artifact").run).toContain(
'if [[ "$source_sha" != "$PACKAGE_REF" ]]',
);
expect(live.with).toMatchObject({
shared_image_artifact_namespace: "release-live",
shared_image_policy: "no-push-artifact",
});
expect(live.with).not.toHaveProperty("allow_unreleased_changelog");
expect(docker.with).toMatchObject({
package_artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}",
package_artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}",
package_artifact_name: "${{ needs.prepare_release_package.outputs.artifact_name }}",
package_artifact_run_attempt:
"${{ needs.prepare_release_package.outputs.artifact_run_attempt }}",
package_artifact_run_id: "${{ needs.prepare_release_package.outputs.artifact_run_id }}",
package_file_name: "${{ needs.prepare_release_package.outputs.package_file_name }}",
package_sha256: "${{ needs.prepare_release_package.outputs.package_sha256 }}",
package_source_sha: "${{ needs.prepare_release_package.outputs.source_sha }}",
package_version: "${{ needs.prepare_release_package.outputs.package_version }}",
shared_image_artifact_namespace: "release-docker",
shared_image_policy: "no-push-artifact",
});
expect(docker.with).not.toHaveProperty("allow_unreleased_changelog");
expect(acceptance.with).toMatchObject({
artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}",
artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}",
artifact_name: "${{ needs.prepare_release_package.outputs.artifact_name }}",
artifact_run_attempt: "${{ needs.prepare_release_package.outputs.artifact_run_attempt }}",
artifact_run_id: "${{ needs.prepare_release_package.outputs.artifact_run_id }}",
package_file_name: "${{ needs.prepare_release_package.outputs.package_file_name }}",
package_source_sha: "${{ needs.prepare_release_package.outputs.source_sha }}",
package_version: "${{ needs.prepare_release_package.outputs.package_version }}",
shared_image_artifact_namespace: "release-package",
shared_image_policy: "no-push-artifact",
});
const standardAcceptance = job(packageAcceptance, "docker_acceptance");
const registryAcceptance = job(packageAcceptance, "docker_acceptance_registry");
expect(permissionAt(packageAcceptance.permissions, "packages", "none")).toBe("read");
expect(packageAcceptance.on?.workflow_dispatch?.inputs?.shared_image_policy).toMatchObject({
default: "no-push-artifact",
options: ["existing-only", "no-push-artifact"],
type: "choice",
});
expect(packageAcceptance.on?.workflow_call?.inputs?.shared_image_policy).toMatchObject({
default: "no-push-artifact",
type: "string",
});
expect(standardAcceptance.with?.shared_image_policy).toBe("${{ inputs.shared_image_policy }}");
expect(standardAcceptance.with?.shared_image_artifact_namespace).toBe(
"${{ inputs.shared_image_artifact_namespace }}",
);
expect(standardAcceptance.with).toMatchObject({
package_artifact_digest: "${{ needs.resolve_package.outputs.package_artifact_digest }}",
package_artifact_id: "${{ needs.resolve_package.outputs.package_artifact_id }}",
package_artifact_run_attempt:
"${{ needs.resolve_package.outputs.package_artifact_run_attempt }}",
package_artifact_run_id: "${{ needs.resolve_package.outputs.package_artifact_run_id }}",
package_file_name: "${{ needs.resolve_package.outputs.package_file_name }}",
package_sha256: "${{ needs.resolve_package.outputs.package_sha256 }}",
package_source_sha: "${{ needs.resolve_package.outputs.package_source_sha }}",
package_version: "${{ needs.resolve_package.outputs.package_version }}",
});
expect(standardAcceptance.with).not.toHaveProperty("allow_unreleased_changelog");
expect(registryAcceptance.with).not.toHaveProperty("allow_unreleased_changelog");
expect(standardAcceptance.if).toContain("shared_image_policy == 'no-push-artifact'");
expectReadOnlyPackagePermission(standardAcceptance);
expect(registryAcceptance.if).toContain("shared_image_policy == 'existing-only'");
expectReadOnlyPackagePermission(registryAcceptance);
const pluginDocker = job(pluginPrerelease, "plugin-prerelease-docker-suite");
expectReadOnlyPackagePermission(pluginDocker);
expect(pluginDocker.with).toMatchObject({
shared_image_artifact_namespace: "plugin-prerelease",
shared_image_policy: "no-push-artifact",
});
expect(
new Set([
live.with?.shared_image_artifact_namespace,
docker.with?.shared_image_artifact_namespace,
acceptance.with?.shared_image_artifact_namespace,
pluginDocker.with?.shared_image_artifact_namespace,
]).size,
).toBe(4);
});
it("builds shared images locally, verifies artifacts, and cannot fall back to a registry", () => {
const workflow = readWorkflow(LIVE_E2E);
const dispatchPolicy = workflow.on?.workflow_dispatch?.inputs?.shared_image_policy;
const callPolicy = workflow.on?.workflow_call?.inputs?.shared_image_policy;
expect(dispatchPolicy).toMatchObject({
default: "no-push-artifact",
options: ["existing-only", "no-push-artifact"],
});
expect(callPolicy).toMatchObject({ default: "no-push-artifact", type: "string" });
const validation = job(workflow, "validate_selected_ref");
expect(validation.outputs?.workflow_repository).toBe(
"${{ steps.workflow.outputs.workflow_repository }}",
);
expect(validation.outputs?.workflow_sha).toBe("${{ steps.workflow.outputs.workflow_sha }}");
const workflowIdentity = step(validation, "Resolve job workflow identity");
expect(workflowIdentity.env?.JOB_CONTEXT).toBe("${{ toJSON(job) }}");
expect(workflowIdentity.run).toContain(
"job.workflow_repository must be an owner/repository slug",
);
expect(workflowIdentity.run).toContain("job.workflow_sha must be a full lowercase commit SHA");
const trustedCheckouts = Object.entries(workflow.jobs ?? {}).flatMap(([jobName, workflowJob]) =>
(workflowJob.steps ?? [])
.filter((candidate) => candidate.name?.startsWith("Checkout trusted "))
.map((candidate) => ({ candidate, jobName })),
);
expect(trustedCheckouts).toHaveLength(12);
for (const { candidate, jobName } of trustedCheckouts) {
expect(candidate.with, jobName).toMatchObject({
repository: "${{ needs.validate_selected_ref.outputs.workflow_repository }}",
ref: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
"persist-credentials": false,
});
}
const dockerProducer = job(workflow, "prepare_docker_e2e_image");
const liveProducer = job(workflow, "prepare_live_test_image");
expect(permissionAt(workflow.permissions, "actions", "none")).toBe("read");
expect(permissionAt(workflow.permissions, "packages", "none")).toBe("read");
expectReadOnlyPackagePermission(dockerProducer);
expectReadOnlyPackagePermission(liveProducer);
expect(workflow.jobs?.push_docker_e2e_images).toBeUndefined();
expect(workflow.jobs?.push_live_test_image).toBeUndefined();
expect(
permissionAt(job(workflow, "docker_e2e_image_ready").permissions, "packages", "none"),
).toBe("none");
expect(
permissionAt(job(workflow, "live_test_image_ready").permissions, "packages", "none"),
).toBe("none");
const packageWriters = Object.entries(workflow.jobs ?? {}).filter(
([, workflowJob]) => permissionAt(workflowJob.permissions, "packages", "none") === "write",
);
expect(packageWriters).toEqual([]);
expect(workflow.on?.workflow_call?.outputs?.publication_manifest).toBeUndefined();
expect(workflow.jobs?.collect_shared_image_publication).toBeUndefined();
const validateSelectedRef = step(
job(workflow, "validate_selected_ref"),
"Validate selected ref",
);
const dispatchInputs = workflow.on?.workflow_dispatch?.inputs ?? {};
for (const inputName of [
"package_artifact_digest",
"package_artifact_id",
"package_artifact_name",
"package_artifact_run_attempt",
"package_artifact_run_id",
"package_file_name",
"package_sha256",
"package_source_sha",
"package_version",
]) {
expect(dispatchInputs[inputName], inputName).toBeUndefined();
expect(workflow.on?.workflow_call?.inputs?.[inputName], inputName).toBeDefined();
}
expect(validateSelectedRef.env?.PACKAGE_ARTIFACT_DIGEST).toBe(
"${{ inputs.package_artifact_digest }}",
);
expect(validateSelectedRef.env?.PACKAGE_ARTIFACT_RUN_ATTEMPT).toBe(
"${{ inputs.package_artifact_run_attempt }}",
);
expect(validateSelectedRef.env?.PACKAGE_ARTIFACT_RUN_ID).toBe(
"${{ inputs.package_artifact_run_id }}",
);
expect(validateSelectedRef.env?.PACKAGE_ARTIFACT_ID).toBe("${{ inputs.package_artifact_id }}");
expect(validateSelectedRef.env?.PACKAGE_FILE_NAME).toBe("${{ inputs.package_file_name }}");
expect(validateSelectedRef.env?.PACKAGE_SOURCE_SHA).toBe("${{ inputs.package_source_sha }}");
expect(validateSelectedRef.run).toContain(
"Package artifact selection requires the complete immutable artifact and package identity tuple.",
);
expect(validateSelectedRef.run).toContain('"$PACKAGE_SOURCE_SHA" == "$selected_sha"');
for (const name of [
"prepare_docker_e2e_image",
"prepare_live_test_image",
"validate_live_models_docker",
"validate_live_models_docker_targeted",
"validate_live_docker_provider_suites",
]) {
const checkoutSteps = job(workflow, name).steps?.filter((candidate) =>
candidate.uses?.startsWith("actions/checkout@"),
);
expect(checkoutSteps, name).not.toHaveLength(0);
for (const checkout of checkoutSteps ?? []) {
expect(checkout.with?.["persist-credentials"], `${name}:${checkout.name}`).toBe(false);
}
}
expect(dockerProducer.outputs?.image_artifact_name).toContain("image_artifact");
expect(liveProducer.outputs?.image_artifact_name).toContain("image_artifact");
for (const producer of [dockerProducer, liveProducer]) {
expect(producer.outputs?.image_archive_sha256).toContain("archive_sha256");
expect(producer.outputs?.image_artifact_id).toContain("artifact-id");
expect(producer.outputs?.image_artifact_digest).toContain("artifact-digest");
expect(producer.outputs?.image_artifact_run_id).toBe("${{ github.run_id }}");
expect(producer.outputs?.image_artifact_run_attempt).toBe("${{ github.run_attempt }}");
}
expect(dockerProducer.outputs?.package_artifact_id).toContain("artifact-id");
expect(dockerProducer.outputs?.package_artifact_digest).toContain("artifact-digest");
expect(dockerProducer.outputs?.package_artifact_run_attempt).toContain("run_attempt");
expect(dockerProducer.outputs?.package_artifact_run_id).toContain("run_id");
expect(dockerProducer.outputs?.package_file_name).toContain("file_name");
expect(dockerProducer.outputs?.package_source_sha).toContain("source_sha");
const packageIdentity = step(dockerProducer, "Validate OpenClaw package artifact identity");
expect(packageIdentity.env).toMatchObject({
ARTIFACT_DIGEST: "${{ inputs.package_artifact_digest }}",
ARTIFACT_ID: "${{ inputs.package_artifact_id }}",
ARTIFACT_NAME: "${{ inputs.package_artifact_name }}",
ARTIFACT_RUN_ATTEMPT: "${{ inputs.package_artifact_run_attempt }}",
ARTIFACT_RUN_ID: "${{ inputs.package_artifact_run_id }}",
});
expect(packageIdentity.run).toContain('--arg digest "sha256:${ARTIFACT_DIGEST}"');
expect(packageIdentity.run).toContain(
"actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}",
);
expect(packageIdentity.run).toContain("artifact_digest=$ARTIFACT_DIGEST");
for (const [name, condition] of [
[
"Download current-run OpenClaw Docker E2E package",
"inputs.package_artifact_run_id == github.run_id",
],
[
"Download previous-run OpenClaw Docker E2E package",
"inputs.package_artifact_run_id != github.run_id",
],
] as const) {
const packageDownload = step(dockerProducer, name);
expect(packageDownload.if).toContain(condition);
expect(packageDownload.with).toMatchObject({
"artifact-ids": "${{ inputs.package_artifact_id }}",
"github-token": "${{ github.token }}",
"run-id": "${{ inputs.package_artifact_run_id }}",
});
}
for (const name of [
"Build bare Docker E2E image artifact",
"Build functional Docker E2E image artifact",
]) {
const build = step(dockerProducer, name);
expect(build.if).toContain("shared_image_policy == 'no-push-artifact'");
expect(build.run).toContain("--load");
expect(build.run).toContain("--sbom=false");
expect(build.run).toContain("--provenance=false");
expect(build.run).not.toContain("--push");
expect(build.run).not.toContain("--sbom=true");
expect(build.run).not.toContain("--provenance=mode=max");
}
const packDockerArtifact = step(dockerProducer, "Pack Docker E2E image artifact");
expect(packDockerArtifact.env?.PACKAGE_SHA256).toBe("${{ steps.package.outputs.sha256 }}");
expect(packDockerArtifact.run).toContain("shared-image-artifact.sh");
expect(packDockerArtifact.run).toContain(
"docker-e2e-shared-images-${SHARED_IMAGE_ARTIFACT_NAMESPACE}-${TARGET_SHA:0:12}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}",
);
expect(packDockerArtifact.run).toContain(
'OPENCLAW_SHARED_IMAGE_PACKAGE_SHA256="$PACKAGE_SHA256"',
);
expect(packDockerArtifact.run).toContain("archive_sha256=");
const validatePackage = step(dockerProducer, "Validate OpenClaw Docker E2E package");
expect(validatePackage.env).toMatchObject({
EXPECTED_PACKAGE_FILE_NAME: "${{ inputs.package_file_name }}",
EXPECTED_PACKAGE_SHA256: "${{ inputs.package_sha256 }}",
EXPECTED_PACKAGE_SOURCE_SHA: "${{ inputs.package_source_sha }}",
EXPECTED_PACKAGE_VERSION: "${{ inputs.package_version }}",
});
expect(validatePackage.run).toContain('"$SHARED_IMAGE_POLICY" == "no-push-artifact"');
expect(validatePackage.run).toContain(
"Resolved package identity differs from the declared immutable tuple.",
);
expect(validatePackage.run).toContain("package/dist/build-info.json");
expect(validatePackage.run).toContain('[[ "$package_source_sha" == "$SELECTED_SHA" ]]');
const targetedRun = step(
job(workflow, "validate_docker_lanes"),
"Run targeted Docker E2E lanes",
);
expect(targetedRun.env).toMatchObject({
ARTIFACT_SUFFIX: "${{ steps.plan.outputs.artifact_suffix }}",
INCLUDE_RELEASE_PATH_SUITES: "${{ inputs.include_release_path_suites }}",
});
expect(targetedRun.run).toContain('if [[ "$INCLUDE_RELEASE_PATH_SUITES" == "true" ]]');
expect(targetedRun.run).not.toContain("${{ inputs.");
for (const workflowJob of Object.values(workflow.jobs ?? {})) {
for (const workflowStep of workflowJob.steps ?? []) {
for (const inputName of ["shared_image_policy", "package_sha256", "package_version"]) {
expect(workflowStep.run ?? "", `${workflowStep.name}:${inputName}`).not.toContain(
`\${{ inputs.${inputName} }}`,
);
}
}
}
expect(readFileSync(LIVE_E2E, "utf8")).not.toContain("fromJSON(toJSON(job)).workflow_");
expect(readFileSync(LIVE_E2E, "utf8")).not.toContain("${{ github.workflow_sha }}");
const artifactPackAndLoadSteps = Object.values(workflow.jobs ?? {}).flatMap((workflowJob) =>
(workflowJob.steps ?? []).filter((candidate) => candidate.env?.WORKFLOW_SHA !== undefined),
);
expect(artifactPackAndLoadSteps).toHaveLength(8);
for (const artifactStep of artifactPackAndLoadSteps) {
expect(artifactStep.env?.WORKFLOW_SHA, artifactStep.name).toBe(
"${{ needs.validate_selected_ref.outputs.workflow_sha }}",
);
}
expect(step(dockerProducer, "Upload Docker E2E image artifact")).toMatchObject({
id: "upload_image_artifact",
if: "inputs.shared_image_policy == 'no-push-artifact' && steps.plan.outputs.needs_e2e_image == '1'",
with: { "if-no-files-found": "error" },
});
expect(step(liveProducer, "Pack live-test image artifact").run).toContain(
"shared-image-artifact.sh",
);
expect(step(liveProducer, "Pack live-test image artifact").run).toContain(
"live-test-shared-image-${SHARED_IMAGE_ARTIFACT_NAMESPACE}-${TARGET_SHA:0:12}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}",
);
expect(step(liveProducer, "Upload live-test image artifact")).toMatchObject({
id: "upload_image_artifact",
if: "inputs.shared_image_policy == 'no-push-artifact'",
with: { "if-no-files-found": "error" },
});
expect(step(liveProducer, "Build shared live-test image").with).toMatchObject({
load: true,
provenance: false,
push: false,
sbom: false,
});
const dockerLoginCondition = step(dockerProducer, "Log in to GHCR").if;
expect(dockerLoginCondition).toContain("shared_image_policy == 'existing-only'");
expect(dockerLoginCondition).not.toContain("allow-push");
expect(step(liveProducer, "Log in to GHCR").if).toContain(
"shared_image_policy != 'no-push-artifact'",
);
expect(step(dockerProducer, "Check existing shared Docker E2E images").if).toContain(
"shared_image_policy == 'existing-only'",
);
expect(step(liveProducer, "Check existing shared live-test image").if).toContain(
"shared_image_policy != 'no-push-artifact'",
);
const shellPushSteps = Object.entries(workflow.jobs ?? {}).flatMap(([jobName, workflowJob]) =>
(workflowJob.steps ?? [])
.filter((candidate) => candidate.run?.includes("--push"))
.map((candidate) => ({ candidate, jobName })),
);
expect(shellPushSteps).toEqual([]);
for (const name of [
"validate_docker_e2e",
"validate_docker_lanes",
"validate_docker_openwebui",
]) {
const consumer = job(workflow, name);
expect(consumer.needs).toContain("docker_e2e_image_ready");
expect(consumer.env?.OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE).toContain("no-push-artifact");
expect(step(consumer, "Download OpenClaw Docker E2E package").with).toMatchObject({
"artifact-ids": "${{ needs.prepare_docker_e2e_image.outputs.package_artifact_id }}",
"github-token": "${{ github.token }}",
"run-id": "${{ needs.prepare_docker_e2e_image.outputs.package_artifact_run_id }}",
});
const binding = step(consumer, "Validate Docker E2E image artifact binding");
expect(binding.if).toContain("shared_image_policy == 'no-push-artifact'");
expect(binding.env).toMatchObject({
ARTIFACT_DIGEST: "${{ needs.prepare_docker_e2e_image.outputs.image_artifact_digest }}",
ARTIFACT_ID: "${{ needs.prepare_docker_e2e_image.outputs.image_artifact_id }}",
ARTIFACT_NAME: "${{ needs.prepare_docker_e2e_image.outputs.image_artifact_name }}",
ARTIFACT_RUN_ATTEMPT:
"${{ needs.prepare_docker_e2e_image.outputs.image_artifact_run_attempt }}",
ARTIFACT_RUN_ID: "${{ needs.prepare_docker_e2e_image.outputs.image_artifact_run_id }}",
GH_TOKEN: "${{ github.token }}",
});
expect(binding.run).toContain('verify-upload "Docker E2E image"');
expect(binding.run).toContain('"$ARTIFACT_ID" "$ARTIFACT_NAME" "$ARTIFACT_DIGEST"');
expect(binding.run).toContain('"$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT"');
const download = step(consumer, "Download Docker E2E image artifact");
expect(download.if).toContain("shared_image_policy == 'no-push-artifact'");
expect(download.with).toMatchObject({
"artifact-ids": "${{ needs.prepare_docker_e2e_image.outputs.image_artifact_id }}",
"github-token": "${{ github.token }}",
"run-id": "${{ needs.prepare_docker_e2e_image.outputs.image_artifact_run_id }}",
});
expect(consumer.steps?.indexOf(binding) ?? -1).toBeLessThan(
consumer.steps?.indexOf(download) ?? -1,
);
const loadArtifact = step(consumer, "Verify and load Docker E2E image artifact");
expect(loadArtifact.env?.ARCHIVE_SHA256).toBe(
"${{ needs.prepare_docker_e2e_image.outputs.image_archive_sha256 }}",
);
expect(loadArtifact.env?.PACKAGE_SHA256).toBe(
"${{ needs.prepare_docker_e2e_image.outputs.package_sha256 }}",
);
expect(loadArtifact.env?.OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT).toBe(
"${{ needs.prepare_docker_e2e_image.outputs.image_artifact_run_attempt }}",
);
expect(loadArtifact.env?.OPENCLAW_SHARED_IMAGE_RUN_ID).toBe(
"${{ needs.prepare_docker_e2e_image.outputs.image_artifact_run_id }}",
);
expect(loadArtifact.run).toContain("shared-image-artifact.sh");
expect(loadArtifact.run).toContain('OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256="$ARCHIVE_SHA256"');
expect(loadArtifact.run).toContain('OPENCLAW_SHARED_IMAGE_PACKAGE_SHA256="$PACKAGE_SHA256"');
expect(step(consumer, "Log in to GHCR for shared Docker E2E image").if).toContain(
"shared_image_policy != 'no-push-artifact'",
);
for (const pullName of [
"Pull shared bare Docker E2E image",
"Pull shared functional Docker E2E image",
]) {
expect(step(consumer, pullName).if).toContain("shared_image_policy != 'no-push-artifact'");
}
}
for (const name of [
"validate_live_models_docker",
"validate_live_models_docker_targeted",
"validate_live_docker_provider_suites",
]) {
const consumer = job(workflow, name);
expect(consumer.needs).toContain("live_test_image_ready");
expect(consumer.env?.OPENCLAW_LIVE_REQUIRE_LOCAL_IMAGE).toContain("no-push-artifact");
const binding = step(consumer, "Validate live-test image artifact binding");
expect(binding.if).toContain("shared_image_policy == 'no-push-artifact'");
expect(binding.env).toMatchObject({
ARTIFACT_DIGEST: "${{ needs.prepare_live_test_image.outputs.image_artifact_digest }}",
ARTIFACT_ID: "${{ needs.prepare_live_test_image.outputs.image_artifact_id }}",
ARTIFACT_NAME: "${{ needs.prepare_live_test_image.outputs.image_artifact_name }}",
ARTIFACT_RUN_ATTEMPT:
"${{ needs.prepare_live_test_image.outputs.image_artifact_run_attempt }}",
ARTIFACT_RUN_ID: "${{ needs.prepare_live_test_image.outputs.image_artifact_run_id }}",
GH_TOKEN: "${{ github.token }}",
});
expect(binding.run).toContain('verify-upload "live-test image"');
expect(binding.run).toContain('"$ARTIFACT_ID" "$ARTIFACT_NAME" "$ARTIFACT_DIGEST"');
expect(binding.run).toContain('"$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT"');
const download = step(consumer, "Download live-test image artifact");
expect(download.if).toContain("shared_image_policy == 'no-push-artifact'");
expect(download.with).toMatchObject({
"artifact-ids": "${{ needs.prepare_live_test_image.outputs.image_artifact_id }}",
"github-token": "${{ github.token }}",
"run-id": "${{ needs.prepare_live_test_image.outputs.image_artifact_run_id }}",
});
expect(consumer.steps?.indexOf(binding) ?? -1).toBeLessThan(
consumer.steps?.indexOf(download) ?? -1,
);
const loadArtifact = step(consumer, "Verify and load live-test image artifact");
expect(loadArtifact.env?.ARCHIVE_SHA256).toBe(
"${{ needs.prepare_live_test_image.outputs.image_archive_sha256 }}",
);
expect(loadArtifact.env?.OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT).toBe(
"${{ needs.prepare_live_test_image.outputs.image_artifact_run_attempt }}",
);
expect(loadArtifact.env?.OPENCLAW_SHARED_IMAGE_RUN_ID).toBe(
"${{ needs.prepare_live_test_image.outputs.image_artifact_run_id }}",
);
expect(loadArtifact.run).toContain("shared-image-artifact.sh");
expect(loadArtifact.run).toContain('OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256="$ARCHIVE_SHA256"');
expect(step(consumer, "Log in to GHCR").if).toContain(
"shared_image_policy != 'no-push-artifact'",
);
}
const liveBuild = readFileSync(LIVE_BUILD, "utf8");
const requireLocalIndex = liveBuild.indexOf("OPENCLAW_LIVE_REQUIRE_LOCAL_IMAGE");
const pullIndex = liveBuild.indexOf("Live-test image not found locally; pulling");
expect(requireLocalIndex).toBeGreaterThanOrEqual(0);
expect(pullIndex).toBeGreaterThan(requireLocalIndex);
expect(liveBuild).toContain("Required local live-test image not found");
});
it("keeps Docker-save validation artifacts unreachable from package writers", () => {
const liveWorkflow = readWorkflow(LIVE_E2E);
const scheduled = readWorkflow(SCHEDULED_LIVE);
expect(existsSync(SHARED_IMAGE_PUBLISHER)).toBe(false);
expect(liveWorkflow.on?.workflow_call?.outputs?.publication_manifest).toBeUndefined();
expect(liveWorkflow.jobs?.collect_shared_image_publication).toBeUndefined();
expect(scheduled.jobs?.publish_shared_images).toBeUndefined();
const scheduledWriters = Object.entries(scheduled.jobs ?? {})
.filter(
([, workflowJob]) => permissionAt(workflowJob.permissions, "packages", "none") === "write",
)
.map(([name]) => name);
expect(scheduledWriters).toEqual([]);
const publisherCallers = readdirSync(".github/workflows")
.filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
.filter((name) =>
readFileSync(join(".github/workflows", name), "utf8").includes(
"openclaw-shared-image-publish-reusable.yml",
),
);
expect(publisherCallers).toEqual([]);
expect(JSON.stringify(liveWorkflow.jobs)).not.toContain("docker image push");
expect(JSON.stringify(scheduled.jobs)).not.toContain("docker image push");
const scheduledValidation = job(scheduled, "live_and_openwebui_checks");
expect(permissionAt(scheduled.permissions, "packages", "none")).toBe("read");
expectReadOnlyPackagePermission(scheduledValidation);
expect(scheduledValidation.with).toMatchObject({
allow_unreleased_changelog: true,
shared_image_artifact_namespace: "scheduled-live",
shared_image_policy: "no-push-artifact",
});
const dockerRelease = readWorkflow(DOCKER_RELEASE);
const attestedBuilds = Object.values(dockerRelease.jobs ?? {}).flatMap((workflowJob) =>
(workflowJob.steps ?? []).filter(
(candidate) =>
candidate.uses?.startsWith("docker/build-push-action@") && candidate.with?.push === true,
),
);
expect(attestedBuilds).toHaveLength(4);
for (const build of attestedBuilds) {
expect(build.with).toMatchObject({
provenance: "mode=max",
push: true,
sbom: true,
});
}
});
it("keeps performance evidence artifact-only when dispatched by Full Release", () => {
const fullText = readFileSync(FULL_RELEASE, "utf8");
const performance = readWorkflow(PERFORMANCE);
const publisher = job(performance, "publish");
const dangerousSteps = [
"Prepare clawgrit report commit",
"Create clawgrit reports app token",
"Publish to clawgrit reports",
];
expect(performance.on?.workflow_dispatch?.inputs?.publish_reports).toMatchObject({
default: true,
type: "boolean",
});
expect(fullText).toContain("-f publish_reports=false");
expect(fullText).toContain("Report publication: disabled (artifacts only)");
expect(fullText).toContain('performanceReportPublication: "artifact-only"');
expect(publisher.if).toContain("inputs.publish_reports == true");
const guard = job(performance, "artifact_only_guard");
expect(guard.if).toContain("inputs.publish_reports != true");
expect(step(guard, "Verify report publisher stayed disabled").run).toContain(
'[[ "$PUBLISH_RESULT" != "skipped" ]]',
);
for (const name of dangerousSteps) {
expect(step(publisher, name)).toBeDefined();
}
for (const [name, workflowJob] of Object.entries(performance.jobs ?? {})) {
if (name === "publish") {
continue;
}
const text = JSON.stringify(workflowJob);
expect(text).not.toContain("CLAWGRIT_REPORTS_APP_TOKEN");
expect(text).not.toContain("create-github-app-token");
expect(text).not.toContain("git push");
}
});
it("fails a missing required local live image before any registry pull", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-live-local-image-"));
const bin = join(root, "bin");
const calls = join(root, "docker.log");
try {
mkdirSync(bin);
writeFileSync(calls, "");
const docker = join(bin, "docker");
writeFileSync(
docker,
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$FAKE_DOCKER_LOG"
if [[ "$1" == "image" && "$2" == "inspect" ]]; then
exit 1
fi
if [[ "$1" == "pull" ]]; then
exit 0
fi
exit 2
`,
);
chmodSync(docker, 0o755);
const result = spawnSync("bash", [resolve(LIVE_BUILD)], {
encoding: "utf8",
env: {
...process.env,
DOCKER_COMMAND_TIMEOUT: "5s",
FAKE_DOCKER_LOG: calls,
OPENCLAW_LIVE_IMAGE: "openclaw-live-test:required-local",
OPENCLAW_LIVE_REQUIRE_LOCAL_IMAGE: "1",
OPENCLAW_SKIP_DOCKER_BUILD: "1",
PATH: `${bin}:${process.env.PATH ?? ""}`,
},
});
expect(result.status).toBe(1);
expect(result.stderr).toContain(
"Required local live-test image not found: openclaw-live-test:required-local",
);
expect(readFileSync(calls, "utf8")).toBe("image inspect openclaw-live-test:required-local\n");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("fails a missing required local Docker E2E image before pull or build fallback", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-docker-e2e-local-image-"));
const bin = join(root, "bin");
const calls = join(root, "docker.log");
try {
mkdirSync(bin);
writeFileSync(calls, "");
const docker = join(bin, "docker");
writeFileSync(
docker,
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$FAKE_DOCKER_LOG"
if [[ "$1" == "image" && "$2" == "inspect" ]]; then
exit 1
fi
if [[ "$1" == "pull" ]]; then
exit 0
fi
exit 2
`,
);
chmodSync(docker, 0o755);
const result = spawnSync(
"bash",
[
"-c",
`source "$1"
docker_e2e_build_or_reuse "openclaw-e2e:required-local" "required local image test"`,
"bash",
resolve(DOCKER_E2E_IMAGE_HELPER),
],
{
encoding: "utf8",
env: {
...process.env,
FAKE_DOCKER_LOG: calls,
OPENCLAW_DOCKER_BUILD_ON_MISSING: "1",
OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE: "1",
OPENCLAW_SKIP_DOCKER_BUILD: "1",
PATH: `${bin}:${process.env.PATH ?? ""}`,
},
},
);
expect(result.status).toBe(1);
expect(result.stderr).toContain(
"Required local Docker E2E image not found: openclaw-e2e:required-local",
);
expect(readFileSync(calls, "utf8")).toBe("image inspect openclaw-e2e:required-local\n");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
});