fix(release): fail empty control ui tarballs

This commit is contained in:
Peter Steinberger
2026-03-23 11:02:31 -07:00
parent 0ea3c4d5d8
commit 80bd5ba728
3 changed files with 52 additions and 9 deletions

View File

@@ -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)

View File

@@ -41,6 +41,7 @@ const CORRECTION_TAG_REGEX = /^(?<base>\d{4}\.[1-9]\d?\.[1-9]\d?)-(?<correction>
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>): 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 {

View File

@@ -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(