From c6b9be2646f1d784f40d29fe650124d177a207d9 Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Mon, 10 Aug 2026 16:16:33 -0700 Subject: [PATCH] fix(release): run plugin tooling against source checkout (#121762) --- .github/workflows/plugin-npm-release.yml | 74 +++--------------- scripts/generate-npm-package-lock.mts | 5 +- scripts/plugin-npm-publish.sh | 52 ++++++++++--- ...lugin-npm-extended-stable-workflow.test.ts | 77 ++++++------------- test/scripts/plugin-npm-publish.test.ts | 20 ++++- 5 files changed, 97 insertions(+), 131 deletions(-) diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index dc85a9f30642..fa1548c8ff24 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -333,43 +333,14 @@ jobs: ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }} fetch-depth: 1 - - name: Checkout trusted packaging helper + - name: Checkout trusted packaging tooling uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false ref: ${{ github.workflow_sha }} path: .release-tooling fetch-depth: 1 - sparse-checkout: | - scripts/generate-npm-package-lock.mjs - scripts/generate-npm-package-lock.mts - scripts/lib/npm-json-output.mts - scripts/lib/plugin-npm-package-manifest.mjs - scripts/lib/plugin-npm-package-manifest.mts - scripts/lib/tsx-cli-shim.mjs - sparse-checkout-cone-mode: false - - - name: Overlay trusted packaging helper - run: | - set -euo pipefail - cp \ - .release-tooling/scripts/generate-npm-package-lock.mjs \ - scripts/generate-npm-package-lock.mjs - cp \ - .release-tooling/scripts/generate-npm-package-lock.mts \ - scripts/generate-npm-package-lock.mts - cp \ - .release-tooling/scripts/lib/npm-json-output.mts \ - scripts/lib/npm-json-output.mts - cp \ - .release-tooling/scripts/lib/plugin-npm-package-manifest.mjs \ - scripts/lib/plugin-npm-package-manifest.mjs - cp \ - .release-tooling/scripts/lib/plugin-npm-package-manifest.mts \ - scripts/lib/plugin-npm-package-manifest.mts - cp \ - .release-tooling/scripts/lib/tsx-cli-shim.mjs \ - scripts/lib/tsx-cli-shim.mjs + sparse-checkout: scripts - name: Setup Node environment uses: ./.github/actions/setup-node-env @@ -380,12 +351,12 @@ jobs: - name: Preview publish command env: OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }} - run: bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}" + run: bash .release-tooling/scripts/plugin-npm-publish.sh --repo-root "$GITHUB_WORKSPACE" --dry-run "${{ matrix.plugin.packageDir }}" - name: Preview npm pack contents env: OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }} - run: bash scripts/plugin-npm-publish.sh --pack-dry-run "${{ matrix.plugin.packageDir }}" + run: bash .release-tooling/scripts/plugin-npm-publish.sh --repo-root "$GITHUB_WORKSPACE" --pack-dry-run "${{ matrix.plugin.packageDir }}" - name: Prepare immutable npm preflight artifact id: preflight_artifact @@ -416,11 +387,13 @@ jobs: OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD=0 \ OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR="${artifact_dir}" \ - bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}" > "${pack_output}" + bash .release-tooling/scripts/plugin-npm-publish.sh \ + --repo-root "$GITHUB_WORKSPACE" \ + --pack "${PACKAGE_DIR}" > "${pack_output}" node --import tsx --input-type=module - "${pack_output}" "${pack_json}" <<'NODE' import fs from "node:fs"; - import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mts"; + import { resolveNpmJsonEntries } from "./.release-tooling/scripts/lib/npm-json-output.mts"; const raw = fs.readFileSync(process.argv[2], "utf8").trim(); let pack; @@ -1252,33 +1225,7 @@ jobs: path: .publication-target fetch-depth: 1 - - name: Overlay trusted OIDC packaging helper - if: steps.publication_evidence.outputs.publish_route == 'npm-oidc' - run: | - set -euo pipefail - cp \ - scripts/generate-npm-package-lock.mjs \ - .publication-target/scripts/generate-npm-package-lock.mjs - cp \ - scripts/generate-npm-package-lock.mts \ - .publication-target/scripts/generate-npm-package-lock.mts - cp \ - scripts/lib/npm-json-output.mts \ - .publication-target/scripts/lib/npm-json-output.mts - cp \ - scripts/lib/plugin-npm-package-manifest.mjs \ - .publication-target/scripts/lib/plugin-npm-package-manifest.mjs - cp \ - scripts/lib/plugin-npm-package-manifest.mts \ - .publication-target/scripts/lib/plugin-npm-package-manifest.mts - cp \ - scripts/lib/tsx-cli-shim.mjs \ - .publication-target/scripts/lib/tsx-cli-shim.mjs - cp \ - scripts/lib/tsx-cli-shim.mjs \ - .publication-target/scripts/lib/tsx-cli-shim.mjs - - - name: Setup OIDC publication target + - name: Setup trusted OIDC packaging dependencies if: steps.publication_evidence.outputs.publish_route == 'npm-oidc' uses: ./.github/actions/setup-node-env with: @@ -1302,11 +1249,10 @@ jobs: - name: Publish with trusted publisher if: steps.publication_evidence.outputs.publish_route == 'npm-oidc' && steps.npm_package_version.outputs.already_published != 'true' - working-directory: .publication-target env: OPENCLAW_NPM_PUBLISH_AUTH_MODE: trusted-publisher OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }} - run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}" + run: bash scripts/plugin-npm-publish.sh --repo-root .publication-target --publish "${{ matrix.plugin.packageDir }}" - name: Verify OIDC published runtime if: steps.publication_evidence.outputs.publish_route == 'npm-oidc' diff --git a/scripts/generate-npm-package-lock.mts b/scripts/generate-npm-package-lock.mts index 19388af1ec9a..fdc0ee3ae1c5 100644 --- a/scripts/generate-npm-package-lock.mts +++ b/scripts/generate-npm-package-lock.mts @@ -34,7 +34,10 @@ type NpmLockOptions = { installStrategy?: "hoisted" | "nested" | "shallow" | "linked" | "" | null; }; -const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const SCRIPT_ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const ROOT_DIR = path.resolve( + process.env.OPENCLAW_NPM_PACKAGE_LOCK_REPO_ROOT?.trim() || SCRIPT_ROOT_DIR, +); const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u; const STABLE_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/u; const NPM_LOCK_COMMAND_TIMEOUT_MS = 10 * 60 * 1000; diff --git a/scripts/plugin-npm-publish.sh b/scripts/plugin-npm-publish.sh index 2dea40daed19..7d4c0c65fec9 100644 --- a/scripts/plugin-npm-publish.sh +++ b/scripts/plugin-npm-publish.sh @@ -3,7 +3,7 @@ set -euo pipefail usage() { - echo "usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack|--pack-dry-run|--publish] " + echo "usage: bash scripts/plugin-npm-publish.sh [--repo-root ] [--dry-run|--pack|--pack-dry-run|--publish] " } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then @@ -11,6 +11,20 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then exit 0 fi +tooling_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="${tooling_root}" +if [[ "${1:-}" == "--repo-root" ]]; then + if [[ -z "${2:-}" || "${2:-}" == -* ]]; then + echo "--repo-root requires a directory" >&2 + exit 2 + fi + repo_root="$(cd "$2" && pwd)" || { + echo "repository root is not a directory: $2" >&2 + exit 2 + } + shift 2 +fi + mode="${1:-}" if [[ "${mode}" != "--dry-run" && "${mode}" != "--pack" && "${mode}" != "--pack-dry-run" && "${mode}" != "--publish" ]]; then usage >&2 @@ -25,7 +39,8 @@ package_dir="" if [[ "$#" -gt 0 ]]; then case "$1" in -*) echo "unexpected plugin npm package-dir option: $1" >&2; exit 2 ;; - *) package_dir="$1"; shift ;; + /*) package_dir="$1"; shift ;; + *) package_dir="${repo_root}/$1"; shift ;; esac fi if [[ -z "${package_dir}" ]]; then @@ -41,6 +56,7 @@ if [[ "${mode}" == "--pack" && -z "${OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR:-}" ]]; exit 2 fi +cd "${tooling_root}" package_name="$(node -e 'const pkg = require(require("node:path").resolve(process.argv[1], "package.json")); console.log(pkg.name)' "${package_dir}")" package_version="$(node -e 'const pkg = require(require("node:path").resolve(process.argv[1], "package.json")); console.log(pkg.version)' "${package_dir}")" current_beta_version="$(npm view "${package_name}" dist-tags.beta 2>/dev/null || true)" @@ -93,6 +109,7 @@ if [[ "${OPENCLAW_NPM_PUBLISH_PROVENANCE:-1}" != "0" && "${OPENCLAW_NPM_PUBLISH_ fi log "Resolved package dir: ${package_dir}" +log "Resolved repository root: ${repo_root}" log "Resolved package name: ${package_name}" log "Resolved package version: ${package_version}" log "Current beta dist-tag: ${current_beta_version:-}" @@ -108,12 +125,20 @@ build_package_runtime() { return fi log "Package-local runtime build: ${package_dir}" - node scripts/lib/plugin-npm-runtime-build.mjs "${package_dir}" >&2 + ( + cd "${repo_root}" + node "${tooling_root}/scripts/lib/plugin-npm-runtime-build.mjs" "${package_dir}" >&2 + ) } check_package_npm_lock() { log "Package-local npm package-lock check: ${package_dir}" - node scripts/generate-npm-package-lock.mjs --package-dir "${package_dir}" >&2 + ( + cd "${repo_root}" + OPENCLAW_NPM_PACKAGE_LOCK_REPO_ROOT="${repo_root}" \ + node "${tooling_root}/scripts/generate-npm-package-lock.mjs" \ + --package-dir "${package_dir}" >&2 + ) } mirror_auth_token="" @@ -175,9 +200,13 @@ if [[ "${mode}" == "--pack" || "${mode}" == "--pack-dry-run" ]]; then pack_output_dir="$(cd "${OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR}" && pwd)" pack_args+=(--pack-destination "${pack_output_dir}") fi - OPENCLAW_PLUGIN_NPM_BUNDLE_DEPENDENCIES=1 \ - node scripts/lib/plugin-npm-package-manifest.mjs --run "${package_dir}" -- \ - "${pack_args[@]}" + ( + cd "${repo_root}" + OPENCLAW_NPM_PACKAGE_LOCK_REPO_ROOT="${repo_root}" \ + OPENCLAW_PLUGIN_NPM_BUNDLE_DEPENDENCIES=1 \ + node "${tooling_root}/scripts/lib/plugin-npm-package-manifest.mjs" \ + --run "${package_dir}" -- "${pack_args[@]}" + ) exit 0 fi @@ -185,8 +214,13 @@ fi cleanup_files=() trap 'rm -f "${cleanup_files[@]}"' EXIT run_with_manifest_overlay() { - OPENCLAW_PLUGIN_NPM_BUNDLE_DEPENDENCIES=1 \ - node scripts/lib/plugin-npm-package-manifest.mjs --run "${package_dir}" -- "$@" + ( + cd "${repo_root}" + OPENCLAW_NPM_PACKAGE_LOCK_REPO_ROOT="${repo_root}" \ + OPENCLAW_PLUGIN_NPM_BUNDLE_DEPENDENCIES=1 \ + node "${tooling_root}/scripts/lib/plugin-npm-package-manifest.mjs" \ + --run "${package_dir}" -- "$@" + ) } publish_userconfig="" if [[ -n "${publish_auth_token}" ]]; then diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index fd9b1b9db884..b14dbb609c0a 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -105,50 +105,28 @@ describe("plugin npm extended-stable workflow", () => { } }); - it("overlays the complete trusted packaging helper dependency set", () => { + it("runs complete trusted packaging tooling against the frozen source checkout", () => { const parsed = workflow(); - const lockGenerator = readFileSync("scripts/generate-npm-package-lock.mts", "utf8"); - expect(lockGenerator).toContain( - 'path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")', - ); - expect(lockGenerator).not.toContain("./lib/repo-root.mjs"); const preflightCheckout = step( parsed.jobs?.preview_plugin_pack, - "Checkout trusted packaging helper", + "Checkout trusted packaging tooling", ); - expect(preflightCheckout.with?.["sparse-checkout"]).toContain( - "scripts/generate-npm-package-lock.mjs", - ); - expect(preflightCheckout.with?.["sparse-checkout"]).toContain( - "scripts/generate-npm-package-lock.mts", - ); - expect(preflightCheckout.with?.["sparse-checkout"]).toContain( - "scripts/lib/npm-json-output.mts", - ); - expect(preflightCheckout.with?.["sparse-checkout"]).toContain( - "scripts/lib/plugin-npm-package-manifest.mjs", - ); - expect(preflightCheckout.with?.["sparse-checkout"]).toContain( - "scripts/lib/plugin-npm-package-manifest.mts", - ); - expect(preflightCheckout.with?.["sparse-checkout"]).toContain("scripts/lib/tsx-cli-shim.mjs"); + expect(preflightCheckout.with).toMatchObject({ + ref: "${{ github.workflow_sha }}", + path: ".release-tooling", + "sparse-checkout": "scripts", + }); + const previewCommand = step(parsed.jobs?.preview_plugin_pack, "Preview publish command").run; + expect(previewCommand).toContain(".release-tooling/scripts/plugin-npm-publish.sh"); + expect(previewCommand).toContain('--repo-root "$GITHUB_WORKSPACE"'); + expect( + step(parsed.jobs?.preview_plugin_pack, "Prepare immutable npm preflight artifact").run, + ).toContain(".release-tooling/scripts/plugin-npm-publish.sh"); - const expectedCopies = [ - "scripts/generate-npm-package-lock.mjs", - "scripts/generate-npm-package-lock.mts", - "scripts/lib/npm-json-output.mts", - "scripts/lib/plugin-npm-package-manifest.mjs", - "scripts/lib/plugin-npm-package-manifest.mts", - "scripts/lib/tsx-cli-shim.mjs", - ]; - for (const helperPath of expectedCopies) { - expect( - step(parsed.jobs?.preview_plugin_pack, "Overlay trusted packaging helper").run, - ).toContain(helperPath); - expect( - step(parsed.jobs?.publish_plugins_npm, "Overlay trusted OIDC packaging helper").run, - ).toContain(helperPath); - } + const publish = step(parsed.jobs?.publish_plugins_npm, "Publish with trusted publisher"); + expect(publish.run).toContain("scripts/plugin-npm-publish.sh"); + expect(publish.run).toContain("--repo-root .publication-target"); + expect(publish["working-directory"]).toBeUndefined(); }); it("trusts only the canonical monthly branch at the exact checked-out SHA", () => { @@ -220,9 +198,11 @@ describe("plugin npm extended-stable workflow", () => { "plugin-npm-package-source-${{ needs.preview_plugins_npm.outputs.ref_revision }}-${{ matrix.plugin.extensionId }}", ); expect(prepare.if).toBeUndefined(); - expect(prepare.run).toContain('bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}"'); + expect(prepare.run).toContain("bash .release-tooling/scripts/plugin-npm-publish.sh"); + expect(prepare.run).toContain('--repo-root "$GITHUB_WORKSPACE"'); + expect(prepare.run).toContain('--pack "${PACKAGE_DIR}"'); expect(prepare.run).toContain( - 'import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mts";', + 'import { resolveNpmJsonEntries } from "./.release-tooling/scripts/lib/npm-json-output.mts";', ); expect(prepare.run).toContain('raw[index] !== "[" && raw[index] !== "{"'); expect(prepare.run).toContain("const entries = resolveNpmJsonEntries(candidate)"); @@ -449,14 +429,11 @@ describe("plugin npm extended-stable workflow", () => { step(parsed.jobs?.publish_plugins_npm, "Checkout trusted publication tooling").with?.ref, ).toBe("${{ github.workflow_sha }}"); expect( - step(parsed.jobs?.preview_plugin_pack, "Checkout trusted packaging helper").with, + step(parsed.jobs?.preview_plugin_pack, "Checkout trusted packaging tooling").with, ).toMatchObject({ ref: "${{ github.workflow_sha }}", path: ".release-tooling", }); - expect( - step(parsed.jobs?.preview_plugin_pack, "Overlay trusted packaging helper").run, - ).toContain(".release-tooling/scripts/lib/plugin-npm-package-manifest.mjs"); expect( step(parsed.jobs?.publish_plugins_npm, "Setup trusted publication dependencies").if, ).toContain("npm-token-bootstrap"); @@ -470,14 +447,8 @@ describe("plugin npm extended-stable workflow", () => { step(parsed.jobs?.publish_plugins_npm, "Checkout OIDC publication target").with?.path, ).toBe(".publication-target"); expect( - step(parsed.jobs?.publish_plugins_npm, "Overlay trusted OIDC packaging helper").run, - ).toContain(".publication-target/scripts/lib/plugin-npm-package-manifest.mjs"); - expect(step(parsed.jobs?.publish_plugins_npm, "Setup OIDC publication target").uses).toBe( - "./.github/actions/setup-node-env", - ); - expect( - step(parsed.jobs?.publish_plugins_npm, "Publish with trusted publisher")["working-directory"], - ).toBe(".publication-target"); + step(parsed.jobs?.publish_plugins_npm, "Setup trusted OIDC packaging dependencies").uses, + ).toBe("./.github/actions/setup-node-env"); expect(parsed.jobs?.reconcile_plugins_npm).toBeUndefined(); expect(readFileSync(workflowPath, "utf8")).not.toContain( 'npm dist-tag add "${PACKAGE_NAME}@${PACKAGE_VERSION}" extended-stable', diff --git a/test/scripts/plugin-npm-publish.test.ts b/test/scripts/plugin-npm-publish.test.ts index 7ffce68e5bad..de2f3656ab6a 100644 --- a/test/scripts/plugin-npm-publish.test.ts +++ b/test/scripts/plugin-npm-publish.test.ts @@ -23,7 +23,7 @@ function runPluginPublishWrapper(args: string[], env: NodeJS.ProcessEnv = {}) { }); } -function makePackage(version: string): { packageDir: string; path: string } { +function makePackage(version: string): { packageDir: string; path: string; root: string } { const root = mkdtempSync(join(tmpdir(), "openclaw-plugin-publish-test-")); tempDirs.push(root); const packageDir = join(root, "plugin"); @@ -37,7 +37,7 @@ function makePackage(version: string): { packageDir: string; path: string } { const npmPath = join(binDir, "npm"); writeFileSync(npmPath, "#!/bin/sh\nexit 1\n"); chmodSync(npmPath, 0o755); - return { packageDir, path: `${binDir}${delimiter}${process.env.PATH ?? ""}` }; + return { packageDir, path: `${binDir}${delimiter}${process.env.PATH ?? ""}`, root }; } describe("plugin npm publish wrapper", () => { @@ -46,7 +46,7 @@ describe("plugin npm publish wrapper", () => { expect(result.status).toBe(0); expect(result.stdout.trim()).toBe( - "usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack|--pack-dry-run|--publish] ", + "usage: bash scripts/plugin-npm-publish.sh [--repo-root ] [--dry-run|--pack|--pack-dry-run|--publish] ", ); expect(result.stderr).toBe(""); }); @@ -57,10 +57,22 @@ describe("plugin npm publish wrapper", () => { expect(result.status).toBe(2); expect(result.stdout).toBe(""); expect(result.stderr.trim()).toBe( - "usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack|--pack-dry-run|--publish] ", + "usage: bash scripts/plugin-npm-publish.sh [--repo-root ] [--dry-run|--pack|--pack-dry-run|--publish] ", ); }); + it("runs trusted tooling against an explicit repository root", () => { + const fixture = makePackage("2026.8.1-beta.1"); + const result = runPluginPublishWrapper(["--repo-root", fixture.root, "--dry-run", "plugin"], { + PATH: fixture.path, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain(`Resolved repository root: ${fixture.root}`); + expect(result.stdout).toContain(`Resolved package dir: ${fixture.packageDir}`); + expect(result.stdout).toContain("Resolved package name: @openclaw/demo"); + }); + it("requires an explicit artifact directory for real pack mode", () => { const result = runPluginPublishWrapper(["--pack", "extensions/telegram"]);