diff --git a/package.json b/package.json index c74e18b4745..90f95dff9ad 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "dist/", "!dist/**/*.map", "!dist/plugin-sdk/.tsbuildinfo", + "!dist/extensions/*/.openclaw-runtime-deps-*/**", + "!dist/extensions/*/.openclaw-runtime-deps-stamp.json", "!dist/extensions/node_modules/**", "!dist/extensions/*/node_modules/**", "!dist/extensions/qa-channel/**", diff --git a/scripts/release-check.ts b/scripts/release-check.ts index f54b65cb859..c505229bd74 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -457,6 +457,8 @@ export function collectForbiddenPackPaths(paths: Iterable): string[] { .filter( (path) => forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) || + /(^|\/)\.openclaw-runtime-deps-[^/]+(\/|$)/u.test(path) || + path.endsWith("/.openclaw-runtime-deps-stamp.json") || path.includes("node_modules/"), ) .toSorted((left, right) => left.localeCompare(right)); diff --git a/scripts/stage-bundled-plugin-runtime-deps.mjs b/scripts/stage-bundled-plugin-runtime-deps.mjs index e272cac5906..bcdd274879b 100644 --- a/scripts/stage-bundled-plugin-runtime-deps.mjs +++ b/scripts/stage-bundled-plugin-runtime-deps.mjs @@ -852,10 +852,19 @@ function runNpmInstall(params) { throw new Error(output || "npm install failed"); } -function resolveRuntimeDepsStampPath(pluginDir) { +function resolveLegacyRuntimeDepsStampPath(pluginDir) { return path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"); } +function resolveRuntimeDepsStampPath(repoRoot, pluginId) { + return path.join( + repoRoot, + ".artifacts", + "bundled-runtime-deps-stamps", + `${sanitizeTempPrefixSegment(pluginId)}.json`, + ); +} + function createRuntimeDepsFingerprint(packageJson, pruneConfig, params = {}) { const repoRoot = params.repoRoot; const lockfilePath = @@ -892,6 +901,17 @@ function readRuntimeDepsStamp(stampPath) { } } +function removeStaleRuntimeDepsTempDirs(pluginDir) { + if (!fs.existsSync(pluginDir)) { + return; + } + for (const entry of fs.readdirSync(pluginDir, { withFileTypes: true })) { + if (entry.name.startsWith(".openclaw-runtime-deps-")) { + removePathIfExists(path.join(pluginDir, entry.name)); + } + } +} + function stageInstalledRootRuntimeDeps(params) { const { directDependencyPackageRoot = null, @@ -900,6 +920,7 @@ function stageInstalledRootRuntimeDeps(params) { pluginDir, pruneConfig, repoRoot, + stampPath, } = params; const dependencySpecs = { ...packageJson.dependencies, @@ -931,7 +952,6 @@ function stageInstalledRootRuntimeDeps(params) { } const rootsToCopy = selectRuntimeDependencyRootsToCopy(resolution); const nodeModulesDir = path.join(pluginDir, "node_modules"); - const stampPath = resolveRuntimeDepsStampPath(pluginDir); if (rootsToCopy.length === 0) { assertPathIsNotSymlink(nodeModulesDir, "remove runtime deps"); removePathIfExists(nodeModulesDir); @@ -1030,9 +1050,9 @@ function installPluginRuntimeDeps(params) { pluginId, pruneConfig, repoRoot, + stampPath, } = params; const nodeModulesDir = path.join(pluginDir, "node_modules"); - const stampPath = resolveRuntimeDepsStampPath(pluginDir); const tempInstallDir = makePluginOwnedTempDir(pluginDir, "install"); const pinnedGroups = resolvePinnedRuntimeDependencyGroups(packageJson, { directDependencyPackageRoot, @@ -1088,7 +1108,10 @@ export function stageBundledPluginRuntimeDeps(params = {}) { : null; const packageJson = sanitizeBundledManifestForRuntimeInstall(pluginDir); const nodeModulesDir = path.join(pluginDir, "node_modules"); - const stampPath = resolveRuntimeDepsStampPath(pluginDir); + const stampPath = resolveRuntimeDepsStampPath(repoRoot, pluginId); + const legacyStampPath = resolveLegacyRuntimeDepsStampPath(pluginDir); + removePathIfExists(legacyStampPath); + removeStaleRuntimeDepsTempDirs(pluginDir); if (!hasRuntimeDeps(packageJson) || !shouldStageRuntimeDeps(packageJson)) { removePathIfExists(nodeModulesDir); removePathIfExists(stampPath); @@ -1115,6 +1138,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) { pluginDir, pruneConfig, repoRoot, + stampPath, }) ) { continue; @@ -1131,6 +1155,7 @@ export function stageBundledPluginRuntimeDeps(params = {}) { pluginId, pruneConfig, repoRoot, + stampPath, }, }); } catch (error) { diff --git a/src/infra/package-dist-inventory.test.ts b/src/infra/package-dist-inventory.test.ts index 874f4edfca5..00c811ea812 100644 --- a/src/infra/package-dist-inventory.test.ts +++ b/src/infra/package-dist-inventory.test.ts @@ -70,6 +70,22 @@ describe("package dist inventory", () => { "cli.d.ts", ); const omittedQaRuntimeChunk = path.join(packageRoot, "dist", "qa-runtime-B9LDtssJ.js"); + const omittedRuntimeDepsStamp = path.join( + packageRoot, + "dist", + "extensions", + "discord", + ".openclaw-runtime-deps-stamp.json", + ); + const omittedRuntimeDepsTempFile = path.join( + packageRoot, + "dist", + "extensions", + "discord", + ".openclaw-runtime-deps-backup-node_modules-old", + "left-pad", + "index.js", + ); const omittedExtensionNodeModuleSymlink = path.join( packageRoot, "dist", @@ -92,6 +108,8 @@ describe("package dist inventory", () => { await fs.mkdir(path.dirname(packagedQaLabRuntime), { recursive: true }); await fs.mkdir(path.dirname(omittedQaMatrixChunk), { recursive: true }); await fs.mkdir(path.dirname(omittedQaLabTypes), { recursive: true }); + await fs.mkdir(path.dirname(omittedRuntimeDepsStamp), { recursive: true }); + await fs.mkdir(path.dirname(omittedRuntimeDepsTempFile), { recursive: true }); await fs.mkdir(path.dirname(omittedExtensionNodeModuleSymlink), { recursive: true }); await fs.mkdir(path.dirname(omittedExtensionRootAliasSymlink), { recursive: true }); await fs.mkdir(path.join(packageRoot, "dist", "plugin-sdk"), { recursive: true }); @@ -104,6 +122,8 @@ describe("package dist inventory", () => { await fs.writeFile(omittedQaLabPluginSdk, "export {};\n", "utf8"); await fs.writeFile(omittedQaLabTypes, "export {};\n", "utf8"); await fs.writeFile(omittedQaRuntimeChunk, "export {};\n", "utf8"); + await fs.writeFile(omittedRuntimeDepsStamp, "{}\n", "utf8"); + await fs.writeFile(omittedRuntimeDepsTempFile, "module.exports = 1;\n", "utf8"); await fs.symlink( path.join(packageRoot, "color-support.js"), omittedExtensionNodeModuleSymlink, diff --git a/src/infra/package-dist-inventory.ts b/src/infra/package-dist-inventory.ts index 943a68c7b89..723c11a0e83 100644 --- a/src/infra/package-dist-inventory.ts +++ b/src/infra/package-dist-inventory.ts @@ -21,6 +21,7 @@ const OMITTED_PRIVATE_QA_DIST_PREFIXES = ["dist/qa-runtime-"]; const OMITTED_DIST_SUBTREE_PATTERNS = [ /^dist\/extensions\/node_modules(?:\/|$)/u, /^dist\/extensions\/[^/]+\/node_modules(?:\/|$)/u, + /^dist\/extensions\/[^/]+\/\.openclaw-runtime-deps-[^/]+(?:\/|$)/u, /^dist\/extensions\/qa-matrix(?:\/|$)/u, new RegExp(`^dist/plugin-sdk/extensions/${LEGACY_QA_LAB_DIR}(?:/|$)`, "u"), ] as const; @@ -36,6 +37,9 @@ function isPackagedDistPath(relativePath: string): boolean { if (relativePath === PACKAGE_DIST_INVENTORY_RELATIVE_PATH) { return false; } + if (relativePath.endsWith("/.openclaw-runtime-deps-stamp.json")) { + return false; + } if (relativePath.endsWith(".map")) { return false; } diff --git a/test/release-check.test.ts b/test/release-check.test.ts index 91c6d8f19a6..c0331a38251 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -322,6 +322,19 @@ describe("collectForbiddenPackPaths", () => { ]); }); + it("blocks legacy runtime dependency stamps from npm pack output", () => { + expect( + collectForbiddenPackPaths([ + "dist/index.js", + "dist/extensions/codex/.openclaw-runtime-deps-backup-node_modules-old/zod/index.js", + "dist/extensions/discord/.openclaw-runtime-deps-stamp.json", + ]), + ).toEqual([ + "dist/extensions/codex/.openclaw-runtime-deps-backup-node_modules-old/zod/index.js", + "dist/extensions/discord/.openclaw-runtime-deps-stamp.json", + ]); + }); + it("blocks private qa channel, qa lab, and suite paths from npm pack output", () => { expect( collectForbiddenPackPaths([ diff --git a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts index d439ab5a75a..dc1273bc3f7 100644 --- a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts +++ b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts @@ -10,6 +10,11 @@ import { createScriptTestHarness } from "./test-helpers.js"; const { createTempDir } = createScriptTestHarness(); +type RuntimeDepsStampParams = { + fingerprint: string; + stampPath: string; +}; + describe("stageBundledPluginRuntimeDeps", () => { function createBundledPluginFixture(params: { packageJson: Record; @@ -27,6 +32,15 @@ describe("stageBundledPluginRuntimeDeps", () => { return { pluginDir, repoRoot }; } + function writeRuntimeDepsStamp(stampPath: string, fingerprint: string) { + fs.mkdirSync(path.dirname(stampPath), { recursive: true }); + fs.writeFileSync(stampPath, `${JSON.stringify({ fingerprint }, null, 2)}\n`, "utf8"); + } + + function runtimeDepsStampPath(repoRoot: string, pluginId = "fixture-plugin") { + return path.join(repoRoot, ".artifacts", "bundled-runtime-deps-stamps", `${pluginId}.json`); + } + it("pins fallback install specs to exact installed versions", () => { const { repoRoot } = createBundledPluginFixture({ packageJson: { @@ -142,13 +156,9 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); stageBundledPluginRuntimeDeps({ @@ -182,16 +192,12 @@ describe("stageBundledPluginRuntimeDeps", () => { const stageOnce = () => stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules"); fs.mkdirSync(nodeModulesDir, { recursive: true }); fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), `${installCount}\n`, "utf8"); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -226,16 +232,12 @@ describe("stageBundledPluginRuntimeDeps", () => { const stageOnce = () => stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules"); fs.mkdirSync(nodeModulesDir, { recursive: true }); fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), `${installCount}\n`, "utf8"); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -310,7 +312,7 @@ describe("stageBundledPluginRuntimeDeps", () => { }); it("refuses to write a runtime deps stamp through a symlink", () => { - const { pluginDir, repoRoot } = createBundledPluginFixture({ + const { repoRoot } = createBundledPluginFixture({ packageJson: { name: "@openclaw/fixture-plugin", version: "1.0.0", @@ -320,8 +322,9 @@ describe("stageBundledPluginRuntimeDeps", () => { }); const directDir = path.join(repoRoot, "node_modules", "direct"); const outsideStamp = path.join(repoRoot, "outside-stamp.json"); - const stampPath = path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"); + const stampPath = runtimeDepsStampPath(repoRoot); fs.mkdirSync(directDir, { recursive: true }); + fs.mkdirSync(path.dirname(stampPath), { recursive: true }); fs.writeFileSync( path.join(directDir, "package.json"), '{ "name": "direct", "version": "1.0.0" }\n', @@ -359,7 +362,33 @@ describe("stageBundledPluginRuntimeDeps", () => { expect( fs.readFileSync(path.join(pluginDir, "node_modules", "left-pad", "index.js"), "utf8"), ).toBe("module.exports = 1;\n"); - expect(fs.existsSync(path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"))).toBe(true); + expect(fs.existsSync(path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"))).toBe(false); + expect(fs.existsSync(runtimeDepsStampPath(repoRoot))).toBe(true); + }); + + it("removes legacy runtime dependency stamps from dist", () => { + const { pluginDir, repoRoot } = createBundledPluginFixture({ + packageJson: { + name: "@openclaw/fixture-plugin", + version: "1.0.0", + dependencies: { "left-pad": "1.3.0" }, + openclaw: { bundle: { stageRuntimeDependencies: true } }, + }, + }); + const rootDepDir = path.join(repoRoot, "node_modules", "left-pad"); + const legacyStampPath = path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"); + fs.mkdirSync(rootDepDir, { recursive: true }); + fs.writeFileSync( + path.join(rootDepDir, "package.json"), + '{ "name": "left-pad", "version": "1.3.0" }\n', + "utf8", + ); + fs.writeFileSync(legacyStampPath, '{"legacy":true}\n', "utf8"); + + stageBundledPluginRuntimeDeps({ cwd: repoRoot }); + + expect(fs.existsSync(legacyStampPath)).toBe(false); + expect(fs.existsSync(runtimeDepsStampPath(repoRoot))).toBe(true); }); it("skips missing optional runtime deps when copying the installed closure", () => { @@ -617,16 +646,12 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules"); fs.mkdirSync(nodeModulesDir, { recursive: true }); fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "installed\n", "utf8"); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -698,16 +723,12 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules"); fs.mkdirSync(nodeModulesDir, { recursive: true }); fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "installed\n", "utf8"); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -743,16 +764,12 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules"); fs.mkdirSync(nodeModulesDir, { recursive: true }); fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "installed\n", "utf8"); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -788,7 +805,7 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules", "direct"); fs.mkdirSync(nodeModulesDir, { recursive: true }); @@ -802,11 +819,7 @@ describe("stageBundledPluginRuntimeDeps", () => { "module.exports = 'installed';\n", "utf8", ); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -976,7 +989,7 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules", "left-pad"); fs.mkdirSync(nodeModulesDir, { recursive: true }); @@ -990,11 +1003,7 @@ describe("stageBundledPluginRuntimeDeps", () => { "module.exports = 'nested';\n", "utf8", ); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -1024,7 +1033,7 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules", "tiny"); fs.mkdirSync(nodeModulesDir, { recursive: true }); @@ -1033,11 +1042,7 @@ describe("stageBundledPluginRuntimeDeps", () => { '{ "name": "tiny", "version": "0.0.3" }\n', "utf8", ); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -1064,7 +1069,7 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; const nodeModulesDir = path.join(pluginDir, "node_modules", "direct"); fs.mkdirSync(nodeModulesDir, { recursive: true }); @@ -1073,11 +1078,7 @@ describe("stageBundledPluginRuntimeDeps", () => { '{ "name": "direct", "version": "1.2.3" }\n', "utf8", ); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, }); @@ -1097,7 +1098,7 @@ describe("stageBundledPluginRuntimeDeps", () => { let installCount = 0; stageBundledPluginRuntimeDeps({ cwd: repoRoot, - installPluginRuntimeDepsImpl: ({ fingerprint }: { fingerprint: string }) => { + installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => { installCount += 1; if (installCount < 3) { throw new Error(`attempt ${installCount} failed`); @@ -1105,11 +1106,7 @@ describe("stageBundledPluginRuntimeDeps", () => { const nodeModulesDir = path.join(pluginDir, "node_modules"); fs.mkdirSync(nodeModulesDir, { recursive: true }); fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "ok\n", "utf8"); - fs.writeFileSync( - path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), - `${JSON.stringify({ fingerprint }, null, 2)}\n`, - "utf8", - ); + writeRuntimeDepsStamp(stampPath, fingerprint); }, });