fix: skip clean run-node runtime restaging

This commit is contained in:
Shakker
2026-04-22 05:30:31 +01:00
committed by Shakker
parent cc91e8ecf9
commit 08d5ad3828
3 changed files with 364 additions and 10 deletions

View File

@@ -13,6 +13,15 @@ export function resolveBuildRequirement(deps: {
configFiles: string[];
}): { shouldBuild: boolean; reason: string };
export function resolveRuntimePostBuildRequirement(deps: {
cwd: string;
env: NodeJS.ProcessEnv;
fs: unknown;
spawnSync: unknown;
buildStampPath: string;
runtimePostBuildStampPath: string;
}): { shouldSync: boolean; reason: string };
export function acquireRunNodeBuildLock(deps: {
cwd: string;
args: readonly string[];

View File

@@ -17,10 +17,32 @@ const compilerArgs = [buildScript, "--no-clean"];
const runNodeSourceRoots = ["src", BUNDLED_PLUGIN_ROOT_DIR];
const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"];
export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles];
const runtimePostBuildStampFile = ".runtime-postbuildstamp";
const runtimePostBuildWatchedPaths = [
"scripts/copy-bundled-plugin-metadata.mjs",
"scripts/copy-plugin-sdk-root-alias.mjs",
"scripts/lib",
"scripts/npm-runner.mjs",
"scripts/runtime-postbuild-shared.mjs",
"scripts/runtime-postbuild.mjs",
"scripts/stage-bundled-plugin-runtime-deps.mjs",
"scripts/stage-bundled-plugin-runtime.mjs",
"scripts/windows-cmd-helpers.mjs",
"scripts/write-official-channel-catalog.mjs",
"src/plugin-sdk/root-alias.cjs",
BUNDLED_PLUGIN_ROOT_DIR,
];
const ignoredRunNodeRepoPaths = new Set([
"src/canvas-host/a2ui/.bundle.hash",
"src/canvas-host/a2ui/a2ui.bundle.js",
]);
const runtimePostBuildScriptPaths = new Set(
runtimePostBuildWatchedPaths.filter((entry) => entry.startsWith("scripts/")),
);
const runtimePostBuildStaticAssetPaths = new Set([
"extensions/acpx/src/runtime-internals/mcp-proxy.mjs",
"extensions/diffs/assets/viewer-runtime.js",
]);
const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/;
const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]);
@@ -130,11 +152,11 @@ const findLatestMtime = (dirPath, shouldSkip, deps) => {
return latest;
};
const readGitStatus = (deps) => {
const readGitStatus = (deps, paths = runNodeWatchedPaths) => {
try {
const result = deps.spawnSync(
"git",
["status", "--porcelain", "--untracked-files=normal", "--", ...runNodeWatchedPaths],
["status", "--porcelain", "--untracked-files=normal", "--", ...paths],
{
cwd: deps.cwd,
encoding: "utf8",
@@ -165,6 +187,38 @@ const hasDirtySourceTree = (deps) => {
return parseGitStatusPaths(output).some((repoPath) => isBuildRelevantRunNodePath(repoPath));
};
const isRuntimePostBuildRelevantPath = (repoPath) => {
const normalizedPath = normalizePath(repoPath).replace(/^\.\/+/, "");
if (normalizedPath === "src/plugin-sdk/root-alias.cjs") {
return true;
}
if (runtimePostBuildStaticAssetPaths.has(normalizedPath)) {
return true;
}
if (
normalizedPath.startsWith("scripts/") &&
(runtimePostBuildScriptPaths.has(normalizedPath) || normalizedPath.startsWith("scripts/lib/"))
) {
return true;
}
if (!normalizedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return false;
}
const pluginRelativePath = normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length);
if (pluginRelativePath.startsWith("skills/")) {
return true;
}
return extensionRestartMetadataFiles.has(path.posix.basename(pluginRelativePath));
};
const hasDirtyRuntimePostBuildInputs = (deps) => {
const output = readGitStatus(deps, runtimePostBuildWatchedPaths);
if (output === null) {
return null;
}
return parseGitStatusPaths(output).some((repoPath) => isRuntimePostBuildRelevantPath(repoPath));
};
const readBuildStamp = (deps) => {
const mtime = statMtime(deps.buildStampPath, deps.fs);
if (mtime == null) {
@@ -183,6 +237,24 @@ const readBuildStamp = (deps) => {
}
};
const readRuntimePostBuildStamp = (deps) => {
const mtime = statMtime(deps.runtimePostBuildStampPath, deps.fs);
if (mtime == null) {
return { mtime: null, head: null };
}
try {
const raw = deps.fs.readFileSync(deps.runtimePostBuildStampPath, "utf8").trim();
if (!raw.startsWith("{")) {
return { mtime, head: null };
}
const parsed = JSON.parse(raw);
const head = typeof parsed?.head === "string" && parsed.head.trim() ? parsed.head.trim() : null;
return { mtime, head };
} catch {
return { mtime, head: null };
}
};
const hasSourceMtimeChanged = (stampMtime, deps) => {
let latestSourceMtime = null;
for (const sourceRoot of deps.sourceRoots) {
@@ -198,6 +270,43 @@ const hasSourceMtimeChanged = (stampMtime, deps) => {
return latestSourceMtime != null && latestSourceMtime > stampMtime;
};
const findLatestRuntimePostBuildInputMtime = (absolutePath, relativePath, deps) => {
const normalizedRelativePath = normalizePath(relativePath);
const statsMtime = statMtime(absolutePath, deps.fs);
if (statsMtime == null) {
return null;
}
let stat;
try {
stat = deps.fs.statSync(absolutePath);
} catch {
return null;
}
if (!stat.isDirectory()) {
return isRuntimePostBuildRelevantPath(normalizedRelativePath) ? statsMtime : null;
}
return findLatestMtime(
absolutePath,
(candidate) => {
const candidateRelativePath = path.relative(deps.cwd, candidate);
return !isRuntimePostBuildRelevantPath(candidateRelativePath);
},
deps,
);
};
const hasRuntimePostBuildInputMtimeChanged = (stampMtime, deps) => {
let latestInputMtime = null;
for (const relativePath of runtimePostBuildWatchedPaths) {
const absolutePath = path.join(deps.cwd, relativePath);
const inputMtime = findLatestRuntimePostBuildInputMtime(absolutePath, relativePath, deps);
if (inputMtime != null && (latestInputMtime == null || inputMtime > latestInputMtime)) {
latestInputMtime = inputMtime;
}
}
return latestInputMtime != null && latestInputMtime > stampMtime;
};
export const resolveBuildRequirement = (deps) => {
if (deps.env.OPENCLAW_FORCE_BUILD === "1") {
return { shouldBuild: true, reason: "force_build" };
@@ -248,6 +357,48 @@ export const resolveBuildRequirement = (deps) => {
return { shouldBuild: false, reason: "clean" };
};
export const resolveRuntimePostBuildRequirement = (deps) => {
if (deps.env.OPENCLAW_FORCE_RUNTIME_POSTBUILD === "1") {
return { shouldSync: true, reason: "force_runtime_postbuild" };
}
const stamp = readRuntimePostBuildStamp(deps);
if (stamp.mtime == null) {
return { shouldSync: true, reason: "missing_runtime_postbuild_stamp" };
}
const buildStamp = readBuildStamp(deps);
if (buildStamp.mtime == null) {
return { shouldSync: true, reason: "missing_build_stamp" };
}
if (buildStamp.mtime > stamp.mtime) {
return { shouldSync: true, reason: "build_stamp_newer" };
}
const currentHead = resolveGitHead(deps);
if (currentHead && !stamp.head) {
return { shouldSync: true, reason: "runtime_postbuild_stamp_missing_head" };
}
if (currentHead && stamp.head && currentHead !== stamp.head) {
return { shouldSync: true, reason: "git_head_changed" };
}
if (currentHead) {
const dirty = hasDirtyRuntimePostBuildInputs(deps);
if (dirty === true) {
return { shouldSync: true, reason: "dirty_runtime_postbuild_inputs" };
}
if (dirty === false) {
return { shouldSync: false, reason: "clean" };
}
}
if (hasRuntimePostBuildInputMtimeChanged(stamp.mtime, deps)) {
return { shouldSync: true, reason: "runtime_postbuild_input_mtime_newer" };
}
return { shouldSync: false, reason: "clean" };
};
const BUILD_REASON_LABELS = {
force_build: "forced by OPENCLAW_FORCE_BUILD",
missing_build_stamp: "build stamp missing",
@@ -261,7 +412,20 @@ const BUILD_REASON_LABELS = {
clean: "clean",
};
const RUNTIME_POSTBUILD_REASON_LABELS = {
force_runtime_postbuild: "forced by OPENCLAW_FORCE_RUNTIME_POSTBUILD",
missing_runtime_postbuild_stamp: "runtime postbuild stamp missing",
missing_build_stamp: "build stamp missing",
build_stamp_newer: "build stamp newer than runtime postbuild stamp",
runtime_postbuild_stamp_missing_head: "runtime postbuild stamp missing git head",
git_head_changed: "git head changed",
dirty_runtime_postbuild_inputs: "dirty runtime postbuild inputs",
runtime_postbuild_input_mtime_newer: "runtime postbuild input mtime newer than stamp",
clean: "clean",
};
const formatBuildReason = (reason) => BUILD_REASON_LABELS[reason] ?? reason;
const formatRuntimePostBuildReason = (reason) => RUNTIME_POSTBUILD_REASON_LABELS[reason] ?? reason;
const SIGNAL_EXIT_CODES = {
SIGINT: 130,
@@ -565,6 +729,38 @@ const syncRuntimeArtifacts = async (deps) => {
return true;
};
const writeRuntimePostBuildStamp = (deps) => {
try {
deps.fs.mkdirSync(path.dirname(deps.runtimePostBuildStampPath), { recursive: true });
const head = resolveGitHead(deps);
deps.fs.writeFileSync(
deps.runtimePostBuildStampPath,
`${JSON.stringify(
{
syncedAt: Date.now(),
...(head ? { head } : {}),
},
null,
2,
)}\n`,
"utf8",
);
} catch (error) {
logRunner(
`Failed to write runtime postbuild stamp: ${error?.message ?? "unknown error"}`,
deps,
);
}
};
const syncRuntimeArtifactsAndStamp = async (deps) => {
const synced = await syncRuntimeArtifacts(deps);
if (synced) {
writeRuntimePostBuildStamp(deps);
}
return synced;
};
const writeBuildStamp = (deps) => {
try {
writeDistBuildStamp({
@@ -598,6 +794,7 @@ export async function runNodeMain(params = {}) {
deps.distRoot = path.join(deps.cwd, "dist");
deps.distEntry = path.join(deps.distRoot, "/entry.js");
deps.buildStampPath = path.join(deps.distRoot, ".buildstamp");
deps.runtimePostBuildStampPath = path.join(deps.distRoot, runtimePostBuildStampFile);
deps.sourceRoots = runNodeSourceRoots.map((sourceRoot) => ({
name: sourceRoot,
path: path.join(deps.cwd, sourceRoot),
@@ -615,12 +812,22 @@ export async function runNodeMain(params = {}) {
const buildRequirement = resolveBuildRequirement(deps);
if (!buildRequirement.shouldBuild) {
if (!shouldSkipCleanWatchRuntimeSync(deps)) {
const synced = await withRunNodeBuildLock(
deps,
async () => await syncRuntimeArtifacts(deps),
);
if (!synced) {
return await closeRunNodeOutputTee(deps, 1);
const runtimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps);
if (runtimePostBuildRequirement.shouldSync) {
const synced = await withRunNodeBuildLock(deps, async () => {
const lockedRuntimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps);
if (!lockedRuntimePostBuildRequirement.shouldSync) {
return true;
}
logRunner(
`Syncing runtime artifacts (${lockedRuntimePostBuildRequirement.reason} - ${formatRuntimePostBuildReason(lockedRuntimePostBuildRequirement.reason)}).`,
deps,
);
return await syncRuntimeArtifactsAndStamp(deps);
});
if (!synced) {
return await closeRunNodeOutputTee(deps, 1);
}
}
}
exitCode = await runOpenClaw(deps);
@@ -630,7 +837,15 @@ export async function runNodeMain(params = {}) {
const buildExitCode = await withRunNodeBuildLock(deps, async () => {
const lockedBuildRequirement = resolveBuildRequirement(deps);
if (!lockedBuildRequirement.shouldBuild) {
return (await syncRuntimeArtifacts(deps)) ? 0 : 1;
const runtimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps);
if (!runtimePostBuildRequirement.shouldSync) {
return 0;
}
logRunner(
`Syncing runtime artifacts (${runtimePostBuildRequirement.reason} - ${formatRuntimePostBuildReason(runtimePostBuildRequirement.reason)}).`,
deps,
);
return (await syncRuntimeArtifactsAndStamp(deps)) ? 0 : 1;
}
logRunner(
@@ -658,6 +873,7 @@ export async function runNodeMain(params = {}) {
return 1;
}
writeBuildStamp(deps);
writeRuntimePostBuildStamp(deps);
return 0;
});
if (buildExitCode !== 0) {