From e3c58e04c96a5f00bc9081e4dbf80a31d7adf53d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 14 Apr 2026 15:19:54 +0100 Subject: [PATCH] fix(release): verify packaged workspace templates --- scripts/lib/workspace-bootstrap-smoke.mjs | 112 +++++++++++++++++++++ scripts/openclaw-npm-postpublish-verify.ts | 5 + scripts/openclaw-npm-release-check.ts | 3 +- scripts/release-check.ts | 7 ++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 scripts/lib/workspace-bootstrap-smoke.mjs diff --git a/scripts/lib/workspace-bootstrap-smoke.mjs b/scripts/lib/workspace-bootstrap-smoke.mjs new file mode 100644 index 00000000000..7d96007f368 --- /dev/null +++ b/scripts/lib/workspace-bootstrap-smoke.mjs @@ -0,0 +1,112 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +export const WORKSPACE_TEMPLATE_PACK_PATHS = [ + "docs/reference/templates/AGENTS.md", + "docs/reference/templates/SOUL.md", + "docs/reference/templates/TOOLS.md", + "docs/reference/templates/IDENTITY.md", + "docs/reference/templates/USER.md", + "docs/reference/templates/HEARTBEAT.md", + "docs/reference/templates/BOOTSTRAP.md", +]; + +const REQUIRED_BOOTSTRAP_WORKSPACE_FILES = [ + "AGENTS.md", + "SOUL.md", + "TOOLS.md", + "IDENTITY.md", + "USER.md", + "HEARTBEAT.md", + "BOOTSTRAP.md", +]; + +function collectMissingBootstrapWorkspaceFiles(workspaceDir) { + return REQUIRED_BOOTSTRAP_WORKSPACE_FILES.filter( + (filename) => !existsSync(join(workspaceDir, filename)), + ); +} + +function describeExecFailure(error) { + if (!(error instanceof Error)) { + return String(error); + } + const stdout = + typeof error.stdout === "string" + ? error.stdout.trim() + : error.stdout instanceof Uint8Array + ? Buffer.from(error.stdout).toString("utf8").trim() + : ""; + const stderr = + typeof error.stderr === "string" + ? error.stderr.trim() + : error.stderr instanceof Uint8Array + ? Buffer.from(error.stderr).toString("utf8").trim() + : ""; + return [error.message, stdout, stderr].filter(Boolean).join(" | "); +} + +export function runInstalledWorkspaceBootstrapSmoke(params) { + const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-workspace-bootstrap-smoke-")); + const homeDir = join(tempRoot, "home"); + const cwd = join(tempRoot, "cwd"); + mkdirSync(homeDir, { recursive: true }); + mkdirSync(cwd, { recursive: true }); + + let combinedOutput = ""; + try { + try { + execFileSync( + process.execPath, + [ + join(params.packageRoot, "openclaw.mjs"), + "agent", + "--message", + "workspace bootstrap smoke", + "--session-id", + "workspace-bootstrap-smoke", + "--local", + "--timeout", + "1", + "--json", + ], + { + cwd, + encoding: "utf8", + maxBuffer: 1024 * 1024 * 16, + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + HOME: homeDir, + OPENCLAW_HOME: homeDir, + OPENCLAW_SUPPRESS_NOTES: "1", + }, + }, + ); + } catch (error) { + combinedOutput = describeExecFailure(error); + } + + if (combinedOutput.includes("Missing workspace template:")) { + throw new Error( + `installed workspace bootstrap failed before agent execution: ${combinedOutput}`, + ); + } + + const workspaceDir = join(homeDir, ".openclaw", "workspace"); + const missingFiles = collectMissingBootstrapWorkspaceFiles(workspaceDir); + if (missingFiles.length > 0) { + throw new Error( + `installed workspace bootstrap did not create required files in ${workspaceDir}: ${missingFiles.join(", ")}`, + ); + } + } finally { + try { + rmSync(tempRoot, { force: true, recursive: true }); + } catch { + // best effort cleanup only + } + } +} diff --git a/scripts/openclaw-npm-postpublish-verify.ts b/scripts/openclaw-npm-postpublish-verify.ts index fce132b7167..7725319d51b 100644 --- a/scripts/openclaw-npm-postpublish-verify.ts +++ b/scripts/openclaw-npm-postpublish-verify.ts @@ -21,6 +21,7 @@ import { collectRuntimeDependencySpecs, } from "./lib/bundled-plugin-root-runtime-mirrors.mjs"; import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "./lib/npm-update-compat-sidecars.mjs"; +import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs"; import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts"; type InstalledPackageJson = { @@ -371,6 +372,10 @@ function verifyScenario(version: string, scenario: PublishedInstallScenario): vo ); } + if (errors.length === 0) { + runInstalledWorkspaceBootstrapSmoke({ packageRoot }); + } + if (errors.length > 0) { throw new Error(`${scenario.name} failed:\n- ${errors.join("\n- ")}`); } diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 8043ebbb543..49ca6dff643 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -10,6 +10,7 @@ import { parseReleaseVersion as parseReleaseVersionBase, } from "./lib/npm-publish-plan.mjs"; import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "./lib/npm-update-compat-sidecars.mjs"; +import { WORKSPACE_TEMPLATE_PACK_PATHS } from "./lib/workspace-bootstrap-smoke.mjs"; type PackageJson = { name?: string; @@ -55,7 +56,7 @@ export type NpmDistTagMirrorAuth = { }; 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 REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html", ...WORKSPACE_TEMPLATE_PACK_PATHS]; const CONTROL_UI_ASSET_PREFIX = "dist/control-ui/assets/"; const FORBIDDEN_PACKED_PATH_RULES = [ { diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 1b69c4b5445..0b9e63aef22 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -19,6 +19,10 @@ import { } from "./lib/bundled-plugin-root-runtime-mirrors.mjs"; import { collectPackUnpackedSizeErrors as collectNpmPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs"; import { listPluginSdkDistArtifacts } from "./lib/plugin-sdk-entries.mjs"; +import { + runInstalledWorkspaceBootstrapSmoke, + WORKSPACE_TEMPLATE_PACK_PATHS, +} from "./lib/workspace-bootstrap-smoke.mjs"; import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs"; import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts"; @@ -39,6 +43,7 @@ const requiredPathGroups = [ ...listPluginSdkDistArtifacts(), ...listBundledPluginPackArtifacts(), ...listStaticExtensionAssetOutputs(), + ...WORKSPACE_TEMPLATE_PACK_PATHS, ...listRequiredQaScenarioPackPaths(), "scripts/npm-runner.mjs", "scripts/postinstall-bundled-plugins.mjs", @@ -235,6 +240,8 @@ function runPackedBundledChannelEntrySmoke(): void { if (completionFiles.length === 0) { throw new Error("release-check: packed completion smoke produced no completion files."); } + + runInstalledWorkspaceBootstrapSmoke({ packageRoot }); } finally { rmSync(tmpRoot, { recursive: true, force: true }); }