From ced61db64a07fca90473ad8dc14d4733ff1d8fe3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 17:28:45 -0700 Subject: [PATCH] fix(ci): preserve OCM workspace prepack (#107844) * fix(ci): preserve OCM workspace prepack * fix(ci): bind OCM prepack to pack context * fix(ci): avoid path import shadowing --- scripts/ocm-npm-workspace-deps.d.mts | 2 +- scripts/openclaw-prepack.ts | 35 ++++++++++++++++++++++++++++ test/openclaw-prepack.test.ts | 28 ++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/scripts/ocm-npm-workspace-deps.d.mts b/scripts/ocm-npm-workspace-deps.d.mts index 8621e1727d7a..4d2ac60cb668 100644 --- a/scripts/ocm-npm-workspace-deps.d.mts +++ b/scripts/ocm-npm-workspace-deps.d.mts @@ -18,7 +18,7 @@ export function buildInstallManifest( openclaw: string; }; }; -export function resolveNpmEnvironment(args: unknown, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; +export function resolveNpmEnvironment(args: string[], env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; export function resolveRuntimePackPlan( args: string[], env?: NodeJS.ProcessEnv, diff --git a/scripts/openclaw-prepack.ts b/scripts/openclaw-prepack.ts index 04e3ded11915..dd97e795f50e 100644 --- a/scripts/openclaw-prepack.ts +++ b/scripts/openclaw-prepack.ts @@ -3,6 +3,7 @@ import { spawnSync, type SpawnSyncOptions } from "node:child_process"; import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { basename, delimiter, join } from "node:path"; import { pathToFileURL } from "node:url"; import { formatErrorMessage } from "../src/infra/errors.ts"; import { writePackageDistInventory } from "./lib/package-dist-inventory.ts"; @@ -18,6 +19,10 @@ const requiredControlUiCompressionSuffixes = [".br", ".gz"] as const; const DEFAULT_PREPACK_COMMAND_TIMEOUT_MS = 30 * 60 * 1000; const ALLOW_UNRELEASED_CHANGELOG_ENV = "OPENCLAW_PREPACK_ALLOW_UNRELEASED_CHANGELOG"; const PREPARED_RELEASE_ENV = "OPENCLAW_PREPACK_PREPARED"; +const OCM_INTERNAL_NPM_BIN_ENV = "OCM_INTERNAL_NPM_BIN"; +const OCM_WORKSPACE_DIRS_ENV = "OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS"; +const OCM_ADAPTER_BASENAME = "ocm-npm-workspace-deps.mjs"; +const NPM_COMMAND_ENV = "npm_command"; const SELF_CONTAINED_SOURCE_PACK_COMMAND = "node scripts/package-openclaw-for-docker.mjs --allow-unreleased-changelog"; @@ -28,8 +33,35 @@ type PreparedFileReader = { type PackageManifest = { dependencies?: Record; + name?: unknown; }; +function ocmExternalizesWorkspacePackage(packageName: string, env: NodeJS.ProcessEnv): boolean { + if (env[NPM_COMMAND_ENV] !== "pack") { + return false; + } + const adapterPath = env[OCM_INTERNAL_NPM_BIN_ENV]?.trim(); + if (!adapterPath || basename(adapterPath) !== OCM_ADAPTER_BASENAME) { + return false; + } + const workspaceDirs = (env[OCM_WORKSPACE_DIRS_ENV] ?? "") + .split(delimiter) + .map((entry) => entry.trim()) + .filter(Boolean); + // OCM uses these same manifests to pack and install dependencies beside the root archive. + // Require the exact package here so unrelated ambient paths cannot bypass the plain-pack guard. + return workspaceDirs.some((workspaceDir) => { + try { + const manifest = JSON.parse( + readFileSync(join(workspaceDir, "package.json"), "utf8"), + ) as PackageManifest; + return manifest.name === packageName; + } catch { + return false; + } + }); +} + function normalizeFiles(files: Iterable): Set { return new Set(Array.from(files, (file) => file.replace(/\\/g, "/"))); } @@ -45,6 +77,9 @@ export function collectSourcePackWorkspaceDependencyErrors( if (typeof aiDependency !== "string" || !aiDependency.trim().startsWith("workspace:")) { return []; } + if (ocmExternalizesWorkspacePackage("@openclaw/ai", env)) { + return []; + } return [ `plain root packing cannot safely resolve @openclaw/ai from ${aiDependency}: pnpm rewrites the workspace dependency to an exact version without bundling the package`, `use \`${SELF_CONTAINED_SOURCE_PACK_COMMAND}\` for a self-contained source package; official npm release automation prepares and publishes @openclaw/ai separately`, diff --git a/test/openclaw-prepack.test.ts b/test/openclaw-prepack.test.ts index e947af314ca8..5bad9b3d8522 100644 --- a/test/openclaw-prepack.test.ts +++ b/test/openclaw-prepack.test.ts @@ -92,6 +92,34 @@ describe("collectSourcePackWorkspaceDependencyErrors", () => { OPENCLAW_PREPACK_PREPARED: "1", }), ).toEqual([]); + expect( + collectSourcePackWorkspaceDependencyErrors(rootPackageJson, { + npm_command: "pack", + OCM_INTERNAL_NPM_BIN: path.join(rootDir, "scripts", "ocm-npm-workspace-deps.mjs"), + OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS: aiDir, + }), + ).toEqual([]); + expect( + collectSourcePackWorkspaceDependencyErrors(rootPackageJson, { + npm_command: "pack", + OCM_INTERNAL_NPM_BIN: path.join(rootDir, "scripts", "ocm-npm-workspace-deps.mjs"), + OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS: rootDir, + }), + ).toHaveLength(2); + expect( + collectSourcePackWorkspaceDependencyErrors(rootPackageJson, { + npm_command: "pack", + OCM_INTERNAL_NPM_BIN: path.join(rootDir, "scripts", "other-npm-wrapper.mjs"), + OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS: aiDir, + }), + ).toHaveLength(2); + expect( + collectSourcePackWorkspaceDependencyErrors(rootPackageJson, { + npm_command: "publish", + OCM_INTERNAL_NPM_BIN: path.join(rootDir, "scripts", "ocm-npm-workspace-deps.mjs"), + OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS: aiDir, + }), + ).toHaveLength(2); }); });