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; id?: string; if?: string; name?: string; run?: string; uses?: string; with?: Record; }; type WorkflowJob = { env?: Record; if?: string; needs?: string | string[]; outputs?: Record; permissions?: PermissionMap; steps?: WorkflowStep[]; uses?: string; with?: Record; }; type Workflow = { jobs?: Record; on?: { workflow_call?: { inputs?: Record; outputs?: Record; }; workflow_dispatch?: { inputs?: Record }; }; permissions?: PermissionMap; }; type PermissionLevel = "none" | "read" | "write"; type PermissionMap = "read-all" | "write-all" | Record; const PERMISSION_RANK: Record = { 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): 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[] { 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, chain: string, seen: Set, ): 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 = {}; 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("builds planned live images locally without entering pull fallback", () => { const workflow = readWorkflow(LIVE_E2E); for (const jobName of [ "validate_docker_e2e", "validate_docker_lanes", "validate_docker_openwebui", ]) { const job = workflow.jobs?.[jobName]; const runStep = job?.steps?.find((candidate) => candidate.run?.includes("test-live-build-docker.sh"), ); expect(runStep?.run, jobName).toContain("OPENCLAW_SKIP_DOCKER_BUILD=0"); expect(runStep?.run, jobName).not.toContain("OPENCLAW_DOCKER_BUILD_ON_MISSING=1"); } }); 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 }); } }); });