From 26e4eb8e40b17657667f8346e94b0a3ab9acbc8a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 07:27:52 +0100 Subject: [PATCH] fix(update): ignore plugin install stages in dist verify --- CHANGELOG.md | 1 + scripts/postinstall-bundled-plugins.mjs | 8 ++--- src/infra/package-dist-inventory.ts | 1 + src/infra/update-global.test.ts | 28 +++++++++++++++++ .../postinstall-bundled-plugins.test.ts | 30 +++++++++++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 170fd937d8f..f86ba621ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Update: ignore bundled plugin `.openclaw-install-stage` directories during global install verification and packaged dist pruning so leftover runtime-dep staging files do not turn successful updates into `unexpected packaged dist file` failures. Fixes #71752. Thanks @waynegault. - Gateway/plugins: stop persisted WhatsApp auth state from activating bundled channel runtime-dependency repair during startup when `channels.whatsapp` is absent, avoiding npm/git stalls on packaged Linux installs. Fixes #71994. Thanks @xiao398008. - CLI/model runs: keep `openclaw infer model run` on explicit OpenRouter models from loading the full provider catalog or inheriting chat-agent silent-reply policy, restoring non-empty one-shot probe output. Fixes #68791. Thanks @limpredator. - Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io. diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index 553fe9cfbe9..5e5573ae571 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -187,8 +187,8 @@ function assertSafeInstalledDistPath(relativePath, params) { return candidatePath; } -function isStagedRuntimeNodeModulesPath(relativePath) { - return /^dist\/extensions\/[^/]+\/node_modules(?:\/|$)/u.test( +function isStagedRuntimeDependencyPath(relativePath) { + return /^dist\/extensions\/[^/]+\/(?:node_modules|\.openclaw-install-stage(?:-[^/]+)?)(?:\/|$)/u.test( normalizeRelativePath(relativePath), ); } @@ -208,7 +208,7 @@ function listInstalledDistFiles(params = {}) { continue; } const relativeCurrentDir = normalizeRelativePath(relative(packageRoot, currentDir)); - if (isStagedRuntimeNodeModulesPath(relativeCurrentDir)) { + if (isStagedRuntimeDependencyPath(relativeCurrentDir)) { continue; } for (const entry of readDir(currentDir, { withFileTypes: true })) { @@ -247,7 +247,7 @@ function pruneEmptyDistDirectories(params = {}) { function prune(currentDir) { const relativeCurrentDir = normalizeRelativePath(relative(packageRoot, currentDir)); - if (isStagedRuntimeNodeModulesPath(relativeCurrentDir)) { + if (isStagedRuntimeDependencyPath(relativeCurrentDir)) { return; } for (const entry of readDir(currentDir, { withFileTypes: true })) { diff --git a/src/infra/package-dist-inventory.ts b/src/infra/package-dist-inventory.ts index 6c384fb1ea2..3a4f30b316c 100644 --- a/src/infra/package-dist-inventory.ts +++ b/src/infra/package-dist-inventory.ts @@ -26,6 +26,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-install-stage(?:-[^/]+)?(?:\/|$)/u, /^dist\/extensions\/[^/]+\/\.openclaw-runtime-deps-[^/]+(?:\/|$)/u, /^dist\/extensions\/qa-matrix(?:\/|$)/u, new RegExp(`^dist/plugin-sdk/extensions/${LEGACY_QA_LAB_DIR}(?:/|$)`, "u"), diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts index 436a0c9bdbe..869a0943c18 100644 --- a/src/infra/update-global.test.ts +++ b/src/infra/update-global.test.ts @@ -425,6 +425,34 @@ describe("update global helpers", () => { }); }); + it("ignores bundled plugin install stages during installed dist verification", async () => { + await withTempDir({ prefix: "openclaw-update-global-plugin-stage-" }, async (packageRoot) => { + await writeGlobalPackageJson(packageRoot); + await writeCompatSidecars(packageRoot); + await fs.mkdir(path.join(packageRoot, "dist", "extensions", "brave"), { recursive: true }); + await writePackageDistInventory(packageRoot); + + for (const stageDir of [".openclaw-install-stage", ".openclaw-install-stage-retry"]) { + const stagedFile = path.join( + packageRoot, + "dist", + "extensions", + "brave", + stageDir, + "node_modules", + "typebox", + "build", + "compile", + "code.mjs", + ); + await fs.mkdir(path.dirname(stagedFile), { recursive: true }); + await fs.writeFile(stagedFile, "export {};\n", "utf8"); + } + + await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]); + }); + }); + it("does not require private QA sidecars when the inventory is missing", async () => { await withTempDir({ prefix: "openclaw-update-global-legacy-" }, async (packageRoot) => { await writeGlobalPackageJson(packageRoot); diff --git a/test/scripts/postinstall-bundled-plugins.test.ts b/test/scripts/postinstall-bundled-plugins.test.ts index 63a9b5152da..90d25a0185f 100644 --- a/test/scripts/postinstall-bundled-plugins.test.ts +++ b/test/scripts/postinstall-bundled-plugins.test.ts @@ -550,11 +550,39 @@ describe("bundled plugin postinstall", () => { const staleFile = path.join(packageRoot, "dist", "stale-runtime.js"); const packageJson = path.join(packageRoot, "dist", "extensions", "slack", "package.json"); const binDir = path.join(packageRoot, "dist", "extensions", "slack", "node_modules", ".bin"); + const installStageFile = path.join( + packageRoot, + "dist", + "extensions", + "slack", + ".openclaw-install-stage", + "node_modules", + "typebox", + "build", + "compile", + "code.mjs", + ); + const retryInstallStageFile = path.join( + packageRoot, + "dist", + "extensions", + "slack", + ".openclaw-install-stage-retry", + "node_modules", + "typebox", + "build", + "compile", + "code.mjs", + ); await fs.mkdir(path.dirname(staleFile), { recursive: true }); await fs.mkdir(path.dirname(packageJson), { recursive: true }); await fs.mkdir(binDir, { recursive: true }); + await fs.mkdir(path.dirname(installStageFile), { recursive: true }); + await fs.mkdir(path.dirname(retryInstallStageFile), { recursive: true }); await fs.writeFile(staleFile, "export {};\n"); await fs.writeFile(packageJson, "{}\n"); + await fs.writeFile(installStageFile, "export {};\n"); + await fs.writeFile(retryInstallStageFile, "export {};\n"); await fs.symlink("../fxparser/bin.js", path.join(binDir, "fxparser")); expect( @@ -564,6 +592,8 @@ describe("bundled plugin postinstall", () => { log: { log: vi.fn(), warn: vi.fn() }, }), ).toEqual(["dist/stale-runtime.js"]); + await expect(fs.stat(installStageFile)).resolves.toBeDefined(); + await expect(fs.stat(retryInstallStageFile)).resolves.toBeDefined(); }); it("unlinks stale files instead of recursive pruning them", () => {