mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
fix: harden bundled plugin dependency release checks
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/failover: classify Z.ai vendor code `1311` as billing and `1113` as auth, including long wrapped `1311` payloads, so these errors stop falling through to generic failover handling. (#49552) Thanks @1bcMax.
|
||||
- npm packaging: mirror bundled Slack, Telegram, Discord, and Feishu channel runtime deps at the root and harden published-install verification so fresh installs fail fast on manifest drift instead of missing-module crashes. (#63065) Thanks @scoootscooob.
|
||||
- Agents/timeouts: make the LLM idle timeout inherit `agents.defaults.timeoutSeconds` when configured, disable the unconfigured idle watchdog for cron runs, and point idle-timeout errors at `agents.defaults.llm.idleTimeoutSeconds`. Thanks @drvoss.
|
||||
- npm packaging: derive required root runtime mirrors from bundled plugin manifests and built root chunks, then install packed release tarballs without the repo `node_modules` so release checks catch missing plugin deps before publish.
|
||||
|
||||
## 2026.4.8
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
},
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@aws-sdk/client-bedrock"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,6 @@
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@buape/carbon",
|
||||
"@discordjs/opus",
|
||||
"https-proxy-agent",
|
||||
"opusscript"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
|
||||
@@ -51,11 +51,6 @@
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@larksuiteoapi/node-sdk"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
|
||||
@@ -47,13 +47,6 @@
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.9",
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@matrix-org/matrix-sdk-crypto-wasm",
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs",
|
||||
"matrix-js-sdk"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,6 @@
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@slack/bolt",
|
||||
"@slack/web-api",
|
||||
"https-proxy-agent"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,6 @@
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@grammyjs/runner",
|
||||
"@grammyjs/transformer-throttler",
|
||||
"grammy"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -1311,11 +1311,14 @@
|
||||
"@aws-sdk/client-bedrock": "3.1024.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
|
||||
"@aws-sdk/credential-provider-node": "3.972.29",
|
||||
"@aws/bedrock-token-generator": "^1.1.0",
|
||||
"@buape/carbon": "0.14.0",
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@google/genai": "^1.48.0",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.6",
|
||||
"@lancedb/lancedb": "^0.27.2",
|
||||
"@larksuiteoapi/node-sdk": "^1.60.0",
|
||||
"@line/bot-sdk": "^11.0.0",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
@@ -1339,18 +1342,22 @@
|
||||
"express": "^5.2.1",
|
||||
"file-type": "22.0.0",
|
||||
"gaxios": "7.1.4",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"grammy": "^1.42.0",
|
||||
"hono": "4.12.10",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"ipaddr.js": "^2.3.0",
|
||||
"jimp": "^1.6.0",
|
||||
"jiti": "^2.6.1",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.18.12",
|
||||
"long": "^5.3.2",
|
||||
"markdown-it": "^14.1.1",
|
||||
"markdown-it": "14.1.1",
|
||||
"matrix-js-sdk": "41.3.0-rc.0",
|
||||
"mpg123-decoder": "^1.0.3",
|
||||
"node-edge-tts": "^1.2.10",
|
||||
"openai": "^6.33.0",
|
||||
"opusscript": "^0.1.1",
|
||||
"osc-progress": "^0.3.0",
|
||||
"pdfjs-dist": "^5.6.205",
|
||||
@@ -1358,10 +1365,11 @@
|
||||
"proxy-agent": "^8.0.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"sharp": "^0.34.5",
|
||||
"silk-wasm": "^3.7.1",
|
||||
"sqlite-vec": "0.1.9",
|
||||
"tar": "7.5.13",
|
||||
"tslog": "^4.10.2",
|
||||
"undici": "^8.0.2",
|
||||
"undici": "8.0.2",
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.20.0",
|
||||
"yaml": "^2.8.3",
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -45,12 +45,18 @@ importers:
|
||||
'@aws-sdk/credential-provider-node':
|
||||
specifier: 3.972.29
|
||||
version: 3.972.29
|
||||
'@aws/bedrock-token-generator':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@buape/carbon':
|
||||
specifier: 0.14.0
|
||||
version: 0.14.0(@discordjs/opus@0.10.0)(hono@4.12.10)(opusscript@0.1.1)
|
||||
'@clack/prompts':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
'@google/genai':
|
||||
specifier: ^1.48.0
|
||||
version: 1.48.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))
|
||||
'@grammyjs/runner':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(grammy@1.42.0)
|
||||
@@ -60,6 +66,9 @@ importers:
|
||||
'@homebridge/ciao':
|
||||
specifier: ^1.3.6
|
||||
version: 1.3.6
|
||||
'@lancedb/lancedb':
|
||||
specifier: ^0.27.2
|
||||
version: 0.27.2(apache-arrow@18.1.0)
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.60.0
|
||||
version: 1.60.0
|
||||
@@ -132,6 +141,9 @@ importers:
|
||||
gaxios:
|
||||
specifier: 7.1.4
|
||||
version: 7.1.4
|
||||
google-auth-library:
|
||||
specifier: ^10.6.2
|
||||
version: 10.6.2
|
||||
grammy:
|
||||
specifier: ^1.42.0
|
||||
version: 1.42.0
|
||||
@@ -144,6 +156,9 @@ importers:
|
||||
ipaddr.js:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
jimp:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
jiti:
|
||||
specifier: ^2.6.1
|
||||
version: 2.6.1
|
||||
@@ -160,17 +175,23 @@ importers:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
markdown-it:
|
||||
specifier: ^14.1.1
|
||||
specifier: 14.1.1
|
||||
version: 14.1.1
|
||||
matrix-js-sdk:
|
||||
specifier: 41.3.0-rc.0
|
||||
version: 41.3.0-rc.0
|
||||
mpg123-decoder:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
node-edge-tts:
|
||||
specifier: ^1.2.10
|
||||
version: 1.2.10
|
||||
node-llama-cpp:
|
||||
specifier: 3.18.1
|
||||
version: 3.18.1(typescript@6.0.2)
|
||||
openai:
|
||||
specifier: ^6.33.0
|
||||
version: 6.33.0(ws@8.20.0)(zod@4.3.6)
|
||||
opusscript:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
@@ -192,6 +213,9 @@ importers:
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
silk-wasm:
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
sqlite-vec:
|
||||
specifier: 0.1.9
|
||||
version: 0.1.9
|
||||
@@ -202,7 +226,7 @@ importers:
|
||||
specifier: ^4.10.2
|
||||
version: 4.10.2
|
||||
undici:
|
||||
specifier: ^8.0.2
|
||||
specifier: 8.0.2
|
||||
version: 8.0.2
|
||||
uuid:
|
||||
specifier: ^13.0.0
|
||||
|
||||
177
scripts/lib/bundled-plugin-root-runtime-mirrors.mjs
Normal file
177
scripts/lib/bundled-plugin-root-runtime-mirrors.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const JS_EXTENSIONS = new Set([".cjs", ".js", ".mjs"]);
|
||||
|
||||
export function collectRuntimeDependencySpecs(packageJson = {}) {
|
||||
return new Map(
|
||||
[
|
||||
...Object.entries(packageJson.dependencies ?? {}),
|
||||
...Object.entries(packageJson.optionalDependencies ?? {}),
|
||||
].filter((entry) => typeof entry[1] === "string" && entry[1].length > 0),
|
||||
);
|
||||
}
|
||||
|
||||
export function packageNameFromSpecifier(specifier) {
|
||||
if (
|
||||
typeof specifier !== "string" ||
|
||||
specifier.startsWith(".") ||
|
||||
specifier.startsWith("/") ||
|
||||
specifier.startsWith("node:") ||
|
||||
specifier.startsWith("#")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const [first, second] = specifier.split("/");
|
||||
if (!first) {
|
||||
return null;
|
||||
}
|
||||
return first.startsWith("@") && second ? `${first}/${second}` : first;
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function collectPackageJsonPaths(rootDir) {
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
return fs
|
||||
.readdirSync(rootDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(rootDir, entry.name, "package.json"))
|
||||
.filter((packageJsonPath) => fs.existsSync(packageJsonPath))
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function collectBundledPluginRuntimeDependencySpecs(bundledPluginsDir) {
|
||||
const specs = new Map();
|
||||
|
||||
for (const packageJsonPath of collectPackageJsonPaths(bundledPluginsDir)) {
|
||||
const packageJson = readJson(packageJsonPath);
|
||||
const pluginId = path.basename(path.dirname(packageJsonPath));
|
||||
for (const [name, spec] of collectRuntimeDependencySpecs(packageJson)) {
|
||||
const existing = specs.get(name);
|
||||
if (existing) {
|
||||
if (existing.spec !== spec) {
|
||||
existing.conflicts.push({ pluginId, spec });
|
||||
} else if (!existing.pluginIds.includes(pluginId)) {
|
||||
existing.pluginIds.push(pluginId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
specs.set(name, { conflicts: [], pluginIds: [pluginId], spec });
|
||||
}
|
||||
}
|
||||
|
||||
return specs;
|
||||
}
|
||||
|
||||
function walkJavaScriptFiles(rootDir) {
|
||||
const files = [];
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return files;
|
||||
}
|
||||
const queue = [rootDir];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (fullPath.split(path.sep).includes("extensions")) {
|
||||
continue;
|
||||
}
|
||||
queue.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && JS_EXTENSIONS.has(path.extname(entry.name))) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function extractModuleSpecifiers(source) {
|
||||
const specifiers = new Set();
|
||||
const patterns = [
|
||||
/\bfrom\s*["']([^"']+)["']/g,
|
||||
/\bimport\s*["']([^"']+)["']/g,
|
||||
/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
|
||||
/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
if (match[1]) {
|
||||
specifiers.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
export function collectRootDistBundledRuntimeMirrors(params) {
|
||||
const distDir = params.distDir;
|
||||
const bundledSpecs = params.bundledRuntimeDependencySpecs;
|
||||
const mirrors = new Map();
|
||||
|
||||
for (const filePath of walkJavaScriptFiles(distDir)) {
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
const relativePath = path.relative(distDir, filePath).replaceAll(path.sep, "/");
|
||||
for (const specifier of extractModuleSpecifiers(source)) {
|
||||
const dependencyName = packageNameFromSpecifier(specifier);
|
||||
if (!dependencyName || !bundledSpecs.has(dependencyName)) {
|
||||
continue;
|
||||
}
|
||||
const bundledSpec = bundledSpecs.get(dependencyName);
|
||||
const existing = mirrors.get(dependencyName);
|
||||
if (existing) {
|
||||
existing.importers.add(relativePath);
|
||||
continue;
|
||||
}
|
||||
mirrors.set(dependencyName, {
|
||||
importers: new Set([relativePath]),
|
||||
pluginIds: bundledSpec.pluginIds,
|
||||
spec: bundledSpec.spec,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return mirrors;
|
||||
}
|
||||
|
||||
export function collectBundledPluginRootRuntimeMirrorErrors(params) {
|
||||
const rootRuntimeDeps = collectRuntimeDependencySpecs(params.rootPackageJson);
|
||||
const errors = [];
|
||||
|
||||
for (const [dependencyName, record] of params.bundledRuntimeDependencySpecs) {
|
||||
for (const conflict of record.conflicts) {
|
||||
errors.push(
|
||||
`bundled runtime dependency '${dependencyName}' has conflicting plugin specs: ${record.pluginIds.join(", ")} use '${record.spec}', ${conflict.pluginId} uses '${conflict.spec}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [dependencyName, mirror] of params.requiredRootMirrors) {
|
||||
const rootSpec = rootRuntimeDeps.get(dependencyName);
|
||||
const importers = [...mirror.importers].toSorted((left, right) => left.localeCompare(right));
|
||||
const importerLabel = importers.join(", ");
|
||||
const pluginLabel = mirror.pluginIds
|
||||
.toSorted((left, right) => left.localeCompare(right))
|
||||
.join(", ");
|
||||
if (typeof rootSpec !== "string" || rootSpec.length === 0) {
|
||||
errors.push(
|
||||
`root dist imports bundled plugin runtime dependency '${dependencyName}' from ${importerLabel}; mirror '${dependencyName}: ${mirror.spec}' in root package.json (declared by ${pluginLabel}).`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (rootSpec !== mirror.spec) {
|
||||
errors.push(
|
||||
`root dist imports bundled plugin runtime dependency '${dependencyName}' from ${importerLabel}; root package.json has '${rootSpec}' but plugin manifest declares '${mirror.spec}' (${pluginLabel}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -15,6 +15,11 @@ import { isAbsolute, join, relative } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { formatErrorMessage } from "../src/infra/errors.ts";
|
||||
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
|
||||
import {
|
||||
collectBundledPluginRootRuntimeMirrorErrors,
|
||||
collectRootDistBundledRuntimeMirrors,
|
||||
collectRuntimeDependencySpecs,
|
||||
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
|
||||
import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
|
||||
|
||||
type InstalledPackageJson = {
|
||||
@@ -26,14 +31,10 @@ type InstalledPackageJson = {
|
||||
type InstalledBundledExtensionPackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type InstalledBundledExtensionManifestRecord = {
|
||||
id: string;
|
||||
manifest: InstalledBundledExtensionPackageJson;
|
||||
path: string;
|
||||
};
|
||||
@@ -109,13 +110,6 @@ export function resolveInstalledBinaryPath(prefixDir: string, platform = process
|
||||
: join(prefixDir, "bin", "openclaw");
|
||||
}
|
||||
|
||||
function collectRuntimeDependencySpecs(packageJson: InstalledPackageJson): Map<string, string> {
|
||||
return new Map([
|
||||
...Object.entries(packageJson.dependencies ?? {}),
|
||||
...Object.entries(packageJson.optionalDependencies ?? {}),
|
||||
]);
|
||||
}
|
||||
|
||||
function collectExpectedBundledExtensionPackageIds(
|
||||
sourceExtensionsDir = join(process.cwd(), "extensions"),
|
||||
): ReadonlySet<string> | null {
|
||||
@@ -183,6 +177,7 @@ function readBundledExtensionPackageJsons(packageRoot: string): {
|
||||
}
|
||||
|
||||
manifests.push({
|
||||
id: entry.name,
|
||||
manifest: JSON.parse(
|
||||
readFileSync(realPackageJsonPath, "utf8"),
|
||||
) as InstalledBundledExtensionPackageJson,
|
||||
@@ -207,54 +202,40 @@ export function collectInstalledMirroredRootDependencyManifestErrors(
|
||||
}
|
||||
|
||||
const rootPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as InstalledPackageJson;
|
||||
const rootRuntimeDeps = collectRuntimeDependencySpecs(rootPackageJson);
|
||||
const { manifests, errors } = readBundledExtensionPackageJsons(packageRoot);
|
||||
const bundledRuntimeDependencySpecs = new Map<
|
||||
string,
|
||||
{ conflicts: Array<{ pluginId: string; spec: string }>; pluginIds: string[]; spec: string }
|
||||
>();
|
||||
|
||||
for (const { manifest: extensionPackageJson } of manifests) {
|
||||
const allowlist = extensionPackageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
|
||||
if (allowlist === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(allowlist)) {
|
||||
errors.push(
|
||||
"installed bundled extension manifest invalid: openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array.",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const { id, manifest: extensionPackageJson } of manifests) {
|
||||
const extensionRuntimeDeps = collectRuntimeDependencySpecs(extensionPackageJson);
|
||||
for (const entry of allowlist) {
|
||||
if (typeof entry !== "string" || entry.trim().length === 0) {
|
||||
errors.push(
|
||||
"installed bundled extension manifest invalid: openclaw.releaseChecks.rootDependencyMirrorAllowlist entries must be non-empty strings.",
|
||||
);
|
||||
for (const [dependencyName, spec] of extensionRuntimeDeps) {
|
||||
const existing = bundledRuntimeDependencySpecs.get(dependencyName);
|
||||
if (existing) {
|
||||
if (existing.spec !== spec) {
|
||||
existing.conflicts.push({ pluginId: id, spec });
|
||||
} else if (!existing.pluginIds.includes(id)) {
|
||||
existing.pluginIds.push(id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionSpec = extensionRuntimeDeps.get(entry);
|
||||
if (!extensionSpec) {
|
||||
errors.push(
|
||||
`installed bundled extension manifest invalid: mirrored dependency '${entry}' must be declared in the extension runtime dependencies.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const rootSpec = rootRuntimeDeps.get(entry);
|
||||
if (!rootSpec) {
|
||||
errors.push(
|
||||
`installed package is missing mirrored root runtime dependency '${entry}' required by a bundled extension.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rootSpec !== extensionSpec) {
|
||||
errors.push(
|
||||
`installed package mirrored dependency '${entry}' version mismatch: root '${rootSpec}', extension '${extensionSpec}'.`,
|
||||
);
|
||||
}
|
||||
bundledRuntimeDependencySpecs.set(dependencyName, { conflicts: [], pluginIds: [id], spec });
|
||||
}
|
||||
}
|
||||
|
||||
const requiredRootMirrors = collectRootDistBundledRuntimeMirrors({
|
||||
bundledRuntimeDependencySpecs,
|
||||
distDir: join(packageRoot, "dist"),
|
||||
});
|
||||
errors.push(
|
||||
...collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs,
|
||||
requiredRootMirrors,
|
||||
rootPackageJson,
|
||||
}),
|
||||
);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -276,8 +257,12 @@ function resolveGlobalRoot(prefixDir: string, cwd: string): string {
|
||||
return npmExec(["root", "-g", "--prefix", prefixDir], cwd);
|
||||
}
|
||||
|
||||
export function buildPublishedInstallCommandArgs(prefixDir: string, spec: string): string[] {
|
||||
return ["install", "-g", "--prefix", prefixDir, spec, "--no-fund", "--no-audit"];
|
||||
}
|
||||
|
||||
function installSpec(prefixDir: string, spec: string, cwd: string): void {
|
||||
npmExec(["install", "-g", "--prefix", prefixDir, spec, "--no-fund", "--no-audit"], cwd);
|
||||
npmExec(buildPublishedInstallCommandArgs(prefixDir, spec), cwd);
|
||||
}
|
||||
|
||||
function readInstalledBinaryVersion(prefixDir: string, cwd: string): string {
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { execFileSync, execSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
} from "node:fs";
|
||||
import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -19,11 +11,21 @@ import {
|
||||
type ExtensionPackageJson as PackageJson,
|
||||
} from "./lib/bundled-extension-manifest.ts";
|
||||
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
|
||||
import {
|
||||
collectBundledPluginRootRuntimeMirrorErrors,
|
||||
collectBundledPluginRuntimeDependencySpecs,
|
||||
collectRootDistBundledRuntimeMirrors,
|
||||
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
|
||||
import { listPluginSdkDistArtifacts } from "./lib/plugin-sdk-entries.mjs";
|
||||
import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs";
|
||||
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
|
||||
|
||||
export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts";
|
||||
export {
|
||||
collectBundledPluginRootRuntimeMirrorErrors,
|
||||
collectRootDistBundledRuntimeMirrors,
|
||||
packageNameFromSpecifier,
|
||||
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
|
||||
|
||||
type PackFile = { path: string };
|
||||
type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number };
|
||||
@@ -78,16 +80,6 @@ function collectBundledExtensions(): BundledExtension[] {
|
||||
});
|
||||
}
|
||||
|
||||
function collectRuntimeDependencySpecs(packageJson: {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
}): Map<string, string> {
|
||||
return new Map([
|
||||
...Object.entries(packageJson.dependencies ?? {}),
|
||||
...Object.entries(packageJson.optionalDependencies ?? {}),
|
||||
]);
|
||||
}
|
||||
|
||||
function checkBundledExtensionMetadata() {
|
||||
const extensions = collectBundledExtensions();
|
||||
const manifestErrors = collectBundledExtensionManifestErrors(extensions);
|
||||
@@ -95,11 +87,18 @@ function checkBundledExtensionMetadata() {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
};
|
||||
const rootRuntimeDeps = collectRuntimeDependencySpecs(rootPackage);
|
||||
const rootMirrorErrors = collectBundledExtensionRootDependencyMirrorErrors(
|
||||
extensions,
|
||||
rootRuntimeDeps,
|
||||
const bundledRuntimeDependencySpecs = collectBundledPluginRuntimeDependencySpecs(
|
||||
resolve("extensions"),
|
||||
);
|
||||
const requiredRootMirrors = collectRootDistBundledRuntimeMirrors({
|
||||
bundledRuntimeDependencySpecs,
|
||||
distDir: resolve("dist"),
|
||||
});
|
||||
const rootMirrorErrors = collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs,
|
||||
requiredRootMirrors,
|
||||
rootPackageJson: rootPackage,
|
||||
});
|
||||
const errors = [...manifestErrors, ...rootMirrorErrors];
|
||||
if (errors.length > 0) {
|
||||
console.error("release-check: bundled extension manifest validation failed:");
|
||||
@@ -110,63 +109,6 @@ function checkBundledExtensionMetadata() {
|
||||
}
|
||||
}
|
||||
|
||||
export function collectBundledExtensionRootDependencyMirrorErrors(
|
||||
extensions: BundledExtension[],
|
||||
rootRuntimeDeps: ReadonlyMap<string, string>,
|
||||
): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const extension of extensions) {
|
||||
const rawReleaseChecks = extension.packageJson.openclaw?.releaseChecks;
|
||||
const allowlist = (rawReleaseChecks as { rootDependencyMirrorAllowlist?: unknown } | undefined)
|
||||
?.rootDependencyMirrorAllowlist;
|
||||
|
||||
if (allowlist === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(allowlist)) {
|
||||
errors.push(
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionRuntimeDeps = collectRuntimeDependencySpecs(extension.packageJson);
|
||||
|
||||
for (const entry of allowlist) {
|
||||
if (typeof entry !== "string" || entry.trim().length === 0) {
|
||||
errors.push(
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entries must be non-empty strings`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionSpec = extensionRuntimeDeps.get(entry);
|
||||
if (!extensionSpec) {
|
||||
errors.push(
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '${entry}' must be declared in extension runtime dependencies`,
|
||||
);
|
||||
}
|
||||
const rootSpec = rootRuntimeDeps.get(entry);
|
||||
if (!rootSpec) {
|
||||
errors.push(
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '${entry}' must be mirrored in root runtime dependencies`,
|
||||
);
|
||||
}
|
||||
if (!extensionSpec || !rootSpec) {
|
||||
continue;
|
||||
}
|
||||
if (extensionSpec !== rootSpec) {
|
||||
errors.push(
|
||||
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '${entry}' must match root runtime dependency version (extension '${extensionSpec}', root '${rootSpec}')`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function runPackDry(): PackResult[] {
|
||||
const raw = execSync("npm pack --dry-run --json --ignore-scripts", {
|
||||
encoding: "utf8",
|
||||
@@ -201,32 +143,47 @@ function resolvePackedTarballPath(packDestination: string, results: PackResult[]
|
||||
return resolve(packDestination, filenames[0]);
|
||||
}
|
||||
|
||||
function linkRootNodeModules(packageRoot: string): void {
|
||||
const rootNodeModules = resolve("node_modules");
|
||||
if (!existsSync(rootNodeModules)) {
|
||||
return;
|
||||
}
|
||||
symlinkSync(
|
||||
rootNodeModules,
|
||||
join(packageRoot, "node_modules"),
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
function installPackedTarball(prefixDir: string, tarballPath: string, cwd: string): void {
|
||||
execFileSync(
|
||||
"npm",
|
||||
[
|
||||
"install",
|
||||
"-g",
|
||||
"--prefix",
|
||||
prefixDir,
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
tarballPath,
|
||||
],
|
||||
{
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function resolveGlobalRoot(prefixDir: string, cwd: string): string {
|
||||
return execFileSync("npm", ["root", "-g", "--prefix", prefixDir], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function runPackedBundledChannelEntrySmoke(): void {
|
||||
const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-"));
|
||||
try {
|
||||
const packDir = join(tmpRoot, "pack");
|
||||
const extractDir = join(tmpRoot, "extract");
|
||||
mkdirSync(packDir);
|
||||
mkdirSync(extractDir);
|
||||
|
||||
const packResults = runPack(packDir);
|
||||
const tarballPath = resolvePackedTarballPath(packDir, packResults);
|
||||
execFileSync("tar", ["-xzf", tarballPath, "-C", extractDir], { stdio: "inherit" });
|
||||
const prefixDir = join(tmpRoot, "prefix");
|
||||
installPackedTarball(prefixDir, tarballPath, tmpRoot);
|
||||
|
||||
const packageRoot = join(extractDir, "package");
|
||||
linkRootNodeModules(packageRoot);
|
||||
const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw");
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
|
||||
@@ -19,14 +19,18 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
|
||||
mirroredRootRuntimeDeps: ["@larksuiteoapi/node-sdk"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "googlechat", minHostVersionBaseline: "2026.3.22" },
|
||||
{
|
||||
pluginId: "googlechat",
|
||||
mirroredRootRuntimeDeps: ["google-auth-library"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "irc", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "line", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "matrix", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "mattermost", minHostVersionBaseline: "2026.3.22" },
|
||||
{
|
||||
pluginId: "memory-lancedb",
|
||||
pluginLocalRuntimeDeps: ["@lancedb/lancedb"],
|
||||
mirroredRootRuntimeDeps: ["@lancedb/lancedb", "openai"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "msteams", minHostVersionBaseline: "2026.3.22" },
|
||||
@@ -46,7 +50,8 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
|
||||
{ pluginId: "voice-call", minHostVersionBaseline: "2026.3.22" },
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
pluginLocalRuntimeDeps: ["@whiskeysockets/baileys", "jimp"],
|
||||
pluginLocalRuntimeDeps: ["@whiskeysockets/baileys"],
|
||||
mirroredRootRuntimeDeps: ["jimp"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "zalo", minHostVersionBaseline: "2026.3.22" },
|
||||
|
||||
@@ -66,42 +66,22 @@ function readRootPackageJson(): {
|
||||
function readMatrixPackageJson(): {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
} {
|
||||
return JSON.parse(readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function readAmazonBedrockPackageJson(): {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
} {
|
||||
return JSON.parse(
|
||||
readFileSync(resolve(REPO_ROOT, "extensions/amazon-bedrock/package.json"), "utf8"),
|
||||
) as {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -327,15 +307,12 @@ describe("plugin-sdk package contract guardrails", () => {
|
||||
const rootRuntimeDeps = collectRuntimeDependencySpecs(readRootPackageJson());
|
||||
const matrixPackageJson = readMatrixPackageJson();
|
||||
const matrixRuntimeDeps = collectRuntimeDependencySpecs(matrixPackageJson);
|
||||
const allowlist = matrixPackageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
|
||||
|
||||
expect(Array.isArray(allowlist)).toBe(true);
|
||||
const matrixRootMirrorAllowlist = allowlist as string[];
|
||||
expect(matrixRootMirrorAllowlist).toEqual(
|
||||
expect.arrayContaining(["@matrix-org/matrix-sdk-crypto-wasm"]),
|
||||
);
|
||||
|
||||
for (const dep of matrixRootMirrorAllowlist) {
|
||||
for (const dep of [
|
||||
"@matrix-org/matrix-sdk-crypto-wasm",
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs",
|
||||
"matrix-js-sdk",
|
||||
]) {
|
||||
expect(rootRuntimeDeps.get(dep)).toBe(matrixRuntimeDeps.get(dep));
|
||||
}
|
||||
});
|
||||
@@ -344,13 +321,8 @@ describe("plugin-sdk package contract guardrails", () => {
|
||||
const rootRuntimeDeps = collectRuntimeDependencySpecs(readRootPackageJson());
|
||||
const bedrockPackageJson = readAmazonBedrockPackageJson();
|
||||
const bedrockRuntimeDeps = collectRuntimeDependencySpecs(bedrockPackageJson);
|
||||
const allowlist = bedrockPackageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
|
||||
|
||||
expect(Array.isArray(allowlist)).toBe(true);
|
||||
const bedrockRootMirrorAllowlist = allowlist as string[];
|
||||
expect(bedrockRootMirrorAllowlist).toEqual(expect.arrayContaining(["@aws-sdk/client-bedrock"]));
|
||||
|
||||
for (const dep of bedrockRootMirrorAllowlist) {
|
||||
for (const dep of ["@aws-sdk/client-bedrock"]) {
|
||||
expect(rootRuntimeDeps.get(dep)).toBe(bedrockRuntimeDeps.get(dep));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,9 +12,6 @@ type PackageManifest = {
|
||||
install?: {
|
||||
minHostVersion?: string;
|
||||
};
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -63,12 +60,9 @@ export function describePackageManifestContract(params: PackageManifestContractP
|
||||
const rootSpec =
|
||||
rootManifest.dependencies?.[dependencyName] ??
|
||||
rootManifest.optionalDependencies?.[dependencyName];
|
||||
const allowlist = pluginManifest.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
|
||||
|
||||
expect(pluginSpec).toBeTruthy();
|
||||
expect(rootSpec).toBe(pluginSpec);
|
||||
expect(Array.isArray(allowlist)).toBe(true);
|
||||
expect(allowlist).toContain(dependencyName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPublishedInstallCommandArgs,
|
||||
buildPublishedInstallScenarios,
|
||||
collectInstalledMirroredRootDependencyManifestErrors,
|
||||
collectInstalledPackageErrors,
|
||||
@@ -38,6 +39,23 @@ describe("buildPublishedInstallScenarios", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPublishedInstallCommandArgs", () => {
|
||||
it("runs lifecycle scripts for published install verification", () => {
|
||||
const args = buildPublishedInstallCommandArgs("/tmp/openclaw-prefix", "openclaw@2026.4.9");
|
||||
|
||||
expect(args).toEqual([
|
||||
"install",
|
||||
"-g",
|
||||
"--prefix",
|
||||
"/tmp/openclaw-prefix",
|
||||
"openclaw@2026.4.9",
|
||||
"--no-fund",
|
||||
"--no-audit",
|
||||
]);
|
||||
expect(args).not.toContain("--ignore-scripts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledPackageErrors", () => {
|
||||
it("flags version mismatches and missing runtime sidecars", () => {
|
||||
const errors = collectInstalledPackageErrors({
|
||||
@@ -95,7 +113,7 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
|
||||
writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
it("flags missing mirrored root dependencies required by bundled extensions", () => {
|
||||
it("flags missing root mirrors for bundled plugin deps imported by root dist", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
@@ -107,15 +125,16 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.15.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@slack/web-api"],
|
||||
},
|
||||
},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "probe-Cz2PiFtC.js"),
|
||||
'import("@slack/web-api");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package is missing mirrored root runtime dependency '@slack/web-api' required by a bundled extension.",
|
||||
"root dist imports bundled plugin runtime dependency '@slack/web-api' from probe-Cz2PiFtC.js; mirror '@slack/web-api: ^7.15.0' in root package.json (declared by slack).",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
@@ -136,12 +155,13 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
|
||||
optionalDependencies: {
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@discordjs/opus"],
|
||||
},
|
||||
},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "probe-Cz2PiFtC.js"),
|
||||
'require("@discordjs/opus");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
@@ -149,7 +169,7 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("flags mirrored root dependency version mismatches", () => {
|
||||
it("flags root mirror dependency version mismatches", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
@@ -163,15 +183,16 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.15.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@slack/web-api"],
|
||||
},
|
||||
},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "probe-Cz2PiFtC.js"),
|
||||
'import("@slack/web-api");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package mirrored dependency '@slack/web-api' version mismatch: root '^7.16.0', extension '^7.15.0'.",
|
||||
"root dist imports bundled plugin runtime dependency '@slack/web-api' from probe-Cz2PiFtC.js; root package.json has '^7.16.0' but plugin manifest declares '^7.15.0' (slack).",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listBundledPluginPackArtifacts } from "../scripts/lib/bundled-plugin-build-entries.mjs";
|
||||
import { listPluginSdkDistArtifacts } from "../scripts/lib/plugin-sdk-entries.mjs";
|
||||
import {
|
||||
collectAppcastSparkleVersionErrors,
|
||||
collectBundledExtensionManifestErrors,
|
||||
collectBundledExtensionRootDependencyMirrorErrors,
|
||||
collectBundledPluginRootRuntimeMirrorErrors,
|
||||
collectRootDistBundledRuntimeMirrors,
|
||||
collectForbiddenPackPaths,
|
||||
collectMissingPackPaths,
|
||||
collectPackUnpackedSizeErrors,
|
||||
packageNameFromSpecifier,
|
||||
} from "../scripts/release-check.ts";
|
||||
import { bundledDistPluginFile, bundledPluginFile } from "./helpers/bundled-plugin-paths.js";
|
||||
|
||||
@@ -110,126 +115,132 @@ describe("collectBundledExtensionManifestErrors", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectBundledExtensionRootDependencyMirrorErrors", () => {
|
||||
it("flags a non-array mirror allowlist", () => {
|
||||
describe("bundled plugin root runtime mirrors", () => {
|
||||
function makeBundledSpecs() {
|
||||
return new Map([
|
||||
["@larksuiteoapi/node-sdk", { conflicts: [], pluginIds: ["feishu"], spec: "^1.60.0" }],
|
||||
]);
|
||||
}
|
||||
|
||||
it("maps package names from import specifiers", () => {
|
||||
expect(packageNameFromSpecifier("@larksuiteoapi/node-sdk/subpath")).toBe(
|
||||
"@larksuiteoapi/node-sdk",
|
||||
);
|
||||
expect(packageNameFromSpecifier("grammy/web")).toBe("grammy");
|
||||
expect(packageNameFromSpecifier("node:fs")).toBeNull();
|
||||
expect(packageNameFromSpecifier("./local")).toBeNull();
|
||||
});
|
||||
|
||||
it("derives required root mirrors from built root dist imports", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-root-mirror-"));
|
||||
|
||||
try {
|
||||
const distDir = join(tempRoot, "dist");
|
||||
mkdirSync(join(distDir, "extensions", "feishu"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(distDir, "probe-Cz2PiFtC.js"),
|
||||
`import("@larksuiteoapi/node-sdk");\nrequire("grammy");\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(distDir, "extensions", "feishu", "index.js"),
|
||||
`import("@larksuiteoapi/node-sdk");\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const mirrors = collectRootDistBundledRuntimeMirrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
distDir,
|
||||
});
|
||||
|
||||
expect([...mirrors.keys()]).toEqual(["@larksuiteoapi/node-sdk"]);
|
||||
expect([...mirrors.get("@larksuiteoapi/node-sdk")!.importers]).toEqual(["probe-Cz2PiFtC.js"]);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags missing root mirrors for plugin deps imported by root dist", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyMirrorErrors(
|
||||
[
|
||||
{
|
||||
id: "matrix",
|
||||
packageJson: {
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: true,
|
||||
},
|
||||
},
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
requiredRootMirrors: new Map([
|
||||
[
|
||||
"@larksuiteoapi/node-sdk",
|
||||
{
|
||||
importers: new Set(["probe-Cz2PiFtC.js"]),
|
||||
pluginIds: ["feishu"],
|
||||
spec: "^1.60.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
new Map(),
|
||||
),
|
||||
],
|
||||
]),
|
||||
rootPackageJson: { dependencies: {} },
|
||||
}),
|
||||
).toEqual([
|
||||
"bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array",
|
||||
"root dist imports bundled plugin runtime dependency '@larksuiteoapi/node-sdk' from probe-Cz2PiFtC.js; mirror '@larksuiteoapi/node-sdk: ^1.60.0' in root package.json (declared by feishu).",
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags mirror entries missing from extension runtime dependencies", () => {
|
||||
it("flags root mirror version drift from plugin manifests", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyMirrorErrors(
|
||||
[
|
||||
{
|
||||
id: "matrix",
|
||||
packageJson: {
|
||||
dependencies: {
|
||||
"matrix-js-sdk": "41.2.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"],
|
||||
},
|
||||
},
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
requiredRootMirrors: new Map([
|
||||
[
|
||||
"@larksuiteoapi/node-sdk",
|
||||
{
|
||||
importers: new Set(["probe-Cz2PiFtC.js"]),
|
||||
pluginIds: ["feishu"],
|
||||
spec: "^1.60.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
new Map([["@matrix-org/matrix-sdk-crypto-wasm", "18.0.0"]]),
|
||||
),
|
||||
],
|
||||
]),
|
||||
rootPackageJson: { dependencies: { "@larksuiteoapi/node-sdk": "^1.61.0" } },
|
||||
}),
|
||||
).toEqual([
|
||||
"bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '@matrix-org/matrix-sdk-crypto-wasm' must be declared in extension runtime dependencies",
|
||||
"root dist imports bundled plugin runtime dependency '@larksuiteoapi/node-sdk' from probe-Cz2PiFtC.js; root package.json has '^1.61.0' but plugin manifest declares '^1.60.0' (feishu).",
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags mirror entries missing from root runtime dependencies", () => {
|
||||
it("accepts matching root mirrors for plugin deps imported by root dist", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyMirrorErrors(
|
||||
[
|
||||
{
|
||||
id: "matrix",
|
||||
packageJson: {
|
||||
dependencies: {
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "18.0.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"],
|
||||
},
|
||||
},
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: makeBundledSpecs(),
|
||||
requiredRootMirrors: new Map([
|
||||
[
|
||||
"@larksuiteoapi/node-sdk",
|
||||
{
|
||||
importers: new Set(["probe-Cz2PiFtC.js"]),
|
||||
pluginIds: ["feishu"],
|
||||
spec: "^1.60.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
new Map(),
|
||||
),
|
||||
).toEqual([
|
||||
"bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '@matrix-org/matrix-sdk-crypto-wasm' must be mirrored in root runtime dependencies",
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags mirror entries whose root version drifts from the extension", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyMirrorErrors(
|
||||
[
|
||||
{
|
||||
id: "matrix",
|
||||
packageJson: {
|
||||
dependencies: {
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "18.0.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
new Map([["@matrix-org/matrix-sdk-crypto-wasm", "18.1.0"]]),
|
||||
),
|
||||
).toEqual([
|
||||
"bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '@matrix-org/matrix-sdk-crypto-wasm' must match root runtime dependency version (extension '18.0.0', root '18.1.0')",
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts mirror entries declared by both the extension and root package", () => {
|
||||
expect(
|
||||
collectBundledExtensionRootDependencyMirrorErrors(
|
||||
[
|
||||
{
|
||||
id: "matrix",
|
||||
packageJson: {
|
||||
dependencies: {
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "18.0.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
new Map([["@matrix-org/matrix-sdk-crypto-wasm", "18.0.0"]]),
|
||||
),
|
||||
],
|
||||
]),
|
||||
rootPackageJson: { dependencies: { "@larksuiteoapi/node-sdk": "^1.60.0" } },
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags conflicting plugin dependency specs", () => {
|
||||
expect(
|
||||
collectBundledPluginRootRuntimeMirrorErrors({
|
||||
bundledRuntimeDependencySpecs: new Map([
|
||||
[
|
||||
"@example/sdk",
|
||||
{
|
||||
conflicts: [{ pluginId: "right", spec: "2.0.0" }],
|
||||
pluginIds: ["left"],
|
||||
spec: "1.0.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
requiredRootMirrors: new Map(),
|
||||
rootPackageJson: { dependencies: {} },
|
||||
}),
|
||||
).toEqual([
|
||||
"bundled runtime dependency '@example/sdk' has conflicting plugin specs: left use '1.0.0', right uses '2.0.0'.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectForbiddenPackPaths", () => {
|
||||
|
||||
Reference in New Issue
Block a user