fix(docker): prune external plugin dist (#77547)

This commit is contained in:
Vincent Koc
2026-05-04 15:11:14 -07:00
committed by GitHub
parent 8ee08b2b77
commit 7e229f0d3d
6 changed files with 119 additions and 0 deletions

View File

@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
- Docker: prune package-excluded plugin dist directories from runtime images unless the build explicitly opts that plugin in, so official external plugins such as Feishu stay install-on-demand instead of shipping partial metadata without compiled runtime output. Fixes #77424. Thanks @vincentkoc.
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.

View File

@@ -124,6 +124,7 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
node scripts/postinstall-bundled-plugins.mjs && \
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
node scripts/check-package-dist-imports.mjs /app

View File

@@ -0,0 +1,6 @@
export function parseDockerPluginKeepList(value: unknown): Set<string>;
export function pruneDockerPluginDist(params?: {
cwd?: string;
repoRoot?: string;
env?: NodeJS.ProcessEnv;
}): string[];

View File

@@ -0,0 +1,52 @@
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { collectRootPackageExcludedExtensionDirs } from "./lib/bundled-plugin-build-entries.mjs";
import { removePathIfExists } from "./runtime-postbuild-shared.mjs";
function parsePluginList(value) {
if (typeof value !== "string") {
return new Set();
}
return new Set(
value
.split(/[\s,]+/u)
.map((entry) => entry.trim())
.filter(Boolean),
);
}
export function parseDockerPluginKeepList(value) {
return parsePluginList(value);
}
export function pruneDockerPluginDist(params = {}) {
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
const env = params.env ?? process.env;
const keepPluginIds = parseDockerPluginKeepList(env.OPENCLAW_EXTENSIONS);
const excludedPluginIds = collectRootPackageExcludedExtensionDirs({ cwd: repoRoot });
const removed = [];
for (const pluginId of [...excludedPluginIds].toSorted((left, right) =>
left.localeCompare(right),
)) {
if (keepPluginIds.has(pluginId)) {
continue;
}
for (const root of ["dist", "dist-runtime"]) {
const pluginDistDir = path.join(repoRoot, root, "extensions", pluginId);
if (!fs.existsSync(pluginDistDir)) {
continue;
}
removePathIfExists(pluginDistDir);
removed.push(path.relative(repoRoot, pluginDistDir).replaceAll("\\", "/"));
}
}
return removed;
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
pruneDockerPluginDist();
}

View File

@@ -111,6 +111,9 @@ describe("Dockerfile", () => {
expect(dockerfile).toContain("pnpm-workspace.runtime.yaml");
expect(dockerfile).toContain(" - ui\\n");
expect(dockerfile).toContain("CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod");
expect(dockerfile).toContain(
'OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" node scripts/prune-docker-plugin-dist.mjs',
);
expect(dockerfile).toContain("prune must not rediscover unrelated workspaces");
expect(dockerfile).not.toContain(
`npm install --prefix "${BUNDLED_PLUGIN_ROOT_DIR}/$ext" --omit=dev --silent`,

View File

@@ -0,0 +1,56 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
parseDockerPluginKeepList,
pruneDockerPluginDist,
} from "../../scripts/prune-docker-plugin-dist.mjs";
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "../../test/helpers/temp-repo.js";
const tempDirs: string[] = [];
function makeRepoRoot(prefix: string): string {
return makeTempRepoRoot(tempDirs, prefix);
}
function writeDistPluginFile(repoRoot: string, root: "dist" | "dist-runtime", pluginId: string) {
const pluginDir = path.join(repoRoot, root, "extensions", pluginId);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(path.join(pluginDir, "openclaw.plugin.json"), "{}\n", "utf8");
}
afterEach(() => {
cleanupTempDirs(tempDirs);
});
describe("pruneDockerPluginDist", () => {
it("parses space and comma separated Docker plugin keep lists", () => {
expect([...parseDockerPluginKeepList("diagnostics-otel feishu,discord")]).toEqual([
"diagnostics-otel",
"feishu",
"discord",
]);
});
it("removes package-excluded plugin dist unless Docker explicitly opts it in", () => {
const repoRoot = makeRepoRoot("openclaw-docker-plugin-dist-");
writeJsonFile(path.join(repoRoot, "package.json"), {
files: ["dist/**", "!dist/extensions/diagnostics-otel/**", "!dist/extensions/feishu/**"],
});
writeDistPluginFile(repoRoot, "dist", "diagnostics-otel");
writeDistPluginFile(repoRoot, "dist", "feishu");
writeDistPluginFile(repoRoot, "dist-runtime", "feishu");
writeDistPluginFile(repoRoot, "dist", "telegram");
const removed = pruneDockerPluginDist({
repoRoot,
env: { OPENCLAW_EXTENSIONS: "diagnostics-otel" } as NodeJS.ProcessEnv,
});
expect(removed).toEqual(["dist/extensions/feishu", "dist-runtime/extensions/feishu"]);
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "diagnostics-otel"))).toBe(true);
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "feishu"))).toBe(false);
expect(fs.existsSync(path.join(repoRoot, "dist-runtime", "extensions", "feishu"))).toBe(false);
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "telegram"))).toBe(true);
});
});