mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 10:04:07 +00:00
fix(build): preserve fresh startup metadata across rebuilds
This commit is contained in:
@@ -180,9 +180,15 @@ export const BUILD_ALL_PROFILES = {
|
||||
};
|
||||
|
||||
export const BUILD_ALL_PROFILE_STEP_ENV = {
|
||||
full: {
|
||||
tsdown: {
|
||||
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
||||
},
|
||||
},
|
||||
ciArtifacts: {
|
||||
tsdown: {
|
||||
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
||||
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
||||
},
|
||||
},
|
||||
gatewayWatch: {
|
||||
@@ -201,6 +207,7 @@ export const BUILD_ALL_PROFILE_STEP_ENV = {
|
||||
cliStartup: {
|
||||
tsdown: {
|
||||
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
||||
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
||||
},
|
||||
"runtime-postbuild": {
|
||||
OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0",
|
||||
|
||||
@@ -30,6 +30,8 @@ const CGROUP_MEMORY_LIMIT_PATHS = [
|
||||
const PROC_MEMINFO_PATH = "/proc/meminfo";
|
||||
const TERMINATION_GRACE_MS = 5_000;
|
||||
const ROOT_TSDOWN_OUTPUT_ROOTS = ["dist", "dist-runtime"];
|
||||
const PRESERVED_TSDOWN_OUTPUT_FILES = ["dist/cli-startup-metadata.json"];
|
||||
const PRESERVE_CLI_STARTUP_METADATA_ENV = "OPENCLAW_PRESERVE_CLI_STARTUP_METADATA";
|
||||
const GENERATED_SOURCE_DECLARATION_PATHSPEC = ":(glob)extensions/**/*.d.ts";
|
||||
const DECLARATION_EXTENSIONS = [".d.ts", ".d.mts", ".d.cts"];
|
||||
const SOURCE_DECLARATION_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"];
|
||||
@@ -78,11 +80,15 @@ export function cleanTsdownOutputRoots(params = {}) {
|
||||
roots,
|
||||
})
|
||||
: new Set();
|
||||
const protectedPaths = new Set([
|
||||
...protectedDeclarationPaths,
|
||||
...listExistingPreservedOutputPaths({ cwd, env, fs: fsImpl }),
|
||||
]);
|
||||
for (const root of roots) {
|
||||
const rootPath = path.join(cwd, root);
|
||||
try {
|
||||
if (hasProtectedChild({ rootPath, protectedPaths: protectedDeclarationPaths })) {
|
||||
cleanOutputRootExcept(rootPath, protectedDeclarationPaths, fsImpl);
|
||||
if (hasProtectedChild({ rootPath, protectedPaths })) {
|
||||
cleanOutputRootExcept(rootPath, protectedPaths, fsImpl);
|
||||
} else {
|
||||
fsImpl.rmSync(rootPath, { force: true, recursive: true });
|
||||
}
|
||||
@@ -137,6 +143,24 @@ function listExistingDeclarationOutputPaths({ cwd, fs: fsImpl, roots }) {
|
||||
return protectedPaths;
|
||||
}
|
||||
|
||||
function listExistingPreservedOutputPaths({ cwd, env, fs: fsImpl }) {
|
||||
const protectedPaths = new Set();
|
||||
if (env[PRESERVE_CLI_STARTUP_METADATA_ENV] !== "1") {
|
||||
return protectedPaths;
|
||||
}
|
||||
for (const relativePath of PRESERVED_TSDOWN_OUTPUT_FILES) {
|
||||
const absolutePath = path.resolve(cwd, relativePath);
|
||||
try {
|
||||
if (fsImpl.statSync(absolutePath).isFile()) {
|
||||
protectedPaths.add(absolutePath);
|
||||
}
|
||||
} catch {
|
||||
// Missing preserved outputs are normal on first build.
|
||||
}
|
||||
}
|
||||
return protectedPaths;
|
||||
}
|
||||
|
||||
function collectDeclarationOutputPaths(rootPath, protectedPaths, fsImpl) {
|
||||
let entries = [];
|
||||
try {
|
||||
|
||||
@@ -39,6 +39,7 @@ const CORE_CHANNEL_ORDER = [
|
||||
"signal",
|
||||
"imessage",
|
||||
] as const;
|
||||
const generatorSignature = createHash("sha1").update(readFileSync(scriptPath)).digest("hex");
|
||||
|
||||
type ExtensionChannelEntry = {
|
||||
id: string;
|
||||
@@ -467,6 +468,7 @@ export async function writeCliStartupMetadata(options?: {
|
||||
try {
|
||||
const existing = JSON.parse(readFileSync(resolvedOutputPath, "utf8")) as {
|
||||
rootHelpBundleSignature?: unknown;
|
||||
generatorSignature?: unknown;
|
||||
browserHelpSourceSignature?: unknown;
|
||||
secretsHelpSourceSignature?: unknown;
|
||||
nodesHelpSourceSignature?: unknown;
|
||||
@@ -480,6 +482,7 @@ export async function writeCliStartupMetadata(options?: {
|
||||
if (
|
||||
bundleIdentity &&
|
||||
existing.rootHelpBundleSignature === bundleIdentity.signature &&
|
||||
existing.generatorSignature === generatorSignature &&
|
||||
existing.browserHelpSourceSignature === browserHelpSourceSignature &&
|
||||
existing.secretsHelpSourceSignature === secretsHelpSourceSignature &&
|
||||
existing.nodesHelpSourceSignature === nodesHelpSourceSignature &&
|
||||
@@ -527,6 +530,7 @@ export async function writeCliStartupMetadata(options?: {
|
||||
`${JSON.stringify(
|
||||
{
|
||||
generatedBy: "scripts/write-cli-startup-metadata.ts",
|
||||
generatorSignature,
|
||||
channelOptions,
|
||||
channelCatalogSignature: channelCatalog.signature,
|
||||
rootHelpBundleSignature: bundleIdentity?.signature ?? null,
|
||||
|
||||
@@ -214,7 +214,9 @@ describe("resolveBuildAllSteps", () => {
|
||||
});
|
||||
|
||||
it("keeps the full profile aligned with the declared steps", () => {
|
||||
expect(resolveBuildAllSteps("full")).toEqual(BUILD_ALL_STEPS);
|
||||
expect(resolveBuildAllSteps("full").map((step) => step.label)).toEqual(
|
||||
BUILD_ALL_STEPS.map((step) => step.label),
|
||||
);
|
||||
expect(BUILD_ALL_PROFILES.full).toEqual(BUILD_ALL_STEPS.map((step) => step.label));
|
||||
});
|
||||
|
||||
@@ -246,7 +248,7 @@ describe("resolveBuildAllSteps", () => {
|
||||
throw new Error(`Missing ${profile} tsdown step`);
|
||||
}
|
||||
|
||||
expect(BUILD_ALL_PROFILE_STEP_ENV[profile].tsdown).toEqual({
|
||||
expect(BUILD_ALL_PROFILE_STEP_ENV[profile].tsdown).toMatchObject({
|
||||
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
||||
});
|
||||
expect(
|
||||
@@ -257,6 +259,30 @@ describe("resolveBuildAllSteps", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves startup metadata only for profiles that regenerate it", () => {
|
||||
for (const profile of ["full", "ciArtifacts", "cliStartup"]) {
|
||||
const tsdown = resolveBuildAllSteps(profile).find((step) => step.label === "tsdown");
|
||||
if (!tsdown) {
|
||||
throw new Error(`Missing ${profile} tsdown step`);
|
||||
}
|
||||
|
||||
expect(resolveBuildAllStep(tsdown, { env: {} }).options.env).toMatchObject({
|
||||
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
||||
});
|
||||
}
|
||||
|
||||
for (const profile of ["gatewayWatch", "qaRuntime"]) {
|
||||
const tsdown = resolveBuildAllSteps(profile).find((step) => step.label === "tsdown");
|
||||
if (!tsdown) {
|
||||
throw new Error(`Missing ${profile} tsdown step`);
|
||||
}
|
||||
|
||||
expect(resolveBuildAllStep(tsdown, { env: {} }).options.env).not.toHaveProperty(
|
||||
"OPENCLAW_PRESERVE_CLI_STARTUP_METADATA",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a minimal built runtime profile for gateway watch regression", () => {
|
||||
expect(resolveBuildAllSteps("gatewayWatch").map((step) => step.label)).toEqual([
|
||||
"tsdown",
|
||||
|
||||
@@ -320,6 +320,39 @@ describe("resolveTsdownBuildInvocation", () => {
|
||||
await expect(fsPromises.readFile(unrelatedFile, "utf8")).resolves.toBe("keep\n");
|
||||
});
|
||||
|
||||
it("removes CLI startup metadata during default tsdown clean", async () => {
|
||||
const rootDir = createTempDir("openclaw-tsdown-clean-metadata-default-");
|
||||
const metadataFile = path.join(rootDir, "dist", "cli-startup-metadata.json");
|
||||
await fsPromises.mkdir(path.dirname(metadataFile), { recursive: true });
|
||||
await fsPromises.writeFile(metadataFile, '{"generatedBy":"test"}\n');
|
||||
|
||||
cleanTsdownOutputRoots({ cwd: rootDir });
|
||||
|
||||
await expectPathMissing(metadataFile);
|
||||
});
|
||||
|
||||
it("preserves CLI startup metadata across opted-in build-all tsdown clean", async () => {
|
||||
const rootDir = createTempDir("openclaw-tsdown-clean-metadata-");
|
||||
const metadataFile = path.join(rootDir, "dist", "cli-startup-metadata.json");
|
||||
const staleFile = path.join(rootDir, "dist", "stale.js");
|
||||
const nestedStaleFile = path.join(rootDir, "dist", "nested", "stale.js");
|
||||
await fsPromises.mkdir(path.dirname(nestedStaleFile), { recursive: true });
|
||||
await fsPromises.writeFile(metadataFile, '{"generatedBy":"test"}\n');
|
||||
await fsPromises.writeFile(staleFile, "stale\n");
|
||||
await fsPromises.writeFile(nestedStaleFile, "stale\n");
|
||||
|
||||
cleanTsdownOutputRoots({
|
||||
cwd: rootDir,
|
||||
env: { OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1" },
|
||||
});
|
||||
|
||||
await expect(fsPromises.readFile(metadataFile, "utf8")).resolves.toBe(
|
||||
'{"generatedBy":"test"}\n',
|
||||
);
|
||||
await expectPathMissing(staleFile);
|
||||
await expectPathMissing(nestedStaleFile);
|
||||
});
|
||||
|
||||
it("preserves existing package declarations when tsdown DTS output is skipped", async () => {
|
||||
const rootDir = createTempDir("openclaw-tsdown-clean-skip-dts-");
|
||||
const declarationFile = path.join(
|
||||
|
||||
@@ -93,6 +93,7 @@ describe("write-cli-startup-metadata", () => {
|
||||
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
||||
browserHelpText: string;
|
||||
channelOptions: string[];
|
||||
generatorSignature: string;
|
||||
nodesHelpText: string;
|
||||
rootHelpText: string;
|
||||
secretsHelpText: string;
|
||||
@@ -104,6 +105,7 @@ describe("write-cli-startup-metadata", () => {
|
||||
};
|
||||
};
|
||||
expect(written.channelOptions).toContain("matrix");
|
||||
expect(written.generatorSignature).toMatch(/^[a-f0-9]{40}$/u);
|
||||
expect(written.browserHelpText).toContain("Usage:");
|
||||
expect(written.browserHelpText).toContain("openclaw browser");
|
||||
expect(written.secretsHelpText).toContain("Usage:");
|
||||
@@ -154,6 +156,16 @@ describe("write-cli-startup-metadata", () => {
|
||||
await writeMetadata();
|
||||
expect(nodesRenderCount).toBe(1);
|
||||
|
||||
const staleGeneratorMetadata = JSON.parse(readFileSync(outputPath, "utf8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
staleGeneratorMetadata.generatorSignature = "stale-generator";
|
||||
writeFileSync(outputPath, `${JSON.stringify(staleGeneratorMetadata, null, 2)}\n`, "utf8");
|
||||
|
||||
await writeMetadata();
|
||||
expect(nodesRenderCount).toBe(2);
|
||||
|
||||
writeFixtureFile(
|
||||
tempRoot,
|
||||
"extensions/canvas/src/cli.ts",
|
||||
@@ -165,7 +177,7 @@ describe("write-cli-startup-metadata", () => {
|
||||
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
||||
nodesHelpText: string;
|
||||
};
|
||||
expect(nodesRenderCount).toBe(2);
|
||||
expect(written.nodesHelpText).toContain("openclaw nodes 2");
|
||||
expect(nodesRenderCount).toBe(3);
|
||||
expect(written.nodesHelpText).toContain("openclaw nodes 3");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user