Skip to content

Commit b33cd8c

Browse files
committed
fix(release): package legacy candidates without AI workspace
1 parent 45f561a commit b33cd8c

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

scripts/package-openclaw-for-docker.mjs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export async function prepareBundledAiRuntimePackage(
435435
options = {},
436436
) {
437437
const packageJsonPath = path.join(sourceDir, "package.json");
438+
const aiRuntimePackageJsonPath = path.join(sourceDir, "packages", "ai", "package.json");
438439
const aiRuntimePath = path.join(sourceDir, "node_modules", "@openclaw", "ai");
439440
const aiRuntimeBackupPath = path.join(
440441
sourceDir,
@@ -453,7 +454,24 @@ export async function prepareBundledAiRuntimePackage(
453454
} catch (error) {
454455
throw new Error(`failed to parse ${packageJsonPath}`, { cause: error });
455456
}
456-
if (typeof packageJson.dependencies?.[AI_RUNTIME_PACKAGE] !== "string") {
457+
const aiRuntimeDependency = packageJson.dependencies?.[AI_RUNTIME_PACKAGE];
458+
let hasAiRuntimeWorkspace = false;
459+
try {
460+
await fs.access(aiRuntimePackageJsonPath);
461+
hasAiRuntimeWorkspace = true;
462+
} catch (error) {
463+
if (error?.code !== "ENOENT") {
464+
throw error;
465+
}
466+
}
467+
// Release checks can package refs from before the AI runtime was split into a workspace package.
468+
if (!hasAiRuntimeWorkspace && aiRuntimeDependency === undefined) {
469+
return async () => {};
470+
}
471+
if (!hasAiRuntimeWorkspace) {
472+
throw new Error("@openclaw/ai dependency requires the packages/ai workspace");
473+
}
474+
if (typeof aiRuntimeDependency !== "string") {
457475
throw new Error("root package.json must declare @openclaw/ai as a dependency");
458476
}
459477

test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ describe("package-openclaw-for-docker", () => {
234234
)}\n`;
235235
const installedAiPath = path.join(sourceDir, "node_modules", "@openclaw", "ai");
236236
fs.mkdirSync(path.join(sourceDir, "packages", "ai"), { recursive: true });
237+
fs.writeFileSync(path.join(sourceDir, "packages", "ai", "package.json"), "{}\n");
237238
fs.mkdirSync(installedAiPath, { recursive: true });
238239
fs.writeFileSync(path.join(installedAiPath, "original-marker"), "workspace package");
239240
fs.writeFileSync(packageJsonPath, originalPackageJson);
@@ -291,6 +292,67 @@ describe("package-openclaw-for-docker", () => {
291292
}
292293
});
293294

295+
it("leaves pre-AI-workspace package sources unchanged", async () => {
296+
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-legacy-source-"));
297+
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-legacy-output-"));
298+
const packageJsonPath = path.join(sourceDir, "package.json");
299+
const originalPackageJson = `${JSON.stringify({
300+
dependencies: { "dep-a": "1.2.3" },
301+
name: "openclaw",
302+
version: "2026.7.1",
303+
})}\n`;
304+
fs.writeFileSync(packageJsonPath, originalPackageJson);
305+
const runCapture = vi.fn();
306+
307+
try {
308+
const cleanup = await prepareBundledAiRuntimePackage(sourceDir, outputDir, runCapture);
309+
310+
expect(runCapture).not.toHaveBeenCalled();
311+
expect(fs.readFileSync(packageJsonPath, "utf8")).toBe(originalPackageJson);
312+
await cleanup();
313+
} finally {
314+
fs.rmSync(sourceDir, { recursive: true, force: true });
315+
fs.rmSync(outputDir, { recursive: true, force: true });
316+
}
317+
});
318+
319+
it("rejects incomplete AI workspace package sources", async () => {
320+
const cases = [
321+
{
322+
dependencies: { "@openclaw/ai": "workspace:*" },
323+
expected: "@openclaw/ai dependency requires the packages/ai workspace",
324+
withWorkspace: false,
325+
},
326+
{
327+
dependencies: {},
328+
expected: "root package.json must declare @openclaw/ai as a dependency",
329+
withWorkspace: true,
330+
},
331+
];
332+
333+
for (const testCase of cases) {
334+
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-invalid-source-"));
335+
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-invalid-output-"));
336+
fs.writeFileSync(
337+
path.join(sourceDir, "package.json"),
338+
`${JSON.stringify({ dependencies: testCase.dependencies, name: "openclaw" })}\n`,
339+
);
340+
if (testCase.withWorkspace) {
341+
fs.mkdirSync(path.join(sourceDir, "packages", "ai"), { recursive: true });
342+
fs.writeFileSync(path.join(sourceDir, "packages", "ai", "package.json"), "{}\n");
343+
}
344+
345+
try {
346+
await expect(prepareBundledAiRuntimePackage(sourceDir, outputDir, vi.fn())).rejects.toThrow(
347+
testCase.expected,
348+
);
349+
} finally {
350+
fs.rmSync(sourceDir, { recursive: true, force: true });
351+
fs.rmSync(outputDir, { recursive: true, force: true });
352+
}
353+
}
354+
});
355+
294356
it("trims and restores the changelog around ignore-scripts package artifacts", async () => {
295357
const calls: string[] = [];
296358
const tarball = await packOpenClawPackageForDocker("/repo", "/out", {

0 commit comments

Comments
 (0)