mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 02:00:26 +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:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user