From d4eb23652362a1b7d3fbcebd633a1c6f2a43c16f Mon Sep 17 00:00:00 2001 From: Dewaldt Huysamen Date: Wed, 22 Apr 2026 09:31:40 +0200 Subject: [PATCH] fix(release-check): assert bundled plugin runtime deps after packed postinstall (#70035) * fix(release-check): assert bundled plugin runtime deps after packed postinstall Release-check already validates source dist/extensions runtime deps are staged, but runPackedBundledChannelEntrySmoke never re-validates after the packed postinstall runs against the installed tarball. That gap is how 2026.4.21 shipped without @whiskeysockets/baileys in dist/extensions/whatsapp/node_modules, because the source staging passed while the installed layout was left broken. Re-use collectBuiltBundledPluginStagedRuntimeDependencyErrors against the installed packageRoot right after runPackedBundledPluginPostinstall and fail release-check if any declared runtime dependency is missing from the plugin-local node_modules. * fix(release-check): check postinstalled dep sentinels at packageRoot/node_modules Codex review on #70035 caught that collectInstalledBundledPluginRuntimeDepErrors was pointing at dist/extensions//node_modules, but packed postinstall installs and probes sentinels at packageRoot/node_modules (see dependencySentinelPath in scripts/postinstall-bundled-plugins.mjs). The previous implementation would have falsely failed release-check on healthy packed installs while still missing the original WhatsApp regression. Reuse discoverBundledPluginRuntimeDeps from postinstall-bundled-plugins.mjs so the release guard uses the exact same dep discovery and sentinel paths the packed postinstall uses. Update the test fixtures accordingly so they model the real install layout. --- scripts/release-check.ts | 33 ++++++++++++++++++- test/release-check.test.ts | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 208fe749b25..d785708f317 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -1,7 +1,7 @@ #!/usr/bin/env -S node --import tsx import { execFileSync, execSync } from "node:child_process"; -import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; @@ -27,6 +27,7 @@ import { runInstalledWorkspaceBootstrapSmoke, WORKSPACE_TEMPLATE_PACK_PATHS, } from "./lib/workspace-bootstrap-smoke.mjs"; +import { discoverBundledPluginRuntimeDeps } from "./postinstall-bundled-plugins.mjs"; import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs"; import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts"; @@ -222,6 +223,35 @@ function runPackedBundledPluginPostinstall(packageRoot: string): void { }); } +export function collectInstalledBundledPluginRuntimeDepErrors(packageRoot: string): string[] { + const extensionsDir = join(packageRoot, "dist", "extensions"); + if (!existsSync(extensionsDir)) { + return []; + } + const runtimeDeps = discoverBundledPluginRuntimeDeps({ extensionsDir }); + return runtimeDeps + .filter((dep) => !existsSync(join(packageRoot, dep.sentinelPath))) + .map((dep) => { + const owners = dep.pluginIds.length > 0 ? dep.pluginIds.join(", ") : "unknown"; + return `bundled plugin runtime dependency '${dep.name}@${dep.version}' (owners: ${owners}) is missing at ${dep.sentinelPath}.`; + }) + .toSorted((left, right) => left.localeCompare(right)); +} + +function assertInstalledBundledPluginRuntimeDepsResolved(packageRoot: string): void { + const errors = collectInstalledBundledPluginRuntimeDepErrors(packageRoot); + if (errors.length === 0) { + return; + } + console.error("release-check: packed install is missing bundled plugin runtime dependencies:"); + for (const error of errors) { + console.error(` - ${error}`); + } + throw new Error( + "release-check: bundled plugin runtime dependencies were not installed after packed postinstall.", + ); +} + function runPackedBundledChannelEntrySmoke(): void { const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-")); try { @@ -235,6 +265,7 @@ function runPackedBundledChannelEntrySmoke(): void { const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw"); runPackedBundledPluginPostinstall(packageRoot); + assertInstalledBundledPluginRuntimeDepsResolved(packageRoot); execFileSync( process.execPath, [ diff --git a/test/release-check.test.ts b/test/release-check.test.ts index 0429dbed494..459efb5467c 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -10,6 +10,7 @@ import { collectBundledExtensionManifestErrors, collectBundledPluginRootRuntimeMirrorErrors, collectForbiddenPackContentPaths, + collectInstalledBundledPluginRuntimeDepErrors, collectRootDistBundledRuntimeMirrors, collectForbiddenPackPaths, collectMissingPackPaths, @@ -474,3 +475,67 @@ describe("createPackedBundledPluginPostinstallEnv", () => { }); }); }); + +describe("collectInstalledBundledPluginRuntimeDepErrors", () => { + function createPackageRoot(): string { + const packageRoot = mkdtempSync(join(tmpdir(), "release-check-installed-bundled-")); + mkdirSync(join(packageRoot, "dist", "extensions"), { recursive: true }); + return packageRoot; + } + + function writeBundledPluginPackageJson( + packageRoot: string, + pluginId: string, + packageJson: Record, + ): void { + const pluginRoot = join(packageRoot, "dist", "extensions", pluginId); + mkdirSync(pluginRoot, { recursive: true }); + writeFileSync(join(pluginRoot, "package.json"), JSON.stringify(packageJson, null, 2)); + } + + function installRuntimeDependencyAtPackageRoot( + packageRoot: string, + dependencyName: string, + version: string, + ): void { + const dependencyRoot = join(packageRoot, "node_modules", ...dependencyName.split("/")); + mkdirSync(dependencyRoot, { recursive: true }); + writeFileSync( + join(dependencyRoot, "package.json"), + JSON.stringify({ name: dependencyName, version }, null, 2), + ); + } + + it("returns no errors when declared deps are installed at the openclaw package root", () => { + const packageRoot = createPackageRoot(); + try { + writeBundledPluginPackageJson(packageRoot, "whatsapp", { + name: "@openclaw/whatsapp", + dependencies: { "@whiskeysockets/baileys": "7.0.0-rc.9" }, + openclaw: { bundle: { stageRuntimeDependencies: true } }, + }); + installRuntimeDependencyAtPackageRoot(packageRoot, "@whiskeysockets/baileys", "7.0.0-rc.9"); + + expect(collectInstalledBundledPluginRuntimeDepErrors(packageRoot)).toEqual([]); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); + + it("surfaces an error naming the owning plugin and missing dependency", () => { + const packageRoot = createPackageRoot(); + try { + writeBundledPluginPackageJson(packageRoot, "whatsapp", { + name: "@openclaw/whatsapp", + dependencies: { "@whiskeysockets/baileys": "7.0.0-rc.9" }, + openclaw: { bundle: { stageRuntimeDependencies: true } }, + }); + + expect(collectInstalledBundledPluginRuntimeDepErrors(packageRoot)).toEqual([ + "bundled plugin runtime dependency '@whiskeysockets/baileys@7.0.0-rc.9' (owners: whatsapp) is missing at node_modules/@whiskeysockets/baileys/package.json.", + ]); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); +});