diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 298276bbde6..33431331399 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -32,6 +32,15 @@ OpenClaw has three public release lanes: - Detailed release procedure, approvals, credentials, and recovery notes are maintainer-only +## Release preflight + +- Run `pnpm release:check` before every tagged release +- Run `RELEASE_TAG=vYYYY.M.D node --import tsx scripts/openclaw-npm-release-check.ts` + (or the matching beta/correction tag) before approval +- npm release preflight fails closed unless the tarball includes both + `dist/control-ui/index.html` and a non-empty `dist/control-ui/assets/` payload + so we do not ship an empty browser dashboard again + ## Public references - [`.github/workflows/openclaw-npm-release.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-npm-release.yml) diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index e690f552c72..b998a85079f 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -41,6 +41,7 @@ const CORRECTION_TAG_REGEX = /^(?\d{4}\.[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 CONTROL_UI_ASSET_PREFIX = "dist/control-ui/assets/"; function normalizeRepoUrl(value: unknown): string { if (typeof value !== "string") { @@ -385,6 +386,28 @@ export function parseNpmPackJsonOutput(stdout: string): NpmPackResult[] | null { return null; } +export function collectControlUiPackErrors(paths: Iterable): string[] { + const packedPaths = new Set(paths); + const assetPaths = [...packedPaths].filter((path) => path.startsWith(CONTROL_UI_ASSET_PREFIX)); + const errors: string[] = []; + + 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.`, + ); + } + } + + if (assetPaths.length === 0) { + errors.push( + `npm package is missing Control UI asset payload under "${CONTROL_UI_ASSET_PREFIX}". Refuse release when the dashboard tarball would be empty.`, + ); + } + + return errors; +} + function collectPackedTarballErrors(): string[] { const errors: string[] = []; let stdout = ""; @@ -415,15 +438,7 @@ function collectPackedTarballErrors(): string[] { .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; + return collectControlUiPackErrors(packedPaths); } function main(): number { diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index d863ca74259..643b29aa19d 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + collectControlUiPackErrors, collectReleasePackageMetadataErrors, collectReleaseTagErrors, parseNpmPackJsonOutput, @@ -130,6 +131,24 @@ describe("parseNpmPackJsonOutput", () => { }); }); +describe("collectControlUiPackErrors", () => { + it("rejects packs that ship the dashboard HTML without the asset payload", () => { + expect(collectControlUiPackErrors(["dist/control-ui/index.html"])).toEqual([ + 'npm package is missing Control UI asset payload under "dist/control-ui/assets/". Refuse release when the dashboard tarball would be empty.', + ]); + }); + + it("accepts packs that ship dashboard HTML and bundled assets", () => { + expect( + collectControlUiPackErrors([ + "dist/control-ui/index.html", + "dist/control-ui/assets/index-Bu8rSoJV.js", + "dist/control-ui/assets/index-BK0yXA_h.css", + ]), + ).toEqual([]); + }); +}); + describe("collectReleaseTagErrors", () => { it("accepts versions within the two-day CalVer window", () => { expect(