diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index ad378515bce5..915bbc20cfc0 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -1918,17 +1918,17 @@ jobs: fi if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then - OPENCLAW_NPM_EXPECTED_WORKFLOW_REF="${openclaw_npm_expected_workflow_ref}" \ - OPENCLAW_NPM_EXPECTED_WORKFLOW_SHA="${openclaw_npm_expected_workflow_sha}" \ - node --import tsx \ - "${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-postpublish-verify.ts" \ - "${release_version}" - verify_args+=(--skip-postpublish) + verify_args+=( + --postpublish-verifier + "${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-postpublish-verify.ts" + ) fi - node --import tsx \ - "${GITHUB_WORKSPACE}/.release-harness/scripts/release-verify-beta.ts" \ - "${verify_args[@]}" + OPENCLAW_NPM_EXPECTED_WORKFLOW_REF="${openclaw_npm_expected_workflow_ref}" \ + OPENCLAW_NPM_EXPECTED_WORKFLOW_SHA="${openclaw_npm_expected_workflow_sha}" \ + node --import tsx \ + "${GITHUB_WORKSPACE}/.release-harness/scripts/release-verify-beta.ts" \ + "${verify_args[@]}" # Resolve already validated this exact v3 run before mutation. Append # its immutable tuple here so frozen targets need no new CLI option. diff --git a/scripts/lib/release-beta-verifier.ts b/scripts/lib/release-beta-verifier.ts index 71499219c3fd..21d5cfcc5ff6 100644 --- a/scripts/lib/release-beta-verifier.ts +++ b/scripts/lib/release-beta-verifier.ts @@ -26,6 +26,7 @@ type ReleaseVerifyBetaArgs = { pluginSelection: string[]; clawHubBootstrapPlugins: string[]; evidenceOut?: string; + postpublishVerifier?: string; skipPostpublish: boolean; skipGitHubRelease: boolean; skipClawHub: boolean; @@ -242,6 +243,7 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg pluginSelection: [], clawHubBootstrapPlugins: [], evidenceOut: undefined, + postpublishVerifier: undefined, skipPostpublish: false, skipGitHubRelease: false, skipClawHub: false, @@ -300,6 +302,9 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg case "--evidence-out": parsed.evidenceOut = next(); break; + case "--postpublish-verifier": + parsed.postpublishVerifier = next(); + break; case "--full-release-validation-run": parsed.workflowRuns.fullReleaseValidation = next(); break; @@ -335,6 +340,10 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg } } + if (parsed.skipPostpublish && parsed.postpublishVerifier !== undefined) { + throw new Error("--postpublish-verifier cannot be combined with --skip-postpublish."); + } + if (parsed.workflowRuns.pluginClawHubBootstrap !== undefined) { if (parsed.releaseSha === undefined) { throw new Error("--plugin-clawhub-bootstrap-run requires --release-sha."); @@ -349,6 +358,17 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg return parsed; } +export function resolveOpenClawNpmPostpublishVerifier(rootDir: string, override?: string): string { + if (override === undefined) { + return resolve(rootDir, "scripts/openclaw-npm-postpublish-verify.ts"); + } + const verifier = resolve(override); + if (verifier !== resolve(TRUSTED_TOOLING_ROOT, "scripts/openclaw-npm-postpublish-verify.ts")) { + throw new Error("--postpublish-verifier must select the trusted tooling verifier."); + } + return verifier; +} + async function fetchWithRetry( url: string, options: RequestInit, @@ -1274,12 +1294,11 @@ export async function verifyBetaRelease( lines.push(`openclaw npm OK: ${args.version} (${args.distTag})`); if (!args.skipPostpublish) { - runCommandInherited("node", [ - "--import", - "tsx", - "scripts/openclaw-npm-postpublish-verify.ts", - args.version, - ]); + const postpublishVerifier = resolveOpenClawNpmPostpublishVerifier( + rootDir, + args.postpublishVerifier, + ); + runCommandInherited("node", ["--import", "tsx", postpublishVerifier, args.version]); lines.push("openclaw postpublish verifier OK"); } diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index f9fd93bd9d9f..1b1a81a93940 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -4028,7 +4028,10 @@ describe("package artifact reuse", () => { expect(releaseWorkflow).toContain( '"${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-postpublish-verify.ts"', ); - expect(releaseWorkflow).toContain("verify_args+=(--skip-postpublish)"); + expect(releaseWorkflow).toContain( + '"${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-postpublish-verify.ts"', + ); + expect(releaseWorkflow).toContain("--postpublish-verifier"); expect(releaseWorkflow).toContain('"${verify_args[@]}"'); expect(releaseWorkflow).toContain( "OpenClaw Release Publish must use trusted main workflow tooling", diff --git a/test/scripts/release-beta-verifier.test.ts b/test/scripts/release-beta-verifier.test.ts index 6cc4890defd2..968c33dd4ce3 100644 --- a/test/scripts/release-beta-verifier.test.ts +++ b/test/scripts/release-beta-verifier.test.ts @@ -1,6 +1,7 @@ // Release Beta Verifier tests cover release beta verifier script behavior. /* oxlint-disable typescript/no-base-to-string -- fetch mock normalizes standard RequestInfo inputs for URL assertions. */ import { createHash } from "node:crypto"; +import { resolve } from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, describe, expect, it, vi } from "vitest"; import { @@ -10,6 +11,7 @@ import { parseNpmViewFields, parseReleaseVerifyBetaArgs, readBoundedJsonResponse, + resolveOpenClawNpmPostpublishVerifier, runNpmViewWithRetry, validateClawHubBootstrapEvidence, } from "../../scripts/lib/release-beta-verifier.ts"; @@ -91,6 +93,7 @@ describe("parseReleaseVerifyBetaArgs", () => { pluginSelection: [], clawHubBootstrapPlugins: [], evidenceOut: undefined, + postpublishVerifier: undefined, skipPostpublish: false, skipGitHubRelease: false, skipClawHub: false, @@ -128,7 +131,8 @@ describe("parseReleaseVerifyBetaArgs", () => { "44", "--evidence-out", ".artifacts/release-evidence.json", - "--skip-postpublish", + "--postpublish-verifier", + "/tmp/trusted-postpublish.ts", "--skip-github-release", "--skip-clawhub", "--rerun-failed-clawhub", @@ -145,7 +149,8 @@ describe("parseReleaseVerifyBetaArgs", () => { pluginSelection: ["@openclaw/plugin-a", "@openclaw/plugin-b"], clawHubBootstrapPlugins: ["@openclaw/plugin-b"], evidenceOut: ".artifacts/release-evidence.json", - skipPostpublish: true, + postpublishVerifier: "/tmp/trusted-postpublish.ts", + skipPostpublish: false, skipGitHubRelease: true, skipClawHub: true, rerunFailedClawHub: true, @@ -160,6 +165,27 @@ describe("parseReleaseVerifyBetaArgs", () => { }); }); + it("only accepts the trusted tooling postpublish verifier override", () => { + expect(resolveOpenClawNpmPostpublishVerifier("/tmp/release")).toBe( + "/tmp/release/scripts/openclaw-npm-postpublish-verify.ts", + ); + const trustedVerifier = resolve("scripts/openclaw-npm-postpublish-verify.ts"); + expect(resolveOpenClawNpmPostpublishVerifier("/tmp/release", trustedVerifier)).toBe( + trustedVerifier, + ); + expect(() => + resolveOpenClawNpmPostpublishVerifier("/tmp/release", "/tmp/untrusted-verifier.ts"), + ).toThrow("must select the trusted tooling verifier"); + expect(() => + parseReleaseVerifyBetaArgs([ + "2026.5.10-beta.3", + "--postpublish-verifier", + trustedVerifier, + "--skip-postpublish", + ]), + ).toThrow("cannot be combined"); + }); + it("requires exact target and package inputs for bootstrap run verification", () => { expect(() => parseReleaseVerifyBetaArgs(["2026.5.10-beta.3", "--plugin-clawhub-bootstrap-run", "34"]),