fix(scripts): include ui:build in build-all full and ciArtifacts profiles (#86010)

* fix(scripts): include ui:build in build-all full and ciArtifacts profiles

Closes #85206.

scripts/build-all.mjs only ran ui:build via a separate `pnpm ui:build`
command. Because `pnpm build` invokes tsdown which removes `dist/`,
a backend rebuild silently deletes any previously generated
dist/control-ui assets, leaving the gateway to serve the
"Control UI assets not found" message at startup. Documentation and
startup auto-repair masked the bug at the worst possible time
(LaunchAgent readiness / remote recovery) instead of guaranteeing the
build artifact contract.

This change adds ui:build as a build-all step after
copy-export-html-templates and before write-build-info, and includes
it in the full and ciArtifacts profiles. Minimal backend dev profiles
(gatewayWatch, cliStartup) keep their existing fast-loop step lists
and do not run ui:build.

Regression coverage:
- ciArtifacts step list assertion updated to match the new ordering.
- Three new resolveBuildAllSteps assertions: ui:build is in full and
  ciArtifacts and runs after tsdown/runtime-postbuild-stamp and before
  write-build-info; ui:build is excluded from gatewayWatch/cliStartup;
  ui:build cache outputs declare dist/control-ui.

* fix(scripts): leave ui:build uncached so dist/control-ui never restores stale build IDs

ClawSweeper review on #86010 flagged that the original ui:build cache only
hashed ui/, scripts/ui.js, and scripts/lib/copy-assets.ts, but
ui/vite.config.ts also reads package.json plus git HEAD and the
OPENCLAW_CONTROL_UI_BUILD_ID/OPENCLAW_VERSION env vars to embed a build ID
into the app and service worker. A file-input cache signature cannot
exactly invalidate those metadata sources, so a warm build-all hit could
restore a previously generated dist/control-ui after tsdown clears dist
and ship stale service-worker/app cache metadata.

Leaving the step uncached keeps the contract simple: every pnpm build
re-runs Vite, which is fast for the Control UI bundle and matches the
existing behavior of every other un-cached build-all step. Backend-only
profiles (gatewayWatch, cliStartup) are still unchanged.

Tests:
- Updated the ui:build cache assertion to require step.cache to be
  undefined and explain the metadata-input reason.
- Existing presence/order/exclusion assertions for ui:build are unchanged
  and still cover the full and ciArtifacts profile contract.

* fix(scripts): keep ui build fallback pnpm-free

---------

Co-authored-by: 1052326311 <1052326311@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
xin zhuang
2026-05-25 15:08:52 +08:00
committed by GitHub
parent 73189e3ecb
commit 6704d0ab27
5 changed files with 76 additions and 21 deletions
+1 -2
View File
@@ -7,7 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Control UI: add an ephemeral Activity tab for sanitized live tool activity summaries without persisting raw telemetry. Fixes #12831. Thanks @BunsDev.
- Build: include `ui:build` in the `full` and `ciArtifacts` profiles of `scripts/build-all.mjs` so `pnpm build` always rebuilds `dist/control-ui` after `tsdown` cleans `dist`, removing the second-command requirement and the missing-asset failure mode for source/runtime installs and CI artifact uploads. (#85206)
### Fixes
- WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.
@@ -76,7 +76,6 @@ Docs: https://docs.openclaw.ai
- Tests: fail Docker resource-ceiling checks when stats samples or configured limits are invalid instead of silently reporting zero peaks.
- Agents: fail closed when provider-less session models match multiple provider-prefixed runtime policies so CLI runtime routing no longer depends on config order. (#85970) Thanks @potterdigital.
- Control UI/agents: keep collapsed tool rows readable without early ellipses, preserve raw expanded tool details, and make post-compaction AGENTS.md reinjection opt-in to avoid duplicated project context. Fixes #45649 and #45488. Thanks @BunsDev.
## 2026.5.24
### Changes
+12
View File
@@ -17,6 +17,7 @@ const PNPM_STEP_NODE_FALLBACKS = new Map([
["scripts/run-tsgo.mjs", "-p", "tsconfig.plugin-sdk.dts.json", "--declaration", "true"],
],
["plugins:assets:copy", ["scripts/bundled-plugin-assets.mjs", "--phase", "copy"]],
["ui:build", ["scripts/ui.js", "build"]],
]);
export const BUILD_ALL_STEPS = [
{ label: "plugins:assets:build", kind: "pnpm", pnpmArgs: ["plugins:assets:build"] },
@@ -84,6 +85,16 @@ export const BUILD_ALL_STEPS = [
outputs: ["dist/export-html"],
},
},
{
label: "ui:build",
kind: "pnpm",
pnpmArgs: ["ui:build"],
// No build-all cache: ui/vite.config.ts derives the Control UI build ID
// from package.json, git HEAD, and OPENCLAW_CONTROL_UI_BUILD_ID env, so a
// file-input signature cannot exactly invalidate generated assets and a
// warm hit could restore stale service-worker/app cache metadata.
cache: undefined,
},
{
label: "write-build-info",
kind: "node",
@@ -116,6 +127,7 @@ export const BUILD_ALL_PROFILES = {
"plugins:assets:copy",
"copy-hook-metadata",
"copy-export-html-templates",
"ui:build",
"write-build-info",
"write-cli-startup-metadata",
"write-cli-compat",
+11 -19
View File
@@ -168,42 +168,34 @@ export function main(argv = process.argv.slice(2)) {
process.exit(2);
}
const runner = resolveRunner();
if (!runner) {
process.stderr.write("Missing UI runner: install pnpm, then retry.\n");
process.exit(1);
}
const script = resolveScriptAction(action);
if (action !== "install" && !script) {
usage();
process.exit(2);
}
if (process.env.OPENCLAW_BUILD_ALL_NO_PNPM === "1" && action === "build") {
run(process.execPath, [path.join(repoRoot, "node_modules/vite/bin/vite.js"), "build", ...rest]);
return;
}
const runner = resolveRunner();
if (!runner) {
process.stderr.write("Missing UI runner: install pnpm, then retry.\n");
process.exit(1);
}
if (action === "install") {
run(runner.cmd, ["install", ...rest]);
return;
}
if (!depsInstalled(action === "test" ? "test" : "build")) {
if (process.env.OPENCLAW_BUILD_ALL_NO_PNPM === "1" && action === "build") {
run(process.execPath, [
path.join(repoRoot, "node_modules/vite/bin/vite.js"),
"build",
...rest,
]);
return;
}
const installEnv = process.env;
const installArgs = ["install"];
runSync(runner.cmd, installArgs, installEnv);
}
if (process.env.OPENCLAW_BUILD_ALL_NO_PNPM === "1" && action === "build") {
run(process.execPath, [path.join(repoRoot, "node_modules/vite/bin/vite.js"), "build", ...rest]);
return;
}
run(runner.cmd, ["run", script, ...rest]);
}
+34
View File
@@ -171,6 +171,7 @@ describe("resolveBuildAllSteps", () => {
"plugins:assets:copy",
"copy-hook-metadata",
"copy-export-html-templates",
"ui:build",
"write-build-info",
"write-cli-startup-metadata",
"write-cli-compat",
@@ -209,6 +210,39 @@ describe("resolveBuildAllSteps", () => {
);
});
it("includes ui:build in the full and ciArtifacts profiles after runtime postbuild", () => {
for (const profile of ["full", "ciArtifacts"]) {
const labels = resolveBuildAllSteps(profile).map((step) => step.label);
expect(labels).toContain("ui:build");
// Control UI bundling must run after tsdown clears dist so that
// dist/control-ui survives `pnpm build` without a second command.
expect(labels.indexOf("ui:build")).toBeGreaterThan(labels.indexOf("tsdown"));
expect(labels.indexOf("ui:build")).toBeGreaterThan(labels.indexOf("runtime-postbuild-stamp"));
// ui:build must run before write-build-info so the build manifest can
// see the final dist/control-ui assets.
expect(labels.indexOf("ui:build")).toBeLessThan(labels.indexOf("write-build-info"));
}
});
it("keeps ui:build out of minimal backend-only profiles", () => {
for (const profile of ["gatewayWatch", "cliStartup"]) {
const labels = resolveBuildAllSteps(profile).map((step) => step.label);
expect(labels).not.toContain("ui:build");
}
});
it("does not cache ui:build because Vite reads package.json, git HEAD, and env metadata", () => {
// ui/vite.config.ts derives the Control UI build ID from package.json,
// git HEAD, and OPENCLAW_CONTROL_UI_BUILD_ID env, so a file-input
// signature cannot exactly invalidate generated assets. Leaving this
// step uncached avoids restoring stale service-worker/app cache
// metadata after `tsdown` clears `dist`.
const step = getBuildAllStep("ui:build");
expect(step.kind).toBe("pnpm");
expect(step.pnpmArgs).toEqual(["ui:build"]);
expect(step.cache).toBeUndefined();
});
it("does not cache plugin-sdk entry shims over compiled JS", () => {
const step = getBuildAllStep("write-plugin-sdk-entry-dts");
expect(step.cache).toBeUndefined();
+18
View File
@@ -1,3 +1,4 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
@@ -101,4 +102,21 @@ describe("scripts/ui windows spawn behavior", () => {
expect(isDirectScriptExecution(junctionScriptPath, realScriptPath, realpath)).toBe(true);
});
it("honors build-all no-pnpm mode before requiring a pnpm runner", () => {
const result = spawnSync(process.execPath, ["scripts/ui.js", "build", "--help"], {
cwd: path.resolve("."),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_BUILD_ALL_NO_PNPM: "1",
PATH: "",
},
});
const output = `${result.stdout}${result.stderr}`;
expect(result.status).toBe(0);
expect(output).not.toContain("Missing UI runner");
expect(output).toContain("vite");
});
});