mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
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/<id>/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.
This commit is contained in:
@@ -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,
|
||||
[
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
): 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user