mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(ci): verify bundled plugin runtime deps
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
63
scripts/test-built-bundled-runtime-deps.mjs
Normal file
63
scripts/test-built-bundled-runtime-deps.mjs
Normal file
@@ -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`,
|
||||
);
|
||||
@@ -1 +1 @@
|
||||
bb856be91cddd7131e54cf05acaeb4de745c82017bacc4f5c0d182702d2f1326
|
||||
e8d410067136069ba072e3b325e62c31cd0421499aea202823b4b99cbbc961d8
|
||||
|
||||
65
test/scripts/bundled-plugin-staged-runtime-deps.test.ts
Normal file
65
test/scripts/bundled-plugin-staged-runtime-deps.test.ts
Normal file
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user