From a190f4d5c247a2d55aa6d947e65c08a1998feca8 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 21 Aug 2026 02:42:15 -0700 Subject: [PATCH] fix(ci): declare repo e2e shard capability --- .github/repo-e2e-capabilities.json | 12 ++ .../openclaw-live-and-e2e-checks-reusable.yml | 86 ++++----- scripts/build-all.mts | 11 +- scripts/docker/shared-image-artifact.sh | 3 +- scripts/validate-repo-e2e-capability.mjs | 101 ++++++++++ test/scripts/build-all.test.ts | 39 +++- test/scripts/ci-workflow-guards.test.ts | 172 +++++++++++++++++- test/scripts/shared-image-artifact.test.ts | 19 ++ 8 files changed, 378 insertions(+), 65 deletions(-) create mode 100644 .github/repo-e2e-capabilities.json create mode 100644 scripts/validate-repo-e2e-capability.mjs diff --git a/.github/repo-e2e-capabilities.json b/.github/repo-e2e-capabilities.json new file mode 100644 index 000000000000..9d1108bc4480 --- /dev/null +++ b/.github/repo-e2e-capabilities.json @@ -0,0 +1,12 @@ +{ + "schema": "openclaw.repo-e2e-capabilities/v1", + "schemaVersion": 1, + "repoE2eShards": { + "agentPluginGateway": true, + "gatewayShards": 4, + "realGatewayUi": true, + "runtimeBuildProfile": "repoE2eRuntime", + "sandboxArtifact": true, + "uiShards": 4 + } +} diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 3540b2d61513..f9544879f31f 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -544,7 +544,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 30 outputs: - repo_e2e_shards_available: ${{ steps.validate.outputs.repo_e2e_shards_available }} + repo_e2e_shards_available: ${{ steps.repo_e2e.outputs.repo_e2e_shards_available }} selected_sha: ${{ steps.validate.outputs.selected_sha }} trusted_reason: ${{ steps.validate.outputs.trusted_reason }} workflow_repository: ${{ steps.workflow.outputs.workflow_repository }} @@ -587,6 +587,15 @@ jobs: with: fetch-depth: 0 + - name: Checkout trusted repo E2E capability validator + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: ${{ steps.workflow.outputs.workflow_repository }} + ref: ${{ steps.workflow.outputs.workflow_sha }} + path: .release-harness + fetch-depth: 1 + persist-credentials: false + - name: Validate selected ref id: validate env: @@ -617,7 +626,6 @@ jobs: SHARED_IMAGE_ARTIFACT_RUN_ID: ${{ inputs.shared_image_artifact_run_id }} SHARED_IMAGE_ARCHIVE_SHA256: ${{ inputs.shared_image_archive_sha256 }} SHARED_IMAGE_POLICY: ${{ inputs.shared_image_policy }} - WORKFLOW_SHA: ${{ steps.workflow.outputs.workflow_sha }} shell: bash run: | set -euo pipefail @@ -768,49 +776,37 @@ jobs: ;; esac - repo_e2e_shards_available=true - if [[ "$selected_sha" != "$WORKFLOW_SHA" ]]; then - repo_e2e_shards_available=false - fi - required_repo_e2e_paths=( - scripts/agent-plugin-gateway-e2e.ts - scripts/build-all.mts - scripts/ensure-playwright-chromium.mts - test/vitest/vitest.e2e.config.ts - test/vitest/vitest.e2e.global-setup.ts - test/vitest/vitest.ui-e2e.config.ts - test/vitest/vitest.ui-e2e.global-setup.ts - test/vitest/vitest.ui-e2e.sequencer.ts - ) - for required_path in "${required_repo_e2e_paths[@]}"; do - if ! git cat-file -e "${selected_sha}:${required_path}" 2>/dev/null; then - repo_e2e_shards_available=false - break - fi - done - if [[ "$repo_e2e_shards_available" == "true" ]] && - ! git grep -Fq 'repoE2eRuntime:' "$selected_sha" -- scripts/build-all.mts; then - repo_e2e_shards_available=false - fi - if [[ "$repo_e2e_shards_available" == "true" ]] && - ! git grep -Fq 'OPENCLAW_E2E_USE_PREBUILT_DIST' "$selected_sha" -- test/vitest/vitest.e2e.global-setup.ts; then - repo_e2e_shards_available=false - fi - if [[ "$repo_e2e_shards_available" == "true" ]] && - ! git grep -Fq 'OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY' "$selected_sha" -- test/vitest/vitest.ui-e2e.config.ts; then - repo_e2e_shards_available=false - fi - - echo "repo_e2e_shards_available=$repo_e2e_shards_available" >> "$GITHUB_OUTPUT" echo "selected_sha=$selected_sha" >> "$GITHUB_OUTPUT" echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT" { echo "Validated ref: \`${INPUT_REF}\`" echo "Resolved SHA: \`$selected_sha\`" echo "Trust reason: \`$trusted_reason\`" - echo "Repo E2E mode: \`$([[ "$repo_e2e_shards_available" == "true" ]] && echo sharded || echo legacy)\`" } >> "$GITHUB_STEP_SUMMARY" + - name: Resolve repo E2E capability + id: repo_e2e + env: + CAPABILITY_PATH: .github/repo-e2e-capabilities.json + SELECTED_SHA: ${{ steps.validate.outputs.selected_sha }} + shell: bash + run: | + set -euo pipefail + manifest_path="${RUNNER_TEMP}/repo-e2e-capabilities.json" + rm -f "$manifest_path" + if git cat-file -e "${SELECTED_SHA}:${CAPABILITY_PATH}" 2>/dev/null; then + manifest_size="$(git cat-file -s "${SELECTED_SHA}:${CAPABILITY_PATH}")" + if [[ "$manifest_size" =~ ^[0-9]+$ && "$manifest_size" -le 8192 ]]; then + git show "${SELECTED_SHA}:${CAPABILITY_PATH}" > "$manifest_path" + else + printf '{"invalid":"oversized"}\n' > "$manifest_path" + fi + fi + node .release-harness/scripts/validate-repo-e2e-capability.mjs \ + --manifest "$manifest_path" \ + --github-output "$GITHUB_OUTPUT" \ + --github-step-summary "$GITHUB_STEP_SUMMARY" + validate_live_suite_filter: runs-on: ubuntu-24.04 if: inputs.live_suite_filter != '' @@ -1039,6 +1035,7 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env with: + cache-mode: restore node-version: ${{ env.NODE_VERSION }} install-bun: "true" build-all-cache-scope: full @@ -1055,8 +1052,9 @@ jobs: WORKFLOW_SHA: ${{ needs.validate_selected_ref.outputs.workflow_sha }} run: | set -euo pipefail - artifact_name="repo-e2e-runtime-${TARGET_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - archive_name="${artifact_name}.tar.zst" + artifact_stem="repo-e2e-runtime-${TARGET_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + archive_name="${artifact_stem}.tar.zst" + artifact_name="$archive_name" archive_path="${RUNNER_TEMP}/${archive_name}" bundle_root="${RUNNER_TEMP}/repo-e2e-runtime-bundle" archive_root="repo-e2e-runtime" @@ -1235,6 +1233,7 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env with: + cache-mode: restore node-version: ${{ env.NODE_VERSION }} install-bun: "true" @@ -1249,7 +1248,7 @@ jobs: TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} run: | set -euo pipefail - expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" + expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst" [[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || { echo "Repo E2E runtime artifact name does not match the target and producer run attempt." >&2 exit 1 @@ -1396,6 +1395,7 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env with: + cache-mode: restore node-version: ${{ env.NODE_VERSION }} install-bun: "false" @@ -1410,7 +1410,7 @@ jobs: TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} run: | set -euo pipefail - expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" + expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst" [[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || { echo "Repo E2E runtime artifact name does not match the target and producer run attempt." >&2 exit 1 @@ -1501,6 +1501,7 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env with: + cache-mode: restore node-version: ${{ env.NODE_VERSION }} install-bun: "false" @@ -1560,6 +1561,7 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env with: + cache-mode: restore node-version: ${{ env.NODE_VERSION }} install-bun: "false" @@ -1574,7 +1576,7 @@ jobs: TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} run: | set -euo pipefail - expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" + expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst" [[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || { echo "Repo E2E runtime artifact name does not match the target and producer run attempt." >&2 exit 1 diff --git a/scripts/build-all.mts b/scripts/build-all.mts index efe0b5098277..8b969aff1ac7 100644 --- a/scripts/build-all.mts +++ b/scripts/build-all.mts @@ -341,13 +341,17 @@ export const BUILD_ALL_PROFILES: Record = { ], repoE2eRuntime: [ "plugins:assets:build", - "tsdown", + "tsdown-ai", + "tsdown-packages", + "tsdown-unified", "external-plugins:local-dist", "check-cli-bootstrap-imports", "plugins:assets:copy", "runtime-postbuild", "build-stamp", "runtime-postbuild-stamp", + "write-plugin-sdk-entry-dts", + "check-plugin-sdk-exports", "ui:build", ], sourcePerformance: [ @@ -405,11 +409,6 @@ export const BUILD_ALL_PROFILE_STEP_ENV: Record --github-output --github-step-summary ", + ); + } + values.set(name, value); + } + for (const required of ["--manifest", "--github-output", "--github-step-summary"]) { + if (!values.has(required)) { + throw new Error(`${required} is required`); + } + } + return values; +} + +function hasExactKeys(value, expectedKeys) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const actualKeys = Object.keys(value).toSorted(); + return ( + actualKeys.length === expectedKeys.length && + actualKeys.every((key, index) => key === expectedKeys[index]) + ); +} + +function validateManifest(manifestPath) { + let stat; + try { + stat = lstatSync(manifestPath); + } catch (error) { + if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { + return { available: false, reason: "missing" }; + } + throw error; + } + if (!stat.isFile() || stat.size > MAX_MANIFEST_BYTES) { + return { available: false, reason: "invalid" }; + } + + let value; + try { + value = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch { + return { available: false, reason: "invalid" }; + } + if (!hasExactKeys(value, EXPECTED_ROOT_KEYS)) { + return { available: false, reason: "invalid" }; + } + if (value.schema !== EXPECTED_SCHEMA || value.schemaVersion !== 1) { + return { available: false, reason: "unsupported" }; + } + if (!hasExactKeys(value.repoE2eShards, EXPECTED_SHARD_KEYS)) { + return { available: false, reason: "invalid" }; + } + + const capability = value.repoE2eShards; + const valid = + capability.agentPluginGateway === true && + capability.gatewayShards === 4 && + capability.realGatewayUi === true && + capability.runtimeBuildProfile === "repoE2eRuntime" && + capability.sandboxArtifact === true && + capability.uiShards === 4; + return valid + ? { available: true, reason: "contract-v1" } + : { available: false, reason: "unsupported" }; +} + +const args = parseArgs(process.argv.slice(2)); +const result = validateManifest(args.get("--manifest")); +const mode = result.available ? "sharded" : "legacy"; +appendFileSync( + args.get("--github-output"), + `repo_e2e_shards_available=${result.available}\nrepo_e2e_capability_reason=${result.reason}\n`, +); +appendFileSync( + args.get("--github-step-summary"), + `Repo E2E mode: \`${mode}\`\nRepo E2E capability: \`${result.reason}\`\n`, +); diff --git a/test/scripts/build-all.test.ts b/test/scripts/build-all.test.ts index ecd552d09d14..050024d2d922 100644 --- a/test/scripts/build-all.test.ts +++ b/test/scripts/build-all.test.ts @@ -452,7 +452,6 @@ describe("resolveBuildAllSteps", () => { for (const profile of [ "gatewayWatch", "qaRuntime", - "repoE2eRuntime", "sourcePerformance", "cliStartup", ] as const) { @@ -474,6 +473,20 @@ describe("resolveBuildAllSteps", () => { } }); + it("keeps repository E2E runtime declarations for package compatibility proofs", () => { + const steps = resolveBuildAllSteps("repoE2eRuntime"); + expect(steps.map((step) => step.label)).toEqual( + expect.arrayContaining([ + "tsdown-ai", + "tsdown-packages", + "tsdown-unified", + "write-plugin-sdk-entry-dts", + "check-plugin-sdk-exports", + ]), + ); + expect(steps.some((step) => step.label === "tsdown")).toBe(false); + }); + it("skips global declarations on CI artifacts and self-builds the plugin-sdk gate", () => { // Global dts emission is ~95% of the tsdown wall clock; PR CI dist // consumers are runtime JS, and the plugin-sdk export gate validates @@ -552,7 +565,7 @@ describe("resolveBuildAllSteps", () => { }); } - for (const profile of ["gatewayWatch", "qaRuntime", "repoE2eRuntime"]) { + for (const profile of ["gatewayWatch", "qaRuntime"]) { const tsdown = resolveBuildAllSteps(profile).find((step) => step.label === "tsdown"); if (!tsdown) { throw new Error(`Missing ${profile} tsdown step`); @@ -562,6 +575,16 @@ describe("resolveBuildAllSteps", () => { "OPENCLAW_PRESERVE_CLI_STARTUP_METADATA", ); } + + const repoE2eTsdown = resolveBuildAllSteps("repoE2eRuntime").find( + (step) => step.label === "tsdown-unified", + ); + if (!repoE2eTsdown) { + throw new Error("Missing repoE2eRuntime tsdown-unified step"); + } + expect(resolveBuildAllStep(repoE2eTsdown, { env: {} }).options.env).not.toHaveProperty( + "OPENCLAW_PRESERVE_CLI_STARTUP_METADATA", + ); }); it("uses a minimal built runtime profile for gateway watch regression", () => { @@ -591,13 +614,17 @@ describe("resolveBuildAllSteps", () => { it("uses a repo E2E runtime profile with private QA runtime and Control UI assets", () => { expect(resolveBuildAllSteps("repoE2eRuntime").map((step) => step.label)).toEqual([ "plugins:assets:build", - "tsdown", + "tsdown-ai", + "tsdown-packages", + "tsdown-unified", "external-plugins:local-dist", "check-cli-bootstrap-imports", "plugins:assets:copy", "runtime-postbuild", "build-stamp", "runtime-postbuild-stamp", + "write-plugin-sdk-entry-dts", + "check-plugin-sdk-exports", "ui:build", ]); }); @@ -664,11 +691,7 @@ describe("resolveBuildAllSteps", () => { throw new Error(`Missing ${profile} runtime-postbuild step`); } - expect( - expectDefined(BUILD_ALL_PROFILE_STEP_ENV[profile], `${profile} build step env`)[ - "runtime-postbuild" - ], - ).toBeUndefined(); + expect(BUILD_ALL_PROFILE_STEP_ENV[profile]?.["runtime-postbuild"]).toBeUndefined(); expect( resolveBuildAllStep(runtimePostbuild, { env: { OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "1" }, diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index af18441f2368..ed9a73211f10 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -4135,17 +4135,38 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre const validateStep = validateRef.steps.find( (step: WorkflowStep) => step.name === "Validate selected ref", ); + const capabilityCheckout = validateRef.steps.find( + (step: WorkflowStep) => step.name === "Checkout trusted repo E2E capability validator", + ); + const capabilityStep = validateRef.steps.find( + (step: WorkflowStep) => step.name === "Resolve repo E2E capability", + ); expect(validateRef.outputs.repo_e2e_shards_available).toBe( - "${{ steps.validate.outputs.repo_e2e_shards_available }}", + "${{ steps.repo_e2e.outputs.repo_e2e_shards_available }}", ); - expect(validateStep.run).toContain("required_repo_e2e_paths=("); - expect(validateStep.env.WORKFLOW_SHA).toBe("${{ steps.workflow.outputs.workflow_sha }}"); - expect(validateStep.run).toContain('if [[ "$selected_sha" != "$WORKFLOW_SHA" ]]'); - expect(validateStep.run).toContain("repoE2eRuntime:"); - expect(validateStep.run).toContain("OPENCLAW_E2E_USE_PREBUILT_DIST"); - expect(validateStep.run).toContain("OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY"); - expect(validateStep.run).toContain( + expect(capabilityCheckout?.with).toMatchObject({ + repository: "${{ steps.workflow.outputs.workflow_repository }}", + ref: "${{ steps.workflow.outputs.workflow_sha }}", + path: ".release-harness", + "persist-credentials": false, + }); + expect(capabilityStep?.env).toMatchObject({ + CAPABILITY_PATH: ".github/repo-e2e-capabilities.json", + SELECTED_SHA: "${{ steps.validate.outputs.selected_sha }}", + }); + expect(capabilityStep?.run).toContain( + "node .release-harness/scripts/validate-repo-e2e-capability.mjs", + ); + expect(capabilityStep?.run).toContain('git show "${SELECTED_SHA}:${CAPABILITY_PATH}"'); + expect(capabilityStep?.run).not.toContain("WORKFLOW_SHA"); + expect(validateStep.run).not.toContain("required_repo_e2e_paths=("); + expect(validateStep.env).not.toHaveProperty("WORKFLOW_SHA"); + expect(validateStep.run).not.toContain('if [[ "$selected_sha" != "$WORKFLOW_SHA" ]]'); + expect(validateStep.run).not.toContain("repoE2eRuntime:"); + expect(validateStep.run).not.toContain("OPENCLAW_E2E_USE_PREBUILT_DIST"); + expect(validateStep.run).not.toContain("OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY"); + expect(validateStep.run).not.toContain( 'echo "repo_e2e_shards_available=$repo_e2e_shards_available" >> "$GITHUB_OUTPUT"', ); @@ -4180,6 +4201,7 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre expect(packRuntime?.run).toContain('--arg buildCommand "pnpm build repoE2eRuntime"'); expect(packRuntime?.run).toContain("manifest.json"); expect(packRuntime?.run).toContain('archive_sha256=$(sha256sum "$archive_path"'); + expect(packRuntime?.run).toContain('artifact_name="$archive_name"'); const uploadRuntime = runtime.steps.find( (step: WorkflowStep) => step.name === "Upload repo E2E runtime", ); @@ -4244,6 +4266,13 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre (step: WorkflowStep) => step.name === "Validate repo E2E runtime artifact binding", )?.run, ).toContain('verify-upload "Repo E2E runtime"'); + expect( + job.steps.find( + (step: WorkflowStep) => step.name === "Validate repo E2E runtime artifact binding", + )?.run, + ).toContain( + 'expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst"', + ); expect( job.steps.find((step: WorkflowStep) => step.name === "Download repo E2E runtime")?.with, ).toMatchObject({ @@ -4342,6 +4371,34 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre legacy.steps.find((step: WorkflowStep) => step.name === "Run legacy repo E2E suite")?.run, ).toBe("pnpm test:e2e"); + for (const [jobName, job] of Object.entries({ + prepare_repo_e2e_runtime: runtime, + validate_repo_e2e_gateway: gateway, + validate_repo_e2e_agent_plugin: agentPlugin, + validate_repo_e2e_ui: ui, + validate_repo_e2e_ui_real_gateway: realGateway, + validate_repo_e2e_legacy: legacy, + })) { + expect( + job.steps.find((step: WorkflowStep) => step.name === "Setup Node environment")?.with?.[ + "cache-mode" + ], + jobName, + ).toBe("restore"); + } + for (const job of [ui, realGateway]) { + expect( + job.steps.filter((step: WorkflowStep) => step.name === "Cache Playwright Chromium"), + ).toEqual([ + expect.objectContaining({ + uses: "actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae", + }), + ]); + expect( + job.steps.some((step: WorkflowStep) => step.uses?.startsWith("actions/cache/save@")), + ).toBe(false); + } + const gate = jobs.validate_repo_e2e; expect(gate.needs).toEqual([ "validate_selected_ref", @@ -4359,6 +4416,13 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre expect(gate["runs-on"]).toBe("ubuntu-24.04"); expect(gate["timeout-minutes"]).toBe(5); expect(gate["continue-on-error"]).toBe("${{ inputs.advisory }}"); + expect( + gate.steps.some( + (step: WorkflowStep) => + step.uses === "./.github/actions/setup-node-env" || + step.uses?.startsWith("actions/cache/"), + ), + ).toBe(false); const gateStep = gate.steps.find((step: WorkflowStep) => step.name === "Verify repo E2E lanes"); expect(gateStep.env).toMatchObject({ GATEWAY_RESULT: "${{ needs.validate_repo_e2e_gateway.result }}", @@ -4371,6 +4435,98 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre expect(gateStep.run).toContain('require_result legacy "$LEGACY_RESULT" success'); }); + it("uses the target-owned repo E2E capability across mixed target and tooling SHAs", () => { + const workflow = parse( + readFileSync(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml", "utf8"), + ); + const capabilityStep = workflow.jobs.validate_selected_ref.steps.find( + (step: WorkflowStep) => step.name === "Resolve repo E2E capability", + ); + const root = tempDirs.make("openclaw-repo-e2e-capability-"); + runGit(root, ["init", "-q", "-b", "main"]); + runGit(root, ["config", "commit.gpgsign", "false"]); + runGit(root, ["config", "user.email", "ci-fixture@example.com"]); + runGit(root, ["config", "user.name", "CI Fixture"]); + + writeFileSync(path.join(root, "fixture.txt"), "legacy\n", "utf8"); + runGit(root, ["add", "fixture.txt"]); + runGit(root, ["commit", "-q", "-m", "legacy target"]); + const legacySha = runGit(root, ["rev-parse", "HEAD"]); + + mkdirSync(path.join(root, ".github"), { recursive: true }); + writeFileSync( + path.join(root, ".github", "repo-e2e-capabilities.json"), + readFileSync(".github/repo-e2e-capabilities.json", "utf8"), + "utf8", + ); + runGit(root, ["add", ".github/repo-e2e-capabilities.json"]); + runGit(root, ["commit", "-q", "-m", "capable target"]); + const capableSha = runGit(root, ["rev-parse", "HEAD"]); + + writeFileSync(path.join(root, ".github", "repo-e2e-capabilities.json"), "{}\n", "utf8"); + runGit(root, ["add", ".github/repo-e2e-capabilities.json"]); + runGit(root, ["commit", "-q", "-m", "invalid target"]); + const invalidSha = runGit(root, ["rev-parse", "HEAD"]); + + writeFileSync(path.join(root, "fixture.txt"), "newer tooling\n", "utf8"); + runGit(root, ["add", "fixture.txt"]); + runGit(root, ["commit", "-q", "-m", "newer tooling"]); + const toolingSha = runGit(root, ["rev-parse", "HEAD"]); + expect(toolingSha).not.toBe(capableSha); + + const harnessScripts = path.join(root, ".release-harness", "scripts"); + mkdirSync(harnessScripts, { recursive: true }); + writeFileSync( + path.join(harnessScripts, "validate-repo-e2e-capability.mjs"), + readFileSync("scripts/validate-repo-e2e-capability.mjs", "utf8"), + "utf8", + ); + + const runCapability = (selectedSha: string) => { + const githubOutput = path.join(root, `output-${selectedSha}`); + const githubSummary = path.join(root, `summary-${selectedSha}`); + writeFileSync(githubOutput, "", "utf8"); + writeFileSync(githubSummary, "", "utf8"); + const run = runWorkflowShellScript(capabilityStep.run, { + cwd: root, + env: { + ...process.env, + CAPABILITY_PATH: ".github/repo-e2e-capabilities.json", + GITHUB_OUTPUT: githubOutput, + GITHUB_STEP_SUMMARY: githubSummary, + RUNNER_TEMP: root, + SELECTED_SHA: selectedSha, + WORKFLOW_SHA: toolingSha, + }, + }); + return { + ...run, + outputs: readWorkflowOutputs(githubOutput), + }; + }; + + const capable = runCapability(capableSha); + expect(capable.status, `${capable.stdout}\n${capable.stderr}`).toBe(0); + expect(capable.outputs).toMatchObject({ + repo_e2e_capability_reason: "contract-v1", + repo_e2e_shards_available: "true", + }); + + const legacy = runCapability(legacySha); + expect(legacy.status, `${legacy.stdout}\n${legacy.stderr}`).toBe(0); + expect(legacy.outputs).toMatchObject({ + repo_e2e_capability_reason: "missing", + repo_e2e_shards_available: "false", + }); + + const invalid = runCapability(invalidSha); + expect(invalid.status, `${invalid.stdout}\n${invalid.stderr}`).toBe(0); + expect(invalid.outputs).toMatchObject({ + repo_e2e_capability_reason: "invalid", + repo_e2e_shards_available: "false", + }); + }); + it("persists Node 22 declarations through trusted bounded artifacts", () => { const workflow = parse(readFileSync(".github/workflows/node22-compat.yml", "utf8")); const steps = workflow.jobs.compat.steps as WorkflowStep[]; diff --git a/test/scripts/shared-image-artifact.test.ts b/test/scripts/shared-image-artifact.test.ts index ba6b457fc087..a1cf8d53d000 100644 --- a/test/scripts/shared-image-artifact.test.ts +++ b/test/scripts/shared-image-artifact.test.ts @@ -308,6 +308,25 @@ describe("shared Docker image artifacts", () => { `api --method GET repos/openclaw/openclaw/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}`, ); + writeFileSync(fixture.ghLog, ""); + const rawArchiveName = `repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst`; + const rawArchive = verifyUploadedArtifact(fixture, { + artifactName: rawArchiveName, + env: { FAKE_ARTIFACT_NAME: rawArchiveName }, + }); + expect(rawArchive.status, `${rawArchive.stdout}\n${rawArchive.stderr}`).toBe(0); + + writeFileSync(fixture.ghLog, ""); + const unsupportedSuffix = verifyUploadedArtifact(fixture, { + artifactName: `${ARTIFACT_NAME}.zip`, + env: { FAKE_ARTIFACT_NAME: `${ARTIFACT_NAME}.zip` }, + }); + expect(unsupportedSuffix.status).not.toBe(0); + expect(unsupportedSuffix.stderr).toContain( + "artifact name does not bind the producer run attempt", + ); + expect(readFileSync(fixture.ghLog, "utf8")).toBe(""); + writeFileSync(fixture.ghLog, ""); const digestMismatch = verifyUploadedArtifact(fixture, { artifactDigest: "e".repeat(64),