mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-29 16:54:30 +00:00
fix(release): preserve shipped channel surfaces in npm tar (#52913)
* fix(channels): ship official channel catalog (#52838) * fix(release): keep shipped bundles in npm tar (#52838) * build(release): fix rebased release-check helpers (#52838)
This commit is contained in:
@@ -681,7 +681,7 @@
|
||||
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
||||
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
|
||||
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
|
||||
"release:check": "pnpm config:docs:check && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && node --import tsx scripts/release-check.ts",
|
||||
"release:check": "pnpm config:docs:check && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts",
|
||||
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
|
||||
"release:plugins:npm:check": "node --import tsx scripts/plugin-npm-release-check.ts",
|
||||
"release:plugins:npm:plan": "node --import tsx scripts/plugin-npm-release-plan.ts",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export function rewritePackageExtensions(entries: unknown): string[] | undefined;
|
||||
|
||||
export function copyBundledPluginMetadata(params?: { repoRoot?: string }): void;
|
||||
export function copyBundledPluginMetadata(params?: {
|
||||
repoRoot?: string;
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void;
|
||||
|
||||
19
scripts/lib/bundled-plugin-build-entries.d.mts
Normal file
19
scripts/lib/bundled-plugin-build-entries.d.mts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type BundledPluginBuildEntry = {
|
||||
id: string;
|
||||
hasPackageJson: boolean;
|
||||
packageJson: unknown;
|
||||
sourceEntries: string[];
|
||||
};
|
||||
|
||||
export type BundledPluginBuildEntryParams = {
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
export function collectBundledPluginBuildEntries(
|
||||
params?: BundledPluginBuildEntryParams,
|
||||
): BundledPluginBuildEntry[];
|
||||
export function listBundledPluginBuildEntries(
|
||||
params?: BundledPluginBuildEntryParams,
|
||||
): Record<string, string>;
|
||||
export function listBundledPluginPackArtifacts(params?: BundledPluginBuildEntryParams): string[];
|
||||
19
scripts/lib/bundled-plugin-build-entries.d.ts
vendored
Normal file
19
scripts/lib/bundled-plugin-build-entries.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
export type BundledPluginBuildEntry = {
|
||||
id: string;
|
||||
hasPackageJson: boolean;
|
||||
packageJson: unknown;
|
||||
sourceEntries: string[];
|
||||
};
|
||||
|
||||
export type BundledPluginBuildEntryParams = {
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
export function collectBundledPluginBuildEntries(
|
||||
params?: BundledPluginBuildEntryParams,
|
||||
): BundledPluginBuildEntry[];
|
||||
export function listBundledPluginBuildEntries(
|
||||
params?: BundledPluginBuildEntryParams,
|
||||
): Record<string, string>;
|
||||
export function listBundledPluginPackArtifacts(params?: BundledPluginBuildEntryParams): string[];
|
||||
@@ -23,7 +23,9 @@ export function isOptionalBundledCluster(cluster) {
|
||||
}
|
||||
|
||||
export function shouldIncludeOptionalBundledClusters(env = process.env) {
|
||||
return env[OPTIONAL_BUNDLED_BUILD_ENV] === "1";
|
||||
// Release artifacts should preserve the last shipped upgrade surface by
|
||||
// default. Specific size-sensitive lanes can still opt out explicitly.
|
||||
return env[OPTIONAL_BUNDLED_BUILD_ENV] !== "0";
|
||||
}
|
||||
|
||||
export function hasReleasedBundledInstall(packageJson) {
|
||||
|
||||
@@ -26,13 +26,14 @@ const requiredPathGroups = [
|
||||
"dist/plugin-sdk/compat.js",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
"dist/build-info.json",
|
||||
"dist/channel-catalog.json",
|
||||
"dist/control-ui/index.html",
|
||||
];
|
||||
const forbiddenPrefixes = ["dist-runtime/", "dist/OpenClaw.app/"];
|
||||
// 2026.3.12 ballooned to ~213.6 MiB unpacked and correlated with low-memory
|
||||
// startup/doctor OOM reports. Keep enough headroom for the current pack while
|
||||
// failing fast if duplicate/shim content sneaks back into the release artifact.
|
||||
const npmPackUnpackedSizeBudgetBytes = 160 * 1024 * 1024;
|
||||
// startup/doctor OOM reports. Keep enough headroom for the current pack with
|
||||
// restored bundled upgrade surfaces while still catching regressions quickly.
|
||||
const npmPackUnpackedSizeBudgetBytes = 176 * 1024 * 1024;
|
||||
const appcastPath = resolve("appcast.xml");
|
||||
const laneBuildMin = 1_000_000_000;
|
||||
const laneFloorAdoptionDateKey = 20260227;
|
||||
@@ -79,6 +80,18 @@ function runPackDry(): PackResult[] {
|
||||
return JSON.parse(raw) as PackResult[];
|
||||
}
|
||||
|
||||
export function collectMissingPackPaths(paths: Iterable<string>): string[] {
|
||||
const available = new Set(paths);
|
||||
return requiredPathGroups
|
||||
.flatMap((group) => {
|
||||
if (Array.isArray(group)) {
|
||||
return group.some((path) => available.has(path)) ? [] : [group.join(" or ")];
|
||||
}
|
||||
return available.has(group) ? [] : [group];
|
||||
})
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
|
||||
const isAllowedBundledPluginNodeModulesPath = (path: string) =>
|
||||
/^dist\/extensions\/[^/]+\/node_modules\//.test(path);
|
||||
|
||||
@@ -3,10 +3,12 @@ import { copyBundledPluginMetadata } from "./copy-bundled-plugin-metadata.mjs";
|
||||
import { copyPluginSdkRootAlias } from "./copy-plugin-sdk-root-alias.mjs";
|
||||
import { stageBundledPluginRuntimeDeps } from "./stage-bundled-plugin-runtime-deps.mjs";
|
||||
import { stageBundledPluginRuntime } from "./stage-bundled-plugin-runtime.mjs";
|
||||
import { writeOfficialChannelCatalog } from "./write-official-channel-catalog.mjs";
|
||||
|
||||
export function runRuntimePostBuild(params = {}) {
|
||||
copyPluginSdkRootAlias(params);
|
||||
copyBundledPluginMetadata(params);
|
||||
writeOfficialChannelCatalog(params);
|
||||
stageBundledPluginRuntimeDeps(params);
|
||||
stageBundledPluginRuntime(params);
|
||||
}
|
||||
|
||||
19
scripts/write-official-channel-catalog.d.mts
Normal file
19
scripts/write-official-channel-catalog.d.mts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH: "dist/channel-catalog.json";
|
||||
|
||||
export function buildOfficialChannelCatalog(params?: { repoRoot?: string; cwd?: string }): {
|
||||
entries: Array<{
|
||||
name: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
openclaw: {
|
||||
channel: Record<string, unknown>;
|
||||
install: {
|
||||
npmSpec: string;
|
||||
localPath?: string;
|
||||
defaultChoice?: "npm" | "local";
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export function writeOfficialChannelCatalog(params?: { repoRoot?: string; cwd?: string }): void;
|
||||
104
scripts/write-official-channel-catalog.mjs
Normal file
104
scripts/write-official-channel-catalog.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
|
||||
|
||||
export const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH = "dist/channel-catalog.json";
|
||||
|
||||
function isRecord(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function trimString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function toCatalogInstall(value, packageName) {
|
||||
const install = isRecord(value) ? value : {};
|
||||
const npmSpec = trimString(install.npmSpec) || packageName;
|
||||
if (!npmSpec) {
|
||||
return null;
|
||||
}
|
||||
const localPath = trimString(install.localPath);
|
||||
const defaultChoice = trimString(install.defaultChoice);
|
||||
return {
|
||||
npmSpec,
|
||||
...(localPath ? { localPath } : {}),
|
||||
...(defaultChoice === "npm" || defaultChoice === "local" ? { defaultChoice } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildCatalogEntry(packageJson) {
|
||||
if (!isRecord(packageJson)) {
|
||||
return null;
|
||||
}
|
||||
const packageName = trimString(packageJson.name);
|
||||
const manifest = isRecord(packageJson.openclaw) ? packageJson.openclaw : null;
|
||||
const release = manifest && isRecord(manifest.release) ? manifest.release : null;
|
||||
const channel = manifest && isRecord(manifest.channel) ? manifest.channel : null;
|
||||
if (!packageName || !channel || release?.publishToNpm !== true) {
|
||||
return null;
|
||||
}
|
||||
const install = toCatalogInstall(manifest.install, packageName);
|
||||
if (!install) {
|
||||
return null;
|
||||
}
|
||||
const version = trimString(packageJson.version);
|
||||
const description = trimString(packageJson.description);
|
||||
return {
|
||||
name: packageName,
|
||||
...(version ? { version } : {}),
|
||||
...(description ? { description } : {}),
|
||||
openclaw: {
|
||||
channel,
|
||||
install,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOfficialChannelCatalog(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
const entries = [];
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return { entries };
|
||||
}
|
||||
|
||||
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!dirent.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const packageJsonPath = path.join(extensionsRoot, dirent.name, "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
const entry = buildCatalogEntry(packageJson);
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid package metadata and keep generating the rest of the catalog.
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((left, right) => {
|
||||
const leftId = trimString(left.openclaw?.channel?.id) || left.name;
|
||||
const rightId = trimString(right.openclaw?.channel?.id) || right.name;
|
||||
return leftId.localeCompare(rightId);
|
||||
});
|
||||
|
||||
return { entries };
|
||||
}
|
||||
|
||||
export function writeOfficialChannelCatalog(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const outputPath = path.join(repoRoot, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH);
|
||||
const catalog = buildOfficialChannelCatalog({ repoRoot });
|
||||
writeTextFileIfChanged(outputPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
writeOfficialChannelCatalog();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
||||
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
|
||||
import { discoverOpenClawPlugins } from "../../plugins/discovery.js";
|
||||
import { loadPluginManifest } from "../../plugins/manifest.js";
|
||||
@@ -40,6 +41,7 @@ export type ChannelPluginCatalogEntry = {
|
||||
type CatalogOptions = {
|
||||
workspaceDir?: string;
|
||||
catalogPaths?: string[];
|
||||
officialCatalogPaths?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
@@ -57,6 +59,7 @@ type ExternalCatalogEntry = {
|
||||
} & Partial<Record<ManifestKey, OpenClawPackageManifest>>;
|
||||
|
||||
const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALOG_PATHS"];
|
||||
const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH = path.join("dist", "channel-catalog.json");
|
||||
|
||||
type ManifestKey = typeof MANIFEST_KEY;
|
||||
|
||||
@@ -110,16 +113,20 @@ function resolveExternalCatalogPaths(options: CatalogOptions): string[] {
|
||||
}
|
||||
|
||||
function loadExternalCatalogEntries(options: CatalogOptions): ExternalCatalogEntry[] {
|
||||
const paths = resolveExternalCatalogPaths(options);
|
||||
const env = options.env ?? process.env;
|
||||
const paths = resolveExternalCatalogPaths(options).map((rawPath) =>
|
||||
resolveUserPath(rawPath, options.env ?? process.env),
|
||||
);
|
||||
return loadCatalogEntriesFromPaths(paths);
|
||||
}
|
||||
|
||||
function loadCatalogEntriesFromPaths(paths: Iterable<string>): ExternalCatalogEntry[] {
|
||||
const entries: ExternalCatalogEntry[] = [];
|
||||
for (const rawPath of paths) {
|
||||
const resolved = resolveUserPath(rawPath, env);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
for (const resolvedPath of paths) {
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(fs.readFileSync(resolved, "utf-8")) as unknown;
|
||||
const payload = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
||||
entries.push(...parseCatalogEntries(payload));
|
||||
} catch {
|
||||
// Ignore invalid catalog files.
|
||||
@@ -128,6 +135,37 @@ function loadExternalCatalogEntries(options: CatalogOptions): ExternalCatalogEnt
|
||||
return entries;
|
||||
}
|
||||
|
||||
function resolveOfficialCatalogPaths(options: CatalogOptions): string[] {
|
||||
if (options.officialCatalogPaths && options.officialCatalogPaths.length > 0) {
|
||||
return options.officialCatalogPaths.map((entry) => entry.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
const packageRoots = [
|
||||
resolveOpenClawPackageRootSync({ cwd: process.cwd() }),
|
||||
resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url }),
|
||||
].filter((entry, index, all): entry is string => Boolean(entry) && all.indexOf(entry) === index);
|
||||
|
||||
const candidates = packageRoots.map((packageRoot) =>
|
||||
path.join(packageRoot, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH),
|
||||
);
|
||||
|
||||
try {
|
||||
const execDir = path.dirname(process.execPath);
|
||||
candidates.push(path.join(execDir, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH));
|
||||
candidates.push(path.join(execDir, "channel-catalog.json"));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return candidates.filter((entry, index, all) => entry && all.indexOf(entry) === index);
|
||||
}
|
||||
|
||||
function loadOfficialCatalogEntries(options: CatalogOptions): ChannelPluginCatalogEntry[] {
|
||||
return loadCatalogEntriesFromPaths(resolveOfficialCatalogPaths(options))
|
||||
.map((entry) => buildExternalCatalogEntry(entry))
|
||||
.filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry));
|
||||
}
|
||||
|
||||
function toChannelMeta(params: {
|
||||
channel: NonNullable<OpenClawPackageManifest["channel"]>;
|
||||
id: string;
|
||||
@@ -362,6 +400,14 @@ export function listChannelPluginCatalogEntries(
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of loadOfficialCatalogEntries(options)) {
|
||||
const priority = ORIGIN_PRIORITY.bundled ?? 99;
|
||||
const existing = resolved.get(entry.id);
|
||||
if (!existing || priority < existing.priority) {
|
||||
resolved.set(entry.id, { entry, priority });
|
||||
}
|
||||
}
|
||||
|
||||
const externalEntries = loadExternalCatalogEntries(options)
|
||||
.map((entry) => buildExternalCatalogEntry(entry))
|
||||
.filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry));
|
||||
|
||||
@@ -325,6 +325,46 @@ describe("channel plugin catalog", () => {
|
||||
expect(entry?.install.npmSpec).toBe("@openclaw/whatsapp");
|
||||
expect(entry?.pluginId).toBe("whatsapp");
|
||||
});
|
||||
|
||||
it("includes shipped official channel catalog entries when bundled metadata is omitted", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-official-catalog-"));
|
||||
const catalogPath = path.join(dir, "channel-catalog.json");
|
||||
fs.writeFileSync(
|
||||
catalogPath,
|
||||
JSON.stringify({
|
||||
entries: [
|
||||
{
|
||||
name: "@openclaw/whatsapp",
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp (QR link)",
|
||||
detailLabel: "WhatsApp Web",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "works with your own number; recommend a separate phone + eSIM.",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const entry = listChannelPluginCatalogEntries({
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
|
||||
},
|
||||
officialCatalogPaths: [catalogPath],
|
||||
}).find((item) => item.id === "whatsapp");
|
||||
|
||||
expect(entry?.install.npmSpec).toBe("@openclaw/whatsapp");
|
||||
expect(entry?.pluginId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
const emptyRegistry = createTestRegistry([]);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../scripts/copy-bundled-plugin-metadata.mjs";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const includeOptionalEnv = { OPENCLAW_INCLUDE_OPTIONAL_BUNDLED: "1" } as const;
|
||||
const excludeOptionalEnv = { OPENCLAW_INCLUDE_OPTIONAL_BUNDLED: "0" } as const;
|
||||
const copyBundledPluginMetadataWithEnv = copyBundledPluginMetadata as (params?: {
|
||||
repoRoot?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -60,7 +60,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
openclaw: { extensions: ["./index.ts"] },
|
||||
});
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: includeOptionalEnv });
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
expect(
|
||||
fs.existsSync(path.join(repoRoot, "dist", "extensions", "acpx", "openclaw.plugin.json")),
|
||||
@@ -131,7 +131,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
fs.mkdirSync(staleNodeModulesSkillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleNodeModulesSkillDir, "stale.txt"), "stale\n", "utf8");
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: includeOptionalEnv });
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
const copiedSkillDir = path.join(
|
||||
repoRoot,
|
||||
@@ -174,7 +174,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
openclaw: { extensions: ["./index.ts"] },
|
||||
});
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: includeOptionalEnv });
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
@@ -227,7 +227,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
const staleNodeModulesDir = path.join(repoRoot, "dist", "extensions", "tlon", "node_modules");
|
||||
fs.mkdirSync(staleNodeModulesDir, { recursive: true });
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: includeOptionalEnv });
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
const bundledManifest = JSON.parse(
|
||||
fs.readFileSync(
|
||||
@@ -269,7 +269,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: includeOptionalEnv });
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
} finally {
|
||||
cpSyncSpy.mockRestore();
|
||||
}
|
||||
@@ -319,7 +319,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
});
|
||||
fs.mkdirSync(path.join(repoRoot, "extensions"), { recursive: true });
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: includeOptionalEnv });
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "removed-plugin"))).toBe(false);
|
||||
});
|
||||
@@ -339,12 +339,12 @@ describe("copyBundledPluginMetadata", () => {
|
||||
name: "@openclaw/google-gemini-cli-auth",
|
||||
});
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: includeOptionalEnv });
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
expect(fs.existsSync(staleDistDir)).toBe(false);
|
||||
});
|
||||
|
||||
it("skips metadata for optional bundled clusters unless explicitly enabled", () => {
|
||||
it("skips metadata for optional bundled clusters only when explicitly disabled", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-bundled-plugin-optional-skip-");
|
||||
const pluginDir = path.join(repoRoot, "extensions", "acpx");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
@@ -357,7 +357,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
openclaw: { extensions: ["./index.ts"] },
|
||||
});
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: {} });
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: excludeOptionalEnv });
|
||||
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "acpx"))).toBe(false);
|
||||
});
|
||||
|
||||
146
test/official-channel-catalog.test.ts
Normal file
146
test/official-channel-catalog.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildOfficialChannelCatalog,
|
||||
OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH,
|
||||
writeOfficialChannelCatalog,
|
||||
} from "../scripts/write-official-channel-catalog.mjs";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeRepoRoot(prefix: string): string {
|
||||
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
tempDirs.push(repoRoot);
|
||||
return repoRoot;
|
||||
}
|
||||
|
||||
function writeJson(filePath: string, value: unknown): void {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0, tempDirs.length)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("buildOfficialChannelCatalog", () => {
|
||||
it("includes publishable official channel plugins and skips non-publishable entries", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-official-channel-catalog-");
|
||||
writeJson(path.join(repoRoot, "extensions", "whatsapp", "package.json"), {
|
||||
name: "@openclaw/whatsapp",
|
||||
version: "2026.3.23",
|
||||
description: "OpenClaw WhatsApp channel plugin",
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp (QR link)",
|
||||
detailLabel: "WhatsApp Web",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "works with your own number; recommend a separate phone + eSIM.",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
localPath: "extensions/whatsapp",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJson(path.join(repoRoot, "extensions", "local-only", "package.json"), {
|
||||
name: "@openclaw/local-only",
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "local-only",
|
||||
label: "Local Only",
|
||||
selectionLabel: "Local Only",
|
||||
docsPath: "/channels/local-only",
|
||||
blurb: "dev only",
|
||||
},
|
||||
install: {
|
||||
localPath: "extensions/local-only",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(buildOfficialChannelCatalog({ repoRoot })).toEqual({
|
||||
entries: [
|
||||
{
|
||||
name: "@openclaw/whatsapp",
|
||||
version: "2026.3.23",
|
||||
description: "OpenClaw WhatsApp channel plugin",
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp (QR link)",
|
||||
detailLabel: "WhatsApp Web",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "works with your own number; recommend a separate phone + eSIM.",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
localPath: "extensions/whatsapp",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("writes the official catalog under dist", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-official-channel-catalog-write-");
|
||||
writeJson(path.join(repoRoot, "extensions", "whatsapp", "package.json"), {
|
||||
name: "@openclaw/whatsapp",
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "wa",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
writeOfficialChannelCatalog({ repoRoot });
|
||||
|
||||
const outputPath = path.join(repoRoot, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
expect(JSON.parse(fs.readFileSync(outputPath, "utf8"))).toEqual({
|
||||
entries: [
|
||||
{
|
||||
name: "@openclaw/whatsapp",
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "wa",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
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,
|
||||
collectForbiddenPackPaths,
|
||||
collectMissingPackPaths,
|
||||
collectPackUnpackedSizeErrors,
|
||||
} from "../scripts/release-check.ts";
|
||||
|
||||
@@ -14,6 +17,9 @@ function makePackResult(filename: string, unpackedSize: number) {
|
||||
return { filename, unpackedSize };
|
||||
}
|
||||
|
||||
const requiredPluginSdkPackPaths = [...listPluginSdkDistArtifacts(), "dist/plugin-sdk/compat.js"];
|
||||
const requiredBundledPluginPackPaths = listBundledPluginPackArtifacts();
|
||||
|
||||
describe("collectAppcastSparkleVersionErrors", () => {
|
||||
it("accepts legacy 9-digit calver builds before lane-floor cutover", () => {
|
||||
const xml = `<rss><channel>${makeItem("2026.2.26", "202602260")}</channel></rss>`;
|
||||
@@ -115,6 +121,46 @@ describe("collectForbiddenPackPaths", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectMissingPackPaths", () => {
|
||||
it("requires the shipped channel catalog, control ui, and optional bundled metadata", () => {
|
||||
const missing = collectMissingPackPaths([
|
||||
"dist/index.js",
|
||||
"dist/entry.js",
|
||||
"dist/plugin-sdk/compat.js",
|
||||
"dist/plugin-sdk/index.js",
|
||||
"dist/plugin-sdk/index.d.ts",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
"dist/build-info.json",
|
||||
]);
|
||||
|
||||
expect(missing).toEqual(
|
||||
expect.arrayContaining([
|
||||
"dist/channel-catalog.json",
|
||||
"dist/control-ui/index.html",
|
||||
"dist/extensions/matrix/openclaw.plugin.json",
|
||||
"dist/extensions/matrix/package.json",
|
||||
"dist/extensions/whatsapp/openclaw.plugin.json",
|
||||
"dist/extensions/whatsapp/package.json",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts the shipped upgrade surface when optional bundled metadata is present", () => {
|
||||
expect(
|
||||
collectMissingPackPaths([
|
||||
"dist/index.js",
|
||||
"dist/entry.js",
|
||||
"dist/control-ui/index.html",
|
||||
...requiredBundledPluginPackPaths,
|
||||
...requiredPluginSdkPackPaths,
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
"dist/build-info.json",
|
||||
"dist/channel-catalog.json",
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPackUnpackedSizeErrors", () => {
|
||||
it("accepts pack results within the unpacked size budget", () => {
|
||||
expect(
|
||||
@@ -126,7 +172,7 @@ describe("collectPackUnpackedSizeErrors", () => {
|
||||
expect(
|
||||
collectPackUnpackedSizeErrors([makePackResult("openclaw-2026.3.12.tgz", 224_002_564)]),
|
||||
).toEqual([
|
||||
"openclaw-2026.3.12.tgz unpackedSize 224002564 bytes (213.6 MiB) exceeds budget 167772160 bytes (160.0 MiB). Investigate duplicate channel shims, copied extension trees, or other accidental pack bloat before release.",
|
||||
"openclaw-2026.3.12.tgz unpackedSize 224002564 bytes (213.6 MiB) exceeds budget 184549376 bytes (176.0 MiB). Investigate duplicate channel shims, copied extension trees, or other accidental pack bloat before release.",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user