From fcafd8d4deda29e7e427114347bd7c5e67407e1a Mon Sep 17 00:00:00 2001 From: evann Date: Mon, 23 Mar 2026 10:29:30 -0230 Subject: [PATCH] release: verify control-ui assets are included in npm tarball --- scripts/openclaw-npm-release-check.ts | 71 ++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 768fee6caee..73a133335ec 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -39,6 +39,8 @@ const BETA_VERSION_REGEX = const CORRECTION_TAG_REGEX = /^(?\d{4}\.[1-9]\d?\.[1-9]\d?)-(?[1-9]\d*)$/; const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw"; const MAX_CALVER_DISTANCE_DAYS = 2; +const REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html"]; +const NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm"; function normalizeRepoUrl(value: unknown): string { if (typeof value !== "string") { @@ -286,6 +288,72 @@ function loadPackageJson(): PackageJson { return JSON.parse(readFileSync("package.json", "utf8")) as PackageJson; } +function runNpmCommand(args: string[]): string { + const npmExecPath = process.env.npm_execpath; + if (typeof npmExecPath === "string" && npmExecPath.length > 0) { + return execFileSync(process.execPath, [npmExecPath, ...args], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + } + return execFileSync(NPM_COMMAND, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); +} + +type NpmPackFileEntry = { + path?: string; +}; + +type NpmPackResult = { + filename?: string; + files?: NpmPackFileEntry[]; +}; + +function collectPackedTarballErrors(): string[] { + const errors: string[] = []; + let stdout = ""; + try { + stdout = runNpmCommand(["pack", "--json", "--dry-run"]); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + errors.push(`Failed to inspect npm tarball contents via \`npm pack --json --dry-run\`: ${message}`); + return errors; + } + + let parsed: unknown; + try { + parsed = JSON.parse(stdout); + } catch { + errors.push("Failed to parse JSON output from `npm pack --json --dry-run`."); + return errors; + } + + const packResults = Array.isArray(parsed) ? (parsed as NpmPackResult[]) : []; + const firstResult = packResults[0]; + if (!firstResult || !Array.isArray(firstResult.files)) { + errors.push("`npm pack --json --dry-run` did not return a files list to validate."); + return errors; + } + + const packedPaths = new Set( + firstResult.files + .map((entry) => entry.path) + .filter((path): path is string => typeof path === "string" && path.length > 0), + ); + + for (const requiredPath of REQUIRED_PACKED_PATHS) { + if (!packedPaths.has(requiredPath)) { + errors.push( + `npm package is missing required path "${requiredPath}". Ensure UI assets are built and included before publish.`, + ); + } + } + + return errors; +} + function main(): number { const pkg = loadPackageJson(); const now = new Date(); @@ -297,7 +365,8 @@ function main(): number { releaseMainRef: process.env.RELEASE_MAIN_REF, now, }); - const errors = [...metadataErrors, ...tagErrors]; + const tarballErrors = collectPackedTarballErrors(); + const errors = [...metadataErrors, ...tagErrors, ...tarballErrors]; if (errors.length > 0) { for (const error of errors) {