openclaw/scripts/check-control-ui-performance-base.mts
Dallin Romney 4dbabc40f6
fix(release): skip unavailable frozen Telegram scenarios (#160190)
* fix(release): gate frozen Telegram delivery scenario

* fix(ci): validate repairs against broken UI bases

* fix(ci): reject interrupted UI comparison builds

* test(ui): await rename reconciliation before render assertion
2026-09-28 02:32:38 -07:00

225 lines
8.8 KiB
TypeScript

#!/usr/bin/env node
// Compare source revisions with one installed UI toolchain, outside artifact builds.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createControlUiPrecompressedAssetVariants } from "../ui/vite.config.ts";
import { assertRealOutputRoot } from "./lib/output-root-guard.mjs";
import { isRecord } from "./lib/record-shared.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const require = createRequire(path.join(repoRoot, "ui/package.json"));
const COMPARISON_BUILD_ENV = {
GIT_BRANCH: "ci/control-ui-performance",
GIT_COMMIT: "0123456789abcdef0123456789abcdef01234567",
OPENCLAW_BUILD_TIMESTAMP: "2026-09-01T00:00:00.000Z",
OPENCLAW_CONTROL_UI_BUILD_ID: "control-ui-performance-comparison",
OPENCLAW_CONTROL_UI_RELEASE_BUILD: "1",
} satisfies NodeJS.ProcessEnv;
function run(
command: string,
args: string[],
cwd = repoRoot,
env = process.env,
acceptFailure = false,
): boolean {
const result = spawnSync(command, args, { cwd, env, stdio: "inherit" });
// A historical source mismatch may exit nonzero; a signal means no trustworthy
// build outcome exists and must never enter the absolute-only fallback.
if (result.error || result.signal !== null || (!acceptFailure && result.status !== 0)) {
throw new Error(
`${path.basename(command)} failed (${result.signal ?? result.status ?? "launch"})`,
{
cause: result.error,
},
);
}
return result.status === 0;
}
function resolveCommit(ref: string): string {
const result = spawnSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
cwd: repoRoot,
encoding: "utf8",
});
if (result.status !== 0 || !/^[0-9a-f]{40}\n?$/u.test(result.stdout)) {
throw new Error(`Cannot resolve local Control UI comparison commit: ${ref}`);
}
return result.stdout.trim();
}
function declaredDependencyNames(root: string): string[] {
const file = path.join(root, "package.json");
if (!fs.existsSync(file)) {
return [];
}
const manifest: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
if (!isRecord(manifest)) {
throw new Error(`Invalid package manifest: ${file}`);
}
const names: string[] = [];
for (const key of ["dependencies", "devDependencies", "optionalDependencies"]) {
const entries = manifest[key];
if (entries === undefined) {
continue;
}
if (!isRecord(entries)) {
throw new Error(`Invalid ${key} in package manifest: ${file}`);
}
names.push(...Object.keys(entries));
}
return names;
}
function linkDependencies(baseRoot: string): void {
const roots = ["", "ui"];
for (const parent of ["packages", "extensions"]) {
for (const entry of fs.readdirSync(path.join(repoRoot, parent), { withFileTypes: true })) {
if (entry.isDirectory()) {
roots.push(path.join(parent, entry.name));
}
}
}
if (
roots.some((root) => {
const candidate = new Set(declaredDependencyNames(path.join(repoRoot, root)));
return declaredDependencyNames(path.join(baseRoot, root)).some(
(name) => !candidate.has(name),
);
})
) {
// A removal must not make the historical source unbuildable. Materialize
// its lockfile in this private archive without running historical hooks.
run("pnpm", ["install", "--frozen-lockfile", "--ignore-scripts"], baseRoot);
}
const workspaceRoots = new Map(
roots.map((root) => [fs.realpathSync(path.join(repoRoot, root)), root]),
);
for (const root of roots) {
const dependencies = path.join(repoRoot, root, "node_modules");
const destinationRoot = path.join(baseRoot, root);
if (fs.existsSync(dependencies) && fs.existsSync(destinationRoot)) {
// An outer node_modules link resolves workspace packages back into the candidate.
// Share installed third-party packages, but keep workspace source in the archived tree.
const packages = fs.readdirSync(dependencies).flatMap((name) => {
if (name.startsWith(".") && name !== ".bin") {
return [];
}
return name.startsWith("@")
? fs.readdirSync(path.join(dependencies, name)).map((child) => path.join(name, child))
: [name];
});
for (const name of packages) {
const installed = fs.realpathSync(path.join(dependencies, name));
const workspace = workspaceRoots.get(installed);
const target = workspace === undefined ? installed : path.join(baseRoot, workspace);
const destination = path.join(destinationRoot, "node_modules", name);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.rmSync(destination, { recursive: true, force: true });
fs.symlinkSync(target, destination, "junction");
}
}
}
}
function main(): void {
const [baseRef, ...extra] = process.argv.slice(2);
if (!baseRef || extra.length > 0 || !/^[0-9a-f]{40}$/u.test(baseRef)) {
throw new Error("Usage: pnpm ui:check-performance:base <full-base-commit-sha>");
}
const base = resolveCommit(baseRef);
const head = resolveCommit("HEAD");
assertRealOutputRoot(path.join(repoRoot, "dist"));
assertRealOutputRoot(path.join(repoRoot, "dist/control-ui"));
const vitePackagePath = require.resolve("vite/package.json");
const viteVersion = (JSON.parse(fs.readFileSync(vitePackagePath, "utf8")) as { version: string })
.version;
const pakoVersion = (
JSON.parse(fs.readFileSync(require.resolve("pako/package.json"), "utf8")) as { version: string }
).version;
const viteBin = path.join(path.dirname(vitePackagePath), "bin/vite.js");
console.log(
`Control UI comparison: working-tree head ${head}; base ${base}; Node ${process.version}; Vite ${viteVersion}; Pako ${pakoVersion}`,
);
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ui-performance-base-"));
try {
const baseRoot = path.join(temporaryRoot, "source");
const archive = path.join(temporaryRoot, "source.tar.gz");
const buildEnv = {
...process.env,
...COMPARISON_BUILD_ENV,
GIT_DIR: path.join(temporaryRoot, "git-disabled"),
};
fs.mkdirSync(baseRoot);
run("git", ["archive", "--format=tar.gz", "-1", "--output", archive, base]);
run("tar", ["-xzf", archive, "-C", baseRoot]);
linkDependencies(baseRoot);
// Both builds use the candidate's toolchain and shared dependencies; only
// base-only dependencies come from its lockfile. Calling Vite directly
// keeps historical policy out; one identity isolates source bytes.
const candidateUiRoot = path.join(repoRoot, "ui");
const baseUiRoot = path.join(baseRoot, "ui");
const candidateBuildArgs = [
viteBin,
"build",
"--config",
path.join(candidateUiRoot, "vite.config.ts"),
];
const baseBuildArgs = [viteBin, "build", "--config", path.join(baseUiRoot, "vite.config.ts")];
run(process.execPath, candidateBuildArgs, candidateUiRoot, buildEnv);
const baseBuildPassed = run(process.execPath, baseBuildArgs, baseUiRoot, buildEnv, true);
const loader = path.join(repoRoot, "scripts/tsx.mjs");
run(process.execPath, [
"--import",
loader,
"scripts/check-control-ui-precompressed-assets.mts",
]);
if (!baseBuildPassed) {
console.warn(
"Base Control UI source does not build with the candidate toolchain; enforcing candidate absolute budgets without a differential comparison.",
);
run(process.execPath, ["--import", loader, "scripts/check-control-ui-performance.mts"]);
return;
}
const baseDist = path.join(baseRoot, "dist/control-ui");
const baseAssets = path.join(baseDist, "assets");
// Normalize historical CSS with the candidate's canonical compressor too:
// dependency sharing alone cannot pin options embedded in the old config.
for (const entry of fs.readdirSync(baseAssets, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith(".css")) {
continue;
}
const source = fs.readFileSync(path.join(baseAssets, entry.name));
for (const variant of createControlUiPrecompressedAssetVariants(
`assets/${entry.name}`,
source,
)) {
fs.writeFileSync(path.join(baseDist, variant.fileName), variant.source);
}
}
console.log("Base CSS sidecars normalized with the candidate's canonical compressor.");
run(process.execPath, [
"--import",
loader,
"scripts/check-control-ui-performance.mts",
"--base-dist",
baseDist,
]);
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
console.error("[control-ui-performance-base] FAILED (exit 1)");
process.exitCode = 1;
}