From 6704d0ab271403971347433868b22e83455c8f1a Mon Sep 17 00:00:00 2001 From: xin zhuang <65798732+1052326311@users.noreply.github.com> Date: Mon, 25 May 2026 15:08:52 +0800 Subject: [PATCH] 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 --- CHANGELOG.md | 3 +-- scripts/build-all.mjs | 12 ++++++++++++ scripts/ui.js | 30 +++++++++++------------------- test/scripts/build-all.test.ts | 34 ++++++++++++++++++++++++++++++++++ test/scripts/ui.test.ts | 18 ++++++++++++++++++ 5 files changed, 76 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b09e6890217..508676aa6dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/scripts/build-all.mjs b/scripts/build-all.mjs index ade79b3fb8e4..b3b18639dac4 100644 --- a/scripts/build-all.mjs +++ b/scripts/build-all.mjs @@ -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", diff --git a/scripts/ui.js b/scripts/ui.js index b6c4ec5cae21..c7c4331984d6 100644 --- a/scripts/ui.js +++ b/scripts/ui.js @@ -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]); } diff --git a/test/scripts/build-all.test.ts b/test/scripts/build-all.test.ts index a514dc956ac4..710ae1b99203 100644 --- a/test/scripts/build-all.test.ts +++ b/test/scripts/build-all.test.ts @@ -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(); diff --git a/test/scripts/ui.test.ts b/test/scripts/ui.test.ts index 0d6138cd6c41..187a5e98046b 100644 --- a/test/scripts/ui.test.ts +++ b/test/scripts/ui.test.ts @@ -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"); + }); });