diff --git a/scripts/package-openclaw-for-docker.mts b/scripts/package-openclaw-for-docker.mts index 21defce661f1..9347666825b7 100644 --- a/scripts/package-openclaw-for-docker.mts +++ b/scripts/package-openclaw-for-docker.mts @@ -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] diff --git a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts index 38efd090f6fb..fb279da38110 100644 --- a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts @@ -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; @@ -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; }; 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"),