diff --git a/.github/actions/setup-release-harness/action.yml b/.github/actions/setup-release-harness/action.yml new file mode 100644 index 000000000000..874b7d60d9c0 --- /dev/null +++ b/.github/actions/setup-release-harness/action.yml @@ -0,0 +1,23 @@ +name: Setup trusted release harness +description: Prepare the trusted release harness package manager and dependencies. +inputs: + node-version: + description: Node.js version required by the release harness. + required: true +runs: + using: composite + steps: + - name: Setup trusted release harness package manager + uses: ./.release-harness/.github/actions/setup-pnpm-store-cache + with: + node-version: ${{ inputs.node-version }} + package-manager-file: .release-harness/package.json + use-actions-cache: "false" + + - name: Install trusted release harness dependencies + shell: bash + working-directory: .release-harness + env: + CI: "true" + # Corepack resolves packageManager from cwd before pnpm processes --dir. + run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index ef0678503013..36313bfcb293 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -1256,6 +1256,12 @@ jobs: fetch-depth: 1 path: .release-harness + - name: Setup trusted release harness + if: contains(matrix.profiles, inputs.release_test_profile) + uses: ./.release-harness/.github/actions/setup-release-harness + with: + node-version: ${{ env.NODE_VERSION }} + - name: Log in to GHCR for shared Docker E2E image if: contains(matrix.profiles, inputs.release_test_profile) && inputs.shared_image_policy != 'no-push-artifact' run: bash .release-harness/scripts/ci-docker-login-ghcr.sh @@ -1632,6 +1638,11 @@ jobs: fetch-depth: 1 path: .release-harness + - name: Setup trusted release harness + uses: ./.release-harness/.github/actions/setup-release-harness + with: + node-version: ${{ env.NODE_VERSION }} + - name: Log in to GHCR for shared Docker E2E image if: inputs.shared_image_policy != 'no-push-artifact' run: bash .release-harness/scripts/ci-docker-login-ghcr.sh @@ -1919,6 +1930,11 @@ jobs: path: .release-harness persist-credentials: false + - name: Setup trusted release harness + uses: ./.release-harness/.github/actions/setup-release-harness + with: + node-version: ${{ env.NODE_VERSION }} + - name: Log in to GHCR for shared Docker E2E image if: inputs.shared_image_policy != 'no-push-artifact' run: bash .release-harness/scripts/ci-docker-login-ghcr.sh @@ -2141,6 +2157,11 @@ jobs: path: .release-harness persist-credentials: false + - name: Setup trusted release harness + uses: ./.release-harness/.github/actions/setup-release-harness + with: + node-version: ${{ env.NODE_VERSION }} + - name: Plan Docker E2E images id: plan shell: bash @@ -2187,24 +2208,6 @@ jobs: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - - name: Setup artifact package validation environment - if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_id != '' - uses: ./.release-harness/.github/actions/setup-pnpm-store-cache - with: - node-version: ${{ env.NODE_VERSION }} - package-manager-file: .release-harness/package.json - use-actions-cache: "false" - - - name: Install trusted package validation dependencies - if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_id != '' - env: - CI: "true" - # Corepack resolves packageManager from the process cwd before pnpm can - # process --dir. Keep this in the trusted harness so a frozen candidate - # pin cannot select a different pnpm binary for its harness install. - working-directory: .release-harness - run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts - - name: Validate OpenClaw package artifact identity id: input_package_artifact if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_id != '' diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 20886bf4a434..090b886bb182 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -31,6 +31,7 @@ const MANTIS_TELEGRAM_LIVE_WORKFLOW = ".github/workflows/mantis-telegram-live.ym const MANTIS_WEB_UI_CHAT_PROOF_WORKFLOW = ".github/workflows/mantis-web-ui-chat-proof.yml"; const PACKAGE_JSON = "package.json"; const SETUP_PNPM_STORE_CACHE_ACTION = ".github/actions/setup-pnpm-store-cache/action.yml"; +const SETUP_RELEASE_HARNESS_ACTION = ".github/actions/setup-release-harness/action.yml"; const RELEASE_CHECKS_WORKFLOW = ".github/workflows/openclaw-release-checks.yml"; const RELEASE_TELEGRAM_QA_WORKFLOW = ".github/workflows/openclaw-release-telegram-qa.yml"; const RELEASE_PUBLISH_WORKFLOW = ".github/workflows/openclaw-release-publish.yml"; @@ -118,6 +119,7 @@ type WorkflowStep = { shell?: string; uses?: string; with?: Record; + "working-directory"?: string; }; type WorkflowMatrixEntry = { @@ -1350,6 +1352,26 @@ describe("package acceptance workflow", () => { expect(setupPnpmAction).toContain('corepack enable --install-directory "$PNPM_HOME"'); expect(setupPnpmAction).toContain('echo "PNPM_HOME=$PNPM_HOME" >> "$GITHUB_ENV"'); + const setupReleaseHarnessAction = readFileSync(SETUP_RELEASE_HARNESS_ACTION, "utf8"); + const setupHarnessPackageManagerIndex = setupReleaseHarnessAction.indexOf( + "Setup trusted release harness package manager", + ); + const installHarnessDependenciesIndex = setupReleaseHarnessAction.indexOf( + "Install trusted release harness dependencies", + ); + expect(setupHarnessPackageManagerIndex).toBeGreaterThan(-1); + expect(installHarnessDependenciesIndex).toBeGreaterThan(setupHarnessPackageManagerIndex); + expect(setupReleaseHarnessAction).toContain( + "uses: ./.release-harness/.github/actions/setup-pnpm-store-cache", + ); + expect(setupReleaseHarnessAction).toContain( + "package-manager-file: .release-harness/package.json", + ); + expect(setupReleaseHarnessAction).toContain("working-directory: .release-harness"); + expect(setupReleaseHarnessAction).toContain( + "pnpm install --frozen-lockfile --prefer-offline --ignore-scripts", + ); + const setupNodeAction = readFileSync(".github/actions/setup-node-env/action.yml", "utf8"); expect(setupNodeAction).toContain("Normalize container toolcache"); expect(setupNodeAction).toContain("ln -s /__t /opt/hostedtoolcache"); @@ -2408,6 +2430,7 @@ describe("package artifact reuse", () => { it("lets reusable Docker E2E consume an already resolved package artifact", () => { const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); const parsedWorkflow = parse(workflow) as { + jobs?: Record; on?: { workflow_call?: { inputs?: Record } }; }; const packageJson = readFileSync(PACKAGE_JSON, "utf8"); @@ -2495,8 +2518,80 @@ describe("package artifact reuse", () => { ?.prepublish_plugin_registry_artifact_id, ).toContain("inputs.enable_prepublish_plugin_registry"); expect(workflow).toContain("bash .release-harness/scripts/ci-docker-pull-retry.sh"); + const setupHarnessStepName = "Setup trusted release harness"; + const harnessJobCases = [ + { + jobId: "validate_docker_e2e", + planStepName: "Plan Docker E2E chunk", + setupIf: "contains(matrix.profiles, inputs.release_test_profile)", + }, + { + jobId: "validate_docker_lanes", + planStepName: "Plan targeted Docker E2E lanes", + setupIf: undefined, + }, + { + jobId: "validate_docker_openwebui", + planStepName: "Plan Open WebUI Docker E2E chunk", + setupIf: undefined, + }, + { + jobId: "prepare_docker_e2e_image", + planStepName: "Plan Docker E2E images", + setupIf: undefined, + }, + ] as const; + const typedHarnessJobIds = Object.entries(parsedWorkflow.jobs ?? {}) + .filter(([, job]) => + (job.steps ?? []).some( + (step) => + step.run?.includes("node .release-harness/scripts/test-docker-all.mjs") || + step.run?.includes("node .release-harness/scripts/docker-e2e.mjs"), + ), + ) + .map(([jobId]) => jobId) + .sort(); + expect(typedHarnessJobIds).toEqual(harnessJobCases.map(({ jobId }) => jobId).sort()); + + for (const { jobId, planStepName, setupIf } of harnessJobCases) { + const harnessJob = workflowJob(LIVE_E2E_WORKFLOW, jobId); + const harnessJobSteps = harnessJob.steps ?? []; + const harnessJobStepNames = harnessJobSteps.map((step) => step.name); + const checkoutIndex = harnessJobStepNames.indexOf("Checkout trusted release harness"); + const setupIndex = harnessJobStepNames.indexOf(setupHarnessStepName); + const typedHarnessIndex = harnessJobSteps.findIndex( + (step) => + step.run?.includes("node .release-harness/scripts/test-docker-all.mjs") || + step.run?.includes("node .release-harness/scripts/docker-e2e.mjs"), + ); + expect(harnessJobStepNames.filter((name) => name === setupHarnessStepName)).toHaveLength(1); + expect(checkoutIndex).toBeGreaterThan(-1); + expect(setupIndex).toBeGreaterThan(checkoutIndex); + expect(typedHarnessIndex).toBeGreaterThan(setupIndex); + expect(workflowStep(harnessJob, planStepName)).toBeDefined(); + const setupHarnessStep = workflowStep(harnessJob, setupHarnessStepName); + expect(setupHarnessStep).toMatchObject({ + uses: "./.release-harness/.github/actions/setup-release-harness", + with: { "node-version": "${{ env.NODE_VERSION }}" }, + }); + expect(setupHarnessStep.if).toBe(setupIf); + } + const prepareDockerImage = workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"); - expect(workflowStep(prepareDockerImage, "Plan Docker E2E images").env).toEqual({ + const prepareDockerImageStepNames = (prepareDockerImage.steps ?? []).map((step) => step.name); + const planStepName = "Plan Docker E2E images"; + const setupCandidateStepName = "Setup Node environment"; + expect( + prepareDockerImageStepNames.filter((name) => name === setupCandidateStepName), + ).toHaveLength(1); + expect(prepareDockerImageStepNames.indexOf(setupCandidateStepName)).toBeGreaterThan( + prepareDockerImageStepNames.indexOf(planStepName), + ); + expect(workflowStep(prepareDockerImage, setupCandidateStepName)).toMatchObject({ + if: "(steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == '' && inputs.package_artifact_run_id == '') || (inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id == '')", + uses: "./.github/actions/setup-node-env", + }); + expect(workflowStep(prepareDockerImage, planStepName).env).toEqual({ INCLUDE_OPENWEBUI: "${{ inputs.include_openwebui }}", INCLUDE_RELEASE_PATH_SUITES: "${{ inputs.include_release_path_suites }}", LANES: "${{ inputs.docker_lanes }}", diff --git a/test/scripts/release-no-push-workflow.test.ts b/test/scripts/release-no-push-workflow.test.ts index c0fc518d188b..1d1459187576 100644 --- a/test/scripts/release-no-push-workflow.test.ts +++ b/test/scripts/release-no-push-workflow.test.ts @@ -693,19 +693,11 @@ describe("release validation no-push transport", () => { ); expect(packDockerArtifact.run).toContain("archive_sha256="); const validatePackage = step(dockerProducer, "Validate OpenClaw Docker E2E package"); - expect(step(dockerProducer, "Setup artifact package validation environment")).toMatchObject({ - if: "steps.plan.outputs.needs_package == '1' && inputs.package_artifact_id != ''", - uses: "./.release-harness/.github/actions/setup-pnpm-store-cache", - with: { - "package-manager-file": ".release-harness/package.json", - "use-actions-cache": "false", - }, - }); - expect(step(dockerProducer, "Install trusted package validation dependencies")).toMatchObject({ - if: "steps.plan.outputs.needs_package == '1' && inputs.package_artifact_id != ''", - "working-directory": ".release-harness", - run: "pnpm install --frozen-lockfile --prefer-offline --ignore-scripts", + expect(step(dockerProducer, "Setup trusted release harness")).toMatchObject({ + uses: "./.release-harness/.github/actions/setup-release-harness", + with: { "node-version": "${{ env.NODE_VERSION }}" }, }); + expect(step(dockerProducer, "Setup trusted release harness").if).toBeUndefined(); expect(validatePackage.env).toMatchObject({ EXPECTED_PACKAGE_FILE_NAME: "${{ inputs.package_file_name }}", EXPECTED_PACKAGE_SHA256: "${{ inputs.package_sha256 }}",