mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ci): sanitize AI manifest during installer pack
This commit is contained in:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user