fix(ci): sanitize AI manifest during installer pack

This commit is contained in:
Vincent Koc
2026-08-13 10:53:18 +08:00
parent 812bbd8884
commit 5b01e4302e
2 changed files with 115 additions and 41 deletions
+46 -21
View File
@@ -628,6 +628,7 @@ export async function prepareBundledAiRuntimePackage(
) {
const packageJsonPath = path.join(sourceDir, "package.json");
const aiRuntimePackageJsonPath = path.join(sourceDir, "packages", "ai", "package.json");
const aiRuntimeSourceDir = path.dirname(aiRuntimePackageJsonPath);
const aiRuntimePath = path.join(sourceDir, "node_modules", "@openclaw", "ai");
const aiRuntimeBackupPath = path.join(
sourceDir,
@@ -646,6 +647,8 @@ export async function prepareBundledAiRuntimePackage(
DEFAULT_PACKAGE_PACK_TIMEOUT_MS,
),
}));
const prepareManifest = packageOptions.prepareManifest ?? (async () => false);
const restoreManifest = packageOptions.restoreManifest ?? (async () => false);
const originalPackageJson = await fs.readFile(packageJsonPath, "utf8");
let packageJson: MutableJsonRecord & {
bundleDependencies?: unknown;
@@ -721,26 +724,40 @@ export async function prepareBundledAiRuntimePackage(
};
try {
await runCaptureImpl(
"pnpm",
[
"--dir",
"packages/ai",
"pack",
"--loglevel=error",
"--use-stderr",
"--pack-destination",
outputDir,
],
sourceDir,
{
deferForwardedSignalExit: true,
timeoutMs: resolveTimeoutMs(
"OPENCLAW_DOCKER_PACKAGE_PACK_TIMEOUT_MS",
DEFAULT_PACKAGE_PACK_TIMEOUT_MS,
),
},
);
let packError: Error | undefined;
await prepareManifest(aiRuntimeSourceDir);
try {
await runCaptureImpl(
"pnpm",
[
"--dir",
"packages/ai",
"pack",
"--loglevel=error",
"--use-stderr",
"--pack-destination",
outputDir,
],
sourceDir,
{
deferForwardedSignalExit: true,
timeoutMs: resolveTimeoutMs(
"OPENCLAW_DOCKER_PACKAGE_PACK_TIMEOUT_MS",
DEFAULT_PACKAGE_PACK_TIMEOUT_MS,
),
},
);
} catch (error) {
packError = toErrorObject(error, "AI runtime package failed.");
}
try {
await restoreManifest(aiRuntimeSourceDir);
} catch (restoreError) {
throw packError ? packagePreparationRestoreError(packError, restoreError) : restoreError;
}
if (packError) {
throw packError;
}
packedAiTarballs = (await fs.readdir(outputDir))
.filter(isPackedAiRuntimeTarball)
.map((filename) => path.join(outputDir, filename));
@@ -924,7 +941,15 @@ export async function packOpenClawPackageForDocker(
let cleanupBundledAiRuntime = async () => {};
try {
await cleanPackedOpenClawTarballs(outputPath);
cleanupBundledAiRuntime = await prepareBundledAiRuntime(sourcePath, outputPath, runCaptureImpl);
cleanupBundledAiRuntime = await prepareBundledAiRuntime(
sourcePath,
outputPath,
runCaptureImpl,
{
prepareManifest,
restoreManifest,
},
);
const packArgs =
packTool === "pnpm"
? ["pack", "--silent", "--config.ignore-scripts=true", "--pack-destination", outputPath]
@@ -7,6 +7,10 @@ import { pathToFileURL } from "node:url";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { afterEach, describe, expect, it, vi } from "vitest";
import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "../../../../scripts/lib/bundled-plugin-build-entries.mjs";
import {
preparePackageManifest,
restorePackageManifest,
} from "../../../../scripts/package-manifest.mjs";
import {
buildPackageArtifacts,
packOpenClawPackageForDocker,
@@ -483,8 +487,11 @@ describe("package-openclaw-for-docker", () => {
2,
)}\n`;
const installedAiPath = path.join(sourceDir, "node_modules", "@openclaw", "ai");
const aiPackageJsonPath = path.join(sourceDir, "packages", "ai", "package.json");
const originalAiPackageJson =
'{"name":"@openclaw/ai","version":"2026.6.17","devDependencies":{"@openclaw/normalization-core":"workspace:*"}}\n';
fs.mkdirSync(path.join(sourceDir, "packages", "ai"), { recursive: true });
fs.writeFileSync(path.join(sourceDir, "packages", "ai", "package.json"), "{}\n");
fs.writeFileSync(aiPackageJsonPath, originalAiPackageJson);
fs.mkdirSync(installedAiPath, { recursive: true });
fs.writeFileSync(path.join(installedAiPath, "original-marker"), "workspace package");
fs.writeFileSync(packageJsonPath, originalPackageJson);
@@ -507,6 +514,9 @@ describe("package-openclaw-for-docker", () => {
command: "pnpm",
cwd: sourceDir,
});
expect(
JSON.parse(fs.readFileSync(aiPackageJsonPath, "utf8")).devDependencies,
).toBeUndefined();
fs.writeFileSync(path.join(outputDir, "openclaw-ai-2026.6.17.tgz"), "ai package");
return "";
},
@@ -525,9 +535,12 @@ describe("package-openclaw-for-docker", () => {
);
fs.writeFileSync(path.join(destination, "runtime.js"), "export {};\n");
},
prepareManifest: preparePackageManifest,
restoreManifest: restorePackageManifest,
},
);
expect(fs.readFileSync(aiPackageJsonPath, "utf8")).toBe(originalAiPackageJson);
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
bundleDependencies: string[];
dependencies: Record<string, string>;
@@ -565,27 +578,47 @@ describe("package-openclaw-for-docker", () => {
dependencies: { "@openclaw/ai": "workspace:*" },
name: "openclaw",
})}\n`;
const aiPackageJsonPath = path.join(sourceDir, "packages", "ai", "package.json");
const originalAiPackageJson =
'{"name":"@openclaw/ai","devDependencies":{"@openclaw/normalization-core":"workspace:*"}}\n';
fs.mkdirSync(path.join(sourceDir, "packages", "ai"), { recursive: true });
fs.writeFileSync(
path.join(sourceDir, "packages", "ai", "package.json"),
'{"name":"@openclaw/ai"}\n',
);
fs.writeFileSync(aiPackageJsonPath, originalAiPackageJson);
fs.writeFileSync(packageJsonPath, originalPackageJson);
let stderr = "";
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
stderr += String(chunk);
return true;
});
const packError = new Error("AI pack failed");
try {
await expect(prepareBundledAiRuntimePackage(sourceDir, outputDir)).rejects.toThrow(
"pnpm --dir packages/ai pack --loglevel=error --use-stderr",
);
expect(stderr).toContain("ERR_PNPM_PACKAGE_VERSION_NOT_FOUND");
expect(fs.readFileSync(packageJsonPath, "utf8")).toBe(originalPackageJson);
} finally {
stderrWrite.mockRestore();
}
await expect(
prepareBundledAiRuntimePackage(
sourceDir,
outputDir,
async () => {
throw packError;
},
{
prepareManifest: preparePackageManifest,
restoreManifest: restorePackageManifest,
},
),
).rejects.toBe(packError);
expect(fs.readFileSync(aiPackageJsonPath, "utf8")).toBe(originalAiPackageJson);
expect(fs.readFileSync(packageJsonPath, "utf8")).toBe(originalPackageJson);
const restoreError = new Error("AI manifest restore failed");
await expect(
prepareBundledAiRuntimePackage(
sourceDir,
outputDir,
async () => {
throw packError;
},
{
prepareManifest: preparePackageManifest,
restoreManifest: async (cwd) => {
await restorePackageManifest(cwd);
throw restoreError;
},
},
),
).rejects.toMatchObject({ cause: packError, errors: [packError, restoreError] });
});
it("reuses the source manifest lifecycle for ignore-scripts package artifacts", async () => {
@@ -605,17 +638,31 @@ describe("package-openclaw-for-docker", () => {
null,
2,
)}\n`;
const aiPackageJsonPath = path.join(sourceDir, "packages", "ai", "package.json");
const originalAiPackageJson =
'{"name":"@openclaw/ai","devDependencies":{"@openclaw/normalization-core":"workspace:*"}}\n';
fs.mkdirSync(scriptsDir);
fs.mkdirSync(path.dirname(aiPackageJsonPath), { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), "scripts", "package-manifest.mjs"),
path.join(scriptsDir, "package-manifest.mjs"),
);
fs.writeFileSync(packageJsonPath, originalPackageJson);
fs.writeFileSync(aiPackageJsonPath, originalAiPackageJson);
try {
const tarball = await packOpenClawPackageForDocker(sourceDir, outputDir, {
...skipDocsMapLifecycle,
prepareBundledAiRuntime: skipBundledAiRuntime,
prepareBundledAiRuntime: async (_source, _output, _runCapture, options) => {
const aiDir = path.dirname(aiPackageJsonPath);
expect(options).toBeDefined();
await options?.prepareManifest?.(aiDir);
expect(
JSON.parse(fs.readFileSync(aiPackageJsonPath, "utf8")).devDependencies,
).toBeUndefined();
await options?.restoreManifest?.(aiDir);
return async () => {};
},
prepareChangelog: async () => {},
restoreChangelog: async () => {},
runCaptureImpl: async () => {
@@ -623,6 +670,7 @@ describe("package-openclaw-for-docker", () => {
devDependencies?: Record<string, string>;
};
expect(packageJson.devDependencies).toEqual({ vitest: "4.1.10" });
expect(fs.readFileSync(aiPackageJsonPath, "utf8")).toBe(originalAiPackageJson);
const packedPath = path.join(outputDir, "openclaw-2026.8.1.tgz");
fs.writeFileSync(packedPath, "package");
return `${path.basename(packedPath)}\n`;
@@ -631,6 +679,7 @@ describe("package-openclaw-for-docker", () => {
expect(tarball).toBe(path.join(outputDir, "openclaw-2026.8.1.tgz"));
expect(fs.readFileSync(packageJsonPath, "utf8")).toBe(originalPackageJson);
expect(fs.readFileSync(aiPackageJsonPath, "utf8")).toBe(originalAiPackageJson);
expect(
fs.existsSync(
path.join(sourceDir, ".artifacts", "package-manifest", "package.json.prepack-backup"),