From b5f3fe900f7da889ee91ab9fdafe30f020a6a005 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Jul 2026 21:59:59 +0100 Subject: [PATCH] fix(release): pack local AI runtime for release checks (#103941) --- scripts/release-check.ts | 38 +++++++++++++++----- test/scripts/release-check.test.ts | 58 ++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/scripts/release-check.ts b/scripts/release-check.ts index a1ed2153be98..8114a67d03e8 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -390,10 +390,11 @@ function runPackDry(): PackResult[] { return JSON.parse(raw) as PackResult[]; } -function runPack(packDestination: string): PackResult[] { +function runPack(packDestination: string, cwd?: string): PackResult[] { const raw = execPnpm( ["--config.ignore-scripts=true", "pack", "--json", "--pack-destination", packDestination], { + cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 100, @@ -455,6 +456,25 @@ export function resolveReleaseCheckLocalPackageTarballs( return tarballs; } +export function prepareReleaseCheckLocalPackageTarballs(params: { + tmpRoot: string; + tarballDir?: string; + packLocalAi?: (packDestination: string) => PackResult[]; +}): string[] { + if (params.tarballDir) { + return resolveReleaseCheckLocalPackageTarballs(params.tarballDir); + } + + // The root tarball requires the exact sibling AI version. Never fall back to + // registry bytes, which could silently validate an older publication. + const packDir = join(params.tmpRoot, "ai-pack"); + mkdirSync(packDir); + const packResults = params.packLocalAi + ? params.packLocalAi(packDir) + : runPack(packDir, resolve("packages/ai")); + return [resolvePackedTarballPath(packDir, packResults)]; +} + export function createPackedTarballInstallArgs(prefixDir: string): string[] { return ["install", "--prefix", prefixDir, "--ignore-scripts", "--no-audit", "--no-fund"]; } @@ -464,17 +484,16 @@ export function writePackedTarballInstallManifest( tarballPath: string, localPackageTarballs: string[], ): void { - if (localPackageTarballs.length > 1) { + const aiTarball = localPackageTarballs[0]; + if (localPackageTarballs.length !== 1 || !aiTarball) { throw new Error( - `release-check: packed install accepts at most one @openclaw/ai tarball; found ${localPackageTarballs.length}.`, + `release-check: packed install requires exactly one @openclaw/ai tarball; found ${localPackageTarballs.length}.`, ); } const dependencies: Record = { + "@openclaw/ai": pathToFileURL(aiTarball).href, openclaw: pathToFileURL(tarballPath).href, }; - if (localPackageTarballs[0]) { - dependencies["@openclaw/ai"] = pathToFileURL(localPackageTarballs[0]).href; - } mkdirSync(prefixDir, { recursive: true }); writeFileSync( join(prefixDir, "package.json"), @@ -493,7 +512,7 @@ function installPackedTarball( prefixDir: string, tarballPath: string, cwd: string, - localPackageTarballs: string[] = [], + localPackageTarballs: string[], ): void { writePackedTarballInstallManifest(prefixDir, tarballPath, localPackageTarballs); execNpm(createPackedTarballInstallArgs(prefixDir), { @@ -921,7 +940,10 @@ function runPackedBundledChannelEntrySmoke(): void { const packResults = runPack(packDir); const tarballPath = resolvePackedTarballPath(packDir, packResults); const prefixDir = join(tmpRoot, "prefix"); - const localPackageTarballs = resolveReleaseCheckLocalPackageTarballs(); + const localPackageTarballs = prepareReleaseCheckLocalPackageTarballs({ + tmpRoot, + tarballDir: process.env[RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV], + }); installPackedTarball(prefixDir, tarballPath, tmpRoot, localPackageTarballs); const packageRoot = join(prefixDir, "node_modules", "openclaw"); diff --git a/test/scripts/release-check.test.ts b/test/scripts/release-check.test.ts index 85ab118232fa..db01d07de34e 100644 --- a/test/scripts/release-check.test.ts +++ b/test/scripts/release-check.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { createPackedTarballInstallArgs, + prepareReleaseCheckLocalPackageTarballs, RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV, resolveReleaseCheckLocalPackageTarballs, writePackedTarballInstallManifest, @@ -62,18 +63,55 @@ describe("release-check", () => { } }); - it("preserves the no-env local release check path", () => { + it("packs the local AI workspace when no prepared tarball is supplied", () => { + const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-ai-pack-test-")); + try { + const tarballs = prepareReleaseCheckLocalPackageTarballs({ + tmpRoot: root, + packLocalAi: (packDestination) => { + const filename = "openclaw-ai-2026.7.1-beta.3.tgz"; + writeFileSync(join(packDestination, filename), "fixture"); + return [{ filename }]; + }, + }); + expect(tarballs).toEqual([join(root, "ai-pack", "openclaw-ai-2026.7.1-beta.3.tgz")]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("prefers the prepared AI tarball over packing the workspace", () => { + const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-ai-pack-test-")); + try { + const preparedDir = join(root, "prepared"); + mkdirSync(preparedDir); + const preparedTarball = join(preparedDir, "openclaw-ai-2026.7.1-beta.3.tgz"); + writeFileSync(preparedTarball, "fixture"); + const tarballs = prepareReleaseCheckLocalPackageTarballs({ + tmpRoot: root, + tarballDir: preparedDir, + packLocalAi: () => { + throw new Error("workspace pack should not run"); + }, + }); + expect(tarballs).toEqual([preparedTarball]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("rejects a packed install without the local AI tarball", () => { const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-install-test-")); try { - writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", []); - const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { - dependencies?: Record; - private?: boolean; - }; - expect(manifest.private).toBe(true); - expect(manifest.dependencies).toEqual({ - openclaw: "file:///tmp/openclaw.tgz", - }); + expect(() => writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", [])).toThrow( + "requires exactly one @openclaw/ai tarball", + ); + expect(() => + writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", [ + "/tmp/openclaw-ai-one.tgz", + "/tmp/openclaw-ai-two.tgz", + ]), + ).toThrow("requires exactly one @openclaw/ai tarball"); } finally { rmSync(root, { recursive: true, force: true }); }