From 21ca387eda33aaa7773c434974035a2a649c4f32 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 13 Apr 2026 10:47:50 +0100 Subject: [PATCH] fix(ci): verify bundled plugin runtime deps --- .github/workflows/ci.yml | 3 + package.json | 1 + .../bundled-plugin-root-runtime-mirrors.mjs | 36 ++++++++++ scripts/release-check.ts | 7 +- scripts/test-built-bundled-runtime-deps.mjs | 63 ++++++++++++++++++ src/canvas-host/a2ui/.bundle.hash | 2 +- ...bundled-plugin-staged-runtime-deps.test.ts | 65 +++++++++++++++++++ 7 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 scripts/test-built-bundled-runtime-deps.mjs create mode 100644 test/scripts/bundled-plugin-staged-runtime-deps.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e5869cad33..58449b1eedb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1008,6 +1008,9 @@ jobs: - name: Smoke test built bundled plugin singleton run: pnpm test:build:singleton + - name: Smoke test built bundled runtime deps + run: pnpm test:build:bundled-runtime-deps + - name: Check CLI startup memory run: pnpm test:startup:memory diff --git a/package.json b/package.json index c9aa5307c5a..a230d530e95 100644 --- a/package.json +++ b/package.json @@ -1248,6 +1248,7 @@ "test": "node scripts/test-projects.mjs", "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all", "test:auth:compat": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts", + "test:build:bundled-runtime-deps": "node scripts/test-built-bundled-runtime-deps.mjs", "test:build:singleton": "node scripts/test-built-plugin-singleton.mjs", "test:bundled": "node scripts/run-vitest.mjs run --config test/vitest/vitest.bundled.config.ts", "test:changed": "node scripts/test-projects.mjs --changed origin/main", diff --git a/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs b/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs index ac7b307f90b..a105e8b1b42 100644 --- a/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs +++ b/scripts/lib/bundled-plugin-root-runtime-mirrors.mjs @@ -45,6 +45,18 @@ function collectPackageJsonPaths(rootDir) { .toSorted((left, right) => left.localeCompare(right)); } +function usesStagedRuntimeDependencies(packageJson) { + return packageJson?.openclaw?.bundle?.stageRuntimeDependencies === true; +} + +function dependencySentinelPath(packageRoot, dependencyName) { + return path.join(packageRoot, "node_modules", ...dependencyName.split("/"), "package.json"); +} + +function pluginIdFromPackageJsonPath(packageJsonPath) { + return path.basename(path.dirname(packageJsonPath)); +} + export function collectBundledPluginRuntimeDependencySpecs(bundledPluginsDir) { const specs = new Map(); @@ -68,6 +80,30 @@ export function collectBundledPluginRuntimeDependencySpecs(bundledPluginsDir) { return specs; } +export function collectBuiltBundledPluginStagedRuntimeDependencyErrors(params) { + const errors = []; + + for (const packageJsonPath of collectPackageJsonPaths(params.bundledPluginsDir)) { + const packageJson = readJson(packageJsonPath); + if (!usesStagedRuntimeDependencies(packageJson)) { + continue; + } + const pluginId = pluginIdFromPackageJsonPath(packageJsonPath); + const pluginRoot = path.dirname(packageJsonPath); + + for (const [dependencyName, spec] of collectRuntimeDependencySpecs(packageJson)) { + if (!fs.existsSync(dependencySentinelPath(pluginRoot, dependencyName))) { + const specText = String(spec); + errors.push( + `built bundled plugin '${pluginId}' is missing staged runtime dependency '${dependencyName}: ${specText}' under dist/extensions/${pluginId}/node_modules.`, + ); + } + } + } + + return errors.toSorted((left, right) => left.localeCompare(right)); +} + function walkJavaScriptFiles(rootDir) { const files = []; if (!fs.existsSync(rootDir)) { diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 6f7d7fcd6ae..6cab2636e3f 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -12,6 +12,7 @@ import { } from "./lib/bundled-extension-manifest.ts"; import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs"; import { + collectBuiltBundledPluginStagedRuntimeDependencyErrors, collectBundledPluginRootRuntimeMirrorErrors, collectBundledPluginRuntimeDependencySpecs, collectRootDistBundledRuntimeMirrors, @@ -22,6 +23,7 @@ import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./s export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts"; export { + collectBuiltBundledPluginStagedRuntimeDependencyErrors, collectBundledPluginRootRuntimeMirrorErrors, collectRootDistBundledRuntimeMirrors, packageNameFromSpecifier, @@ -109,7 +111,10 @@ function checkBundledExtensionMetadata() { requiredRootMirrors, rootPackageJson: rootPackage, }); - const errors = [...manifestErrors, ...rootMirrorErrors]; + const builtArtifactErrors = collectBuiltBundledPluginStagedRuntimeDependencyErrors({ + bundledPluginsDir: resolve("dist/extensions"), + }); + const errors = [...manifestErrors, ...rootMirrorErrors, ...builtArtifactErrors]; if (errors.length > 0) { console.error("release-check: bundled extension manifest validation failed:"); for (const error of errors) { diff --git a/scripts/test-built-bundled-runtime-deps.mjs b/scripts/test-built-bundled-runtime-deps.mjs new file mode 100644 index 00000000000..e8f17e631c4 --- /dev/null +++ b/scripts/test-built-bundled-runtime-deps.mjs @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + collectBuiltBundledPluginStagedRuntimeDependencyErrors, + collectBundledPluginRootRuntimeMirrorErrors, + collectBundledPluginRuntimeDependencySpecs, + collectRootDistBundledRuntimeMirrors, +} from "./lib/bundled-plugin-root-runtime-mirrors.mjs"; + +function parseArgs(argv) { + let packageRoot = process.env.OPENCLAW_BUNDLED_RUNTIME_DEPS_ROOT; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--package-root") { + packageRoot = argv[index + 1]; + index += 1; + continue; + } + if (arg?.startsWith("--package-root=")) { + packageRoot = arg.slice("--package-root=".length); + continue; + } + throw new Error(`unknown argument: ${arg}`); + } + return { + packageRoot: path.resolve( + packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), + ), + }; +} + +const { packageRoot } = parseArgs(process.argv.slice(2)); +const rootPackageJsonPath = path.join(packageRoot, "package.json"); +const builtPluginsDir = path.join(packageRoot, "dist", "extensions"); + +assert.ok(fs.existsSync(rootPackageJsonPath), `package.json missing from ${packageRoot}`); +assert.ok(fs.existsSync(builtPluginsDir), `built bundled plugins missing from ${builtPluginsDir}`); + +const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8")); +const bundledRuntimeDependencySpecs = collectBundledPluginRuntimeDependencySpecs( + path.join(packageRoot, "extensions"), +); +const requiredRootMirrors = collectRootDistBundledRuntimeMirrors({ + bundledRuntimeDependencySpecs, + distDir: path.join(packageRoot, "dist"), +}); +const errors = [ + ...collectBundledPluginRootRuntimeMirrorErrors({ + bundledRuntimeDependencySpecs, + requiredRootMirrors, + rootPackageJson, + }), + ...collectBuiltBundledPluginStagedRuntimeDependencyErrors({ + bundledPluginsDir: builtPluginsDir, + }), +]; + +assert.deepEqual(errors, [], errors.join("\n")); +process.stdout.write( + `[build-smoke] bundled runtime dependency smoke passed packageRoot=${packageRoot}\n`, +); diff --git a/src/canvas-host/a2ui/.bundle.hash b/src/canvas-host/a2ui/.bundle.hash index 9111b7c04e3..b2d78db7154 100644 --- a/src/canvas-host/a2ui/.bundle.hash +++ b/src/canvas-host/a2ui/.bundle.hash @@ -1 +1 @@ -bb856be91cddd7131e54cf05acaeb4de745c82017bacc4f5c0d182702d2f1326 +e8d410067136069ba072e3b325e62c31cd0421499aea202823b4b99cbbc961d8 diff --git a/test/scripts/bundled-plugin-staged-runtime-deps.test.ts b/test/scripts/bundled-plugin-staged-runtime-deps.test.ts new file mode 100644 index 00000000000..e6d72636d8a --- /dev/null +++ b/test/scripts/bundled-plugin-staged-runtime-deps.test.ts @@ -0,0 +1,65 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { collectBuiltBundledPluginStagedRuntimeDependencyErrors } from "../../scripts/lib/bundled-plugin-root-runtime-mirrors.mjs"; +import { createScriptTestHarness } from "./test-helpers.js"; + +const { createTempDir } = createScriptTestHarness(); + +function writeJson(root: string, relativePath: string, value: unknown) { + const fullPath = path.join(root, relativePath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +describe("collectBuiltBundledPluginStagedRuntimeDependencyErrors", () => { + it("flags built staged plugins whose dist node_modules are missing runtime deps", () => { + const repoRoot = createTempDir("openclaw-runtime-contracts-"); + + writeJson(repoRoot, "dist/extensions/diffs/package.json", { + name: "@openclaw/diffs", + dependencies: { + "@pierre/diffs": "^0.1.0", + }, + openclaw: { + bundle: { + stageRuntimeDependencies: true, + }, + }, + }); + + expect( + collectBuiltBundledPluginStagedRuntimeDependencyErrors({ + bundledPluginsDir: path.join(repoRoot, "dist/extensions"), + }), + ).toEqual([ + "built bundled plugin 'diffs' is missing staged runtime dependency '@pierre/diffs: ^0.1.0' under dist/extensions/diffs/node_modules.", + ]); + }); + + it("accepts built staged plugins when their staged runtime deps are present", () => { + const repoRoot = createTempDir("openclaw-runtime-contracts-"); + + writeJson(repoRoot, "dist/extensions/diffs/package.json", { + name: "@openclaw/diffs", + dependencies: { + "@pierre/diffs": "^0.1.0", + }, + openclaw: { + bundle: { + stageRuntimeDependencies: true, + }, + }, + }); + writeJson(repoRoot, "dist/extensions/diffs/node_modules/@pierre/diffs/package.json", { + name: "@pierre/diffs", + version: "0.1.0", + }); + + expect( + collectBuiltBundledPluginStagedRuntimeDependencyErrors({ + bundledPluginsDir: path.join(repoRoot, "dist/extensions"), + }), + ).toEqual([]); + }); +});