mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
release: mirror bundled channel deps at root (#63065)
Merged via squash.
Prepared head SHA: ac26799a54
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Reviewed-by: @scoootscooob
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Slack/media: preserve bearer auth across same-origin `files.slack.com` redirects while still stripping it on cross-origin Slack CDN hops, so `url_private_download` image attachments load again. (#62960) Thanks @vincentkoc.
|
||||
- Control UI: guard stale session-history reloads during fast session switches so the selected session and rendered transcript stay in sync. (#62975) Thanks @scoootscooob.
|
||||
- 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.
|
||||
|
||||
## 2026.4.8
|
||||
|
||||
|
||||
@@ -60,6 +60,14 @@
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@buape/carbon",
|
||||
"@discordjs/opus",
|
||||
"https-proxy-agent",
|
||||
"opusscript"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@larksuiteoapi/node-sdk"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
|
||||
@@ -34,6 +34,13 @@
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@slack/bolt",
|
||||
"@slack/web-api",
|
||||
"https-proxy-agent"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
},
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"releaseChecks": {
|
||||
"rootDependencyMirrorAllowlist": [
|
||||
"@grammyjs/runner",
|
||||
"@grammyjs/transformer-throttler",
|
||||
"grammy"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -1311,8 +1311,12 @@
|
||||
"@aws-sdk/client-bedrock": "3.1024.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
|
||||
"@aws-sdk/credential-provider-node": "3.972.29",
|
||||
"@buape/carbon": "0.14.0",
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.6",
|
||||
"@larksuiteoapi/node-sdk": "^1.60.0",
|
||||
"@line/bot-sdk": "^11.0.0",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
"@mariozechner/pi-agent-core": "0.65.2",
|
||||
@@ -1323,6 +1327,8 @@
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@sinclair/typebox": "0.34.49",
|
||||
"@slack/bolt": "^4.6.0",
|
||||
"@slack/web-api": "^7.15.0",
|
||||
"ajv": "^8.18.0",
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
@@ -1333,7 +1339,9 @@
|
||||
"express": "^5.2.1",
|
||||
"file-type": "22.0.0",
|
||||
"gaxios": "7.1.4",
|
||||
"grammy": "^1.42.0",
|
||||
"hono": "4.12.10",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"ipaddr.js": "^2.3.0",
|
||||
"jiti": "^2.6.1",
|
||||
"json5": "^2.2.3",
|
||||
@@ -1343,6 +1351,7 @@
|
||||
"markdown-it": "^14.1.1",
|
||||
"matrix-js-sdk": "41.3.0-rc.0",
|
||||
"node-edge-tts": "^1.2.10",
|
||||
"opusscript": "^0.1.1",
|
||||
"osc-progress": "^0.3.0",
|
||||
"pdfjs-dist": "^5.6.205",
|
||||
"playwright-core": "1.59.1",
|
||||
@@ -1392,6 +1401,7 @@
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||
"openshell": "0.1.0"
|
||||
},
|
||||
|
||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -45,12 +45,24 @@ importers:
|
||||
'@aws-sdk/credential-provider-node':
|
||||
specifier: 3.972.29
|
||||
version: 3.972.29
|
||||
'@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
|
||||
'@grammyjs/runner':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(grammy@1.42.0)
|
||||
'@grammyjs/transformer-throttler':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(grammy@1.42.0)
|
||||
'@homebridge/ciao':
|
||||
specifier: ^1.3.6
|
||||
version: 1.3.6
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.60.0
|
||||
version: 1.60.0
|
||||
'@line/bot-sdk':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
@@ -84,6 +96,12 @@ importers:
|
||||
'@sinclair/typebox':
|
||||
specifier: 0.34.49
|
||||
version: 0.34.49
|
||||
'@slack/bolt':
|
||||
specifier: ^4.6.0
|
||||
version: 4.6.0(@types/express@5.0.6)
|
||||
'@slack/web-api':
|
||||
specifier: ^7.15.0
|
||||
version: 7.15.0
|
||||
ajv:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
@@ -114,9 +132,15 @@ importers:
|
||||
gaxios:
|
||||
specifier: 7.1.4
|
||||
version: 7.1.4
|
||||
grammy:
|
||||
specifier: ^1.42.0
|
||||
version: 1.42.0
|
||||
hono:
|
||||
specifier: 4.12.10
|
||||
version: 4.12.10
|
||||
https-proxy-agent:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
ipaddr.js:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
@@ -147,6 +171,9 @@ importers:
|
||||
node-llama-cpp:
|
||||
specifier: 3.18.1
|
||||
version: 3.18.1(typescript@6.0.2)
|
||||
opusscript:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
osc-progress:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
@@ -257,6 +284,9 @@ importers:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
optionalDependencies:
|
||||
'@discordjs/opus':
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0
|
||||
'@matrix-org/matrix-sdk-crypto-nodejs':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
@@ -9807,7 +9837,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@slack/logger': 4.0.1
|
||||
'@slack/types': 2.20.1
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
'@types/retry': 0.12.0
|
||||
axios: 1.13.6
|
||||
eventemitter3: 5.0.4
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
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";
|
||||
@@ -11,8 +19,27 @@ import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm
|
||||
|
||||
type InstalledPackageJson = {
|
||||
version?: string;
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
type InstalledBundledExtensionPackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type InstalledBundledExtensionManifestRecord = {
|
||||
manifest: InstalledBundledExtensionPackageJson;
|
||||
path: string;
|
||||
};
|
||||
|
||||
const MAX_BUNDLED_EXTENSION_MANIFEST_BYTES = 1024 * 1024;
|
||||
|
||||
export type PublishedInstallScenario = {
|
||||
name: string;
|
||||
installSpecs: string[];
|
||||
@@ -64,6 +91,141 @@ export function collectInstalledPackageErrors(params: {
|
||||
}
|
||||
}
|
||||
|
||||
errors.push(...collectInstalledMirroredRootDependencyManifestErrors(params.packageRoot));
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function resolveInstalledBinaryPath(prefixDir: string, platform = process.platform): string {
|
||||
return platform === "win32"
|
||||
? join(prefixDir, "openclaw.cmd")
|
||||
: join(prefixDir, "bin", "openclaw");
|
||||
}
|
||||
|
||||
function collectRuntimeDependencySpecs(packageJson: InstalledPackageJson): Map<string, string> {
|
||||
return new Map([
|
||||
...Object.entries(packageJson.dependencies ?? {}),
|
||||
...Object.entries(packageJson.optionalDependencies ?? {}),
|
||||
]);
|
||||
}
|
||||
|
||||
function readBundledExtensionPackageJsons(packageRoot: string): {
|
||||
manifests: InstalledBundledExtensionManifestRecord[];
|
||||
errors: string[];
|
||||
} {
|
||||
const extensionsDir = join(packageRoot, "dist", "extensions");
|
||||
if (!existsSync(extensionsDir)) {
|
||||
return { manifests: [], errors: [] };
|
||||
}
|
||||
|
||||
const manifests: InstalledBundledExtensionManifestRecord[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionDirPath = join(extensionsDir, entry.name);
|
||||
const packageJsonPath = join(extensionsDir, entry.name, "package.json");
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
errors.push(`installed bundled extension manifest missing: ${packageJsonPath}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJsonStats = lstatSync(packageJsonPath);
|
||||
if (!packageJsonStats.isFile()) {
|
||||
throw new Error("manifest must be a regular file");
|
||||
}
|
||||
if (packageJsonStats.size > MAX_BUNDLED_EXTENSION_MANIFEST_BYTES) {
|
||||
throw new Error(`manifest exceeds ${MAX_BUNDLED_EXTENSION_MANIFEST_BYTES} bytes`);
|
||||
}
|
||||
|
||||
const realExtensionDirPath = realpathSync(extensionDirPath);
|
||||
const realPackageJsonPath = realpathSync(packageJsonPath);
|
||||
const relativeManifestPath = relative(realExtensionDirPath, realPackageJsonPath);
|
||||
if (
|
||||
relativeManifestPath.length === 0 ||
|
||||
relativeManifestPath.startsWith("..") ||
|
||||
isAbsolute(relativeManifestPath)
|
||||
) {
|
||||
throw new Error("manifest resolves outside the bundled extension directory");
|
||||
}
|
||||
|
||||
manifests.push({
|
||||
manifest: JSON.parse(
|
||||
readFileSync(realPackageJsonPath, "utf8"),
|
||||
) as InstalledBundledExtensionPackageJson,
|
||||
path: realPackageJsonPath,
|
||||
});
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`installed bundled extension manifest invalid: failed to parse ${packageJsonPath}: ${formatErrorMessage(error)}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { manifests, errors };
|
||||
}
|
||||
|
||||
export function collectInstalledMirroredRootDependencyManifestErrors(
|
||||
packageRoot: string,
|
||||
): string[] {
|
||||
const packageJsonPath = join(packageRoot, "package.json");
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return ["installed package is missing package.json."];
|
||||
}
|
||||
|
||||
const rootPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as InstalledPackageJson;
|
||||
const rootRuntimeDeps = collectRuntimeDependencySpecs(rootPackageJson);
|
||||
const { manifests, errors } = readBundledExtensionPackageJsons(packageRoot);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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.",
|
||||
);
|
||||
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}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -89,6 +251,15 @@ function installSpec(prefixDir: string, spec: string, cwd: string): void {
|
||||
npmExec(["install", "-g", "--prefix", prefixDir, spec, "--no-fund", "--no-audit"], cwd);
|
||||
}
|
||||
|
||||
function readInstalledBinaryVersion(prefixDir: string, cwd: string): string {
|
||||
return execFileSync(resolveInstalledBinaryPath(prefixDir), ["--version"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
shell: process.platform === "win32",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function verifyScenario(version: string, scenario: PublishedInstallScenario): void {
|
||||
const workingDir = mkdtempSync(join(tmpdir(), `openclaw-postpublish-${scenario.name}.`));
|
||||
const prefixDir = join(workingDir, "prefix");
|
||||
@@ -108,6 +279,13 @@ function verifyScenario(version: string, scenario: PublishedInstallScenario): vo
|
||||
installedVersion: pkg.version?.trim() ?? "",
|
||||
packageRoot,
|
||||
});
|
||||
const installedBinaryVersion = readInstalledBinaryVersion(prefixDir, workingDir);
|
||||
|
||||
if (installedBinaryVersion !== scenario.expectedVersion) {
|
||||
errors.push(
|
||||
`installed openclaw binary version mismatch: expected ${scenario.expectedVersion}, found ${installedBinaryVersion || "<missing>"}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`${scenario.name} failed:\n- ${errors.join("\n- ")}`);
|
||||
|
||||
@@ -6,12 +6,17 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
|
||||
{ pluginId: "bluebubbles", minHostVersionBaseline: "2026.3.22" },
|
||||
{
|
||||
pluginId: "discord",
|
||||
runtimeDeps: ["@buape/carbon", "https-proxy-agent"],
|
||||
mirroredRootRuntimeDeps: [
|
||||
"@buape/carbon",
|
||||
"@discordjs/opus",
|
||||
"https-proxy-agent",
|
||||
"opusscript",
|
||||
],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{
|
||||
pluginId: "feishu",
|
||||
runtimeDeps: ["@larksuiteoapi/node-sdk"],
|
||||
mirroredRootRuntimeDeps: ["@larksuiteoapi/node-sdk"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "googlechat", minHostVersionBaseline: "2026.3.22" },
|
||||
@@ -21,21 +26,27 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
|
||||
{ pluginId: "mattermost", minHostVersionBaseline: "2026.3.22" },
|
||||
{
|
||||
pluginId: "memory-lancedb",
|
||||
runtimeDeps: ["@lancedb/lancedb"],
|
||||
pluginLocalRuntimeDeps: ["@lancedb/lancedb"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "msteams", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "nextcloud-talk", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "nostr", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "slack", runtimeDeps: ["@slack/bolt"] },
|
||||
{
|
||||
pluginId: "slack",
|
||||
mirroredRootRuntimeDeps: ["@slack/bolt", "@slack/web-api", "https-proxy-agent"],
|
||||
},
|
||||
{ pluginId: "synology-chat", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "telegram", runtimeDeps: ["grammy"] },
|
||||
{
|
||||
pluginId: "telegram",
|
||||
mirroredRootRuntimeDeps: ["@grammyjs/runner", "@grammyjs/transformer-throttler", "grammy"],
|
||||
},
|
||||
{ pluginId: "tlon", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "twitch", minHostVersionBaseline: "2026.3.22" },
|
||||
{ pluginId: "voice-call", minHostVersionBaseline: "2026.3.22" },
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
runtimeDeps: ["@whiskeysockets/baileys", "jimp"],
|
||||
pluginLocalRuntimeDeps: ["@whiskeysockets/baileys", "jimp"],
|
||||
minHostVersionBaseline: "2026.3.22",
|
||||
},
|
||||
{ pluginId: "zalo", minHostVersionBaseline: "2026.3.22" },
|
||||
|
||||
@@ -7,16 +7,21 @@ import { bundledPluginFile } from "../bundled-plugin-paths.js";
|
||||
|
||||
type PackageManifest = {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
openclaw?: {
|
||||
install?: {
|
||||
minHostVersion?: string;
|
||||
};
|
||||
releaseChecks?: {
|
||||
rootDependencyMirrorAllowlist?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type PackageManifestContractParams = {
|
||||
pluginId: string;
|
||||
runtimeDeps?: string[];
|
||||
pluginLocalRuntimeDeps?: string[];
|
||||
mirroredRootRuntimeDeps?: string[];
|
||||
minHostVersionBaseline?: string;
|
||||
};
|
||||
|
||||
@@ -29,13 +34,17 @@ export function describePackageManifestContract(params: PackageManifestContractP
|
||||
const packagePath = bundledPluginFile(params.pluginId, "package.json");
|
||||
|
||||
describe(`${params.pluginId} package manifest contract`, () => {
|
||||
if (params.runtimeDeps?.length) {
|
||||
for (const dependencyName of params.runtimeDeps) {
|
||||
if (params.pluginLocalRuntimeDeps?.length) {
|
||||
for (const dependencyName of params.pluginLocalRuntimeDeps) {
|
||||
it(`keeps ${dependencyName} plugin-local`, () => {
|
||||
const rootManifest = readJson<PackageManifest>("package.json");
|
||||
const pluginManifest = readJson<PackageManifest>(packagePath);
|
||||
const pluginSpec = pluginManifest.dependencies?.[dependencyName];
|
||||
const rootSpec = rootManifest.dependencies?.[dependencyName];
|
||||
const pluginSpec =
|
||||
pluginManifest.dependencies?.[dependencyName] ??
|
||||
pluginManifest.optionalDependencies?.[dependencyName];
|
||||
const rootSpec =
|
||||
rootManifest.dependencies?.[dependencyName] ??
|
||||
rootManifest.optionalDependencies?.[dependencyName];
|
||||
|
||||
expect(pluginSpec).toBeTruthy();
|
||||
expect(rootSpec).toBeUndefined();
|
||||
@@ -43,6 +52,27 @@ export function describePackageManifestContract(params: PackageManifestContractP
|
||||
}
|
||||
}
|
||||
|
||||
if (params.mirroredRootRuntimeDeps?.length) {
|
||||
for (const dependencyName of params.mirroredRootRuntimeDeps) {
|
||||
it(`mirrors ${dependencyName} at the root package`, () => {
|
||||
const rootManifest = readJson<PackageManifest>("package.json");
|
||||
const pluginManifest = readJson<PackageManifest>(packagePath);
|
||||
const pluginSpec =
|
||||
pluginManifest.dependencies?.[dependencyName] ??
|
||||
pluginManifest.optionalDependencies?.[dependencyName];
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const minHostVersionBaseline = params.minHostVersionBaseline;
|
||||
if (minHostVersionBaseline) {
|
||||
it("declares a parseable minHostVersion floor at or above the baseline", () => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPublishedInstallScenarios,
|
||||
collectInstalledMirroredRootDependencyManifestErrors,
|
||||
collectInstalledPackageErrors,
|
||||
resolveInstalledBinaryPath,
|
||||
} from "../scripts/openclaw-npm-postpublish-verify.ts";
|
||||
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
|
||||
|
||||
@@ -54,3 +59,185 @@ describe("collectInstalledPackageErrors", () => {
|
||||
expect(errors.length).toBeGreaterThanOrEqual(1 + BUNDLED_RUNTIME_SIDECAR_PATHS.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveInstalledBinaryPath", () => {
|
||||
it("uses the Unix global bin path on non-Windows platforms", () => {
|
||||
expect(resolveInstalledBinaryPath("/tmp/openclaw-prefix", "darwin")).toBe(
|
||||
"/tmp/openclaw-prefix/bin/openclaw",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the Windows npm shim path on win32", () => {
|
||||
expect(resolveInstalledBinaryPath("C:/openclaw-prefix", "win32")).toBe(
|
||||
"C:/openclaw-prefix/openclaw.cmd",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
|
||||
function makeInstalledPackageRoot(): string {
|
||||
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-installed-"));
|
||||
}
|
||||
|
||||
function writePackageFile(root: string, relativePath: string, value: unknown): void {
|
||||
const fullPath = join(root, relativePath);
|
||||
mkdirSync(dirname(fullPath), { recursive: true });
|
||||
writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
it("flags missing mirrored root dependencies required by bundled extensions", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.9",
|
||||
dependencies: {},
|
||||
});
|
||||
writePackageFile(packageRoot, "dist/extensions/slack/package.json", {
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.15.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@slack/web-api"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package is missing mirrored root runtime dependency '@slack/web-api' required by a bundled extension.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts mirrored root dependencies declared in package optionalDependencies", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.9",
|
||||
optionalDependencies: {
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
},
|
||||
});
|
||||
writePackageFile(packageRoot, "dist/extensions/discord/package.json", {
|
||||
optionalDependencies: {
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@discordjs/opus"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags mirrored root dependency version mismatches", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.9",
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.16.0",
|
||||
},
|
||||
});
|
||||
writePackageFile(packageRoot, "dist/extensions/slack/package.json", {
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.15.0",
|
||||
},
|
||||
openclaw: {
|
||||
releaseChecks: {
|
||||
rootDependencyMirrorAllowlist: ["@slack/web-api"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package mirrored dependency '@slack/web-api' version mismatch: root '^7.16.0', extension '^7.15.0'.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags malformed bundled extension manifests instead of silently skipping them", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.9",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist/extensions/slack/package.json"),
|
||||
'{\n "openclaw": { invalid json\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
expect.stringContaining("installed bundled extension manifest invalid: failed to parse"),
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags bundled extension directories that are missing package.json", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.9",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
`installed bundled extension manifest missing: ${join(packageRoot, "dist/extensions/slack/package.json")}.`,
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects bundled extension manifests that are not regular files", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
const outsideManifestRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.9",
|
||||
dependencies: {},
|
||||
});
|
||||
writePackageFile(outsideManifestRoot, "package.json", {
|
||||
dependencies: {
|
||||
"@slack/web-api": "^7.15.0",
|
||||
},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
|
||||
symlinkSync(
|
||||
join(outsideManifestRoot, "package.json"),
|
||||
join(packageRoot, "dist/extensions/slack/package.json"),
|
||||
);
|
||||
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
expect.stringContaining("installed bundled extension manifest invalid: failed to parse"),
|
||||
]);
|
||||
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)[0]).toContain(
|
||||
"manifest must be a regular file",
|
||||
);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
rmSync(outsideManifestRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user