fix: bound dev update cleanup

This commit is contained in:
Peter Steinberger
2026-04-26 12:39:38 +01:00
parent ddc2036956
commit 73affb491a
4 changed files with 114 additions and 13 deletions

View File

@@ -1149,6 +1149,7 @@ run_dev_channel_update() {
rm -rf $(shell_quote "$update_root")
export PATH=$(shell_quote "$bootstrap_bin:$GUEST_EXEC_PATH")
/usr/bin/env NODE_OPTIONS=--max-old-space-size=4096 \
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \
$GUEST_NODE_BIN $GUEST_OPENCLAW_ENTRY update --channel dev --yes --json
EOF
)" "$update_log" "$update_done" "$TIMEOUT_UPDATE_DEV_S" "$update_runner"

View File

@@ -2398,8 +2398,9 @@ New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
Remove-Item (Join-Path $workspace 'BOOTSTRAP.md') -Force -ErrorAction SilentlyContinue
EOF
)"
stop_gateway
guest_run_openclaw "$API_KEY_ENV" "$API_KEY_VALUE" \
agent --agent main --session-id parallels-windows-smoke --message "Reply with exact ASCII text OK only." --json
agent --local --agent main --session-id parallels-windows-smoke --message "Reply with exact ASCII text OK only." --json
}
capture_latest_ref_failure() {

View File

@@ -710,6 +710,7 @@ describe("runGatewayUpdate", () => {
it("does not fail a good windows dev preflight only because worktree cleanup hit long paths", async () => {
await setupGitPackageManagerFixture();
const calls: string[] = [];
const cleanupTimeouts: Array<number | undefined> = [];
const upstreamSha = "upstream123";
const doctorNodePath = await resolveStableNodePath(process.execPath);
const doctorCommand = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
@@ -718,7 +719,7 @@ describe("runGatewayUpdate", () => {
try {
const runCommand = async (
argv: string[],
_options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number },
options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number },
) => {
const key = argv.join(" ");
calls.push(key);
@@ -772,6 +773,7 @@ describe("runGatewayUpdate", () => {
key.startsWith(`git -C ${tempDir} worktree remove --force `) &&
preflightPrefixPattern.test(key)
) {
cleanupTimeouts.push(options?.timeoutMs);
return {
stdout: "",
stderr: "error: failed to delete worktree: Filename too long",
@@ -798,6 +800,7 @@ describe("runGatewayUpdate", () => {
expect(result.status).toBe("ok");
const cleanupStep = result.steps.find((step) => step.name === "preflight cleanup");
expect(cleanupStep?.exitCode).toBe(0);
expect(cleanupTimeouts[0]).toBeLessThanOrEqual(60_000);
expect(cleanupStep?.stderrTail ?? "").toContain(
"windows fallback cleanup removed preflight tree",
);
@@ -806,6 +809,101 @@ describe("runGatewayUpdate", () => {
}
});
it("falls back when dev preflight worktree cleanup times out", async () => {
await setupGitPackageManagerFixture();
const calls: string[] = [];
const cleanupTimeouts: Array<number | undefined> = [];
const upstreamSha = "upstream123";
const doctorNodePath = await resolveStableNodePath(process.execPath);
const doctorCommand = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
const runCommand = async (
argv: string[],
options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number },
) => {
const key = argv.join(" ");
calls.push(key);
if (key === `git -C ${tempDir} rev-parse --show-toplevel`) {
return { stdout: tempDir, stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse HEAD`) {
return { stdout: "abc123", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse --abbrev-ref HEAD`) {
return { stdout: "main", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} status --porcelain -- :!dist/control-ui/`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse --abbrev-ref --symbolic-full-name @{upstream}`) {
return { stdout: "origin/main", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} fetch --all --prune --tags`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
return { stdout: upstreamSha, stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rev-list --max-count=10 ${upstreamSha}`) {
return { stdout: `${upstreamSha}\n`, stderr: "", code: 0 };
}
if (key === "pnpm --version") {
return { stdout: "10.0.0", stderr: "", code: 0 };
}
if (
key.startsWith(`git -C ${tempDir} worktree add --detach /tmp/`) &&
key.endsWith(` ${upstreamSha}`) &&
preflightPrefixPattern.test(key)
) {
return { stdout: `HEAD is now at ${upstreamSha}`, stderr: "", code: 0 };
}
if (
key.startsWith("git -C /tmp/") &&
preflightPrefixPattern.test(key) &&
key.includes(" checkout --detach ") &&
key.endsWith(upstreamSha)
) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === "pnpm install" || key === "pnpm build" || key === "pnpm lint") {
return { stdout: "", stderr: "", code: 0 };
}
if (
key.startsWith(`git -C ${tempDir} worktree remove --force `) &&
preflightPrefixPattern.test(key)
) {
cleanupTimeouts.push(options?.timeoutMs);
return {
stdout: "",
stderr: "Command timed out after 60000ms",
code: null,
};
}
if (key === `git -C ${tempDir} worktree prune`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === `git -C ${tempDir} rebase ${upstreamSha}`) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === doctorCommand) {
return { stdout: "", stderr: "", code: 0 };
}
if (key === "pnpm ui:build") {
return { stdout: "", stderr: "", code: 0 };
}
return { stdout: "", stderr: "", code: 0 };
};
const result = await runWithCommand(runCommand, { channel: "dev" });
expect(result.status).toBe("ok");
const cleanupStep = result.steps.find((step) => step.name === "preflight cleanup");
expect(cleanupStep?.exitCode).toBe(0);
expect(cleanupTimeouts[0]).toBeLessThanOrEqual(60_000);
expect(cleanupStep?.stderrTail ?? "").toContain("fallback cleanup removed preflight tree");
});
it("adds heap headroom to windows pnpm build steps during dev updates", async () => {
await setupGitPackageManagerFixture();
const upstreamSha = "upstream123";

View File

@@ -138,6 +138,7 @@ const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]);
const PREFLIGHT_TEMP_PREFIX =
process.platform === "win32" ? "ocu-pf-" : "openclaw-update-preflight-";
const PREFLIGHT_WORKTREE_DIRNAME = process.platform === "win32" ? "wt" : "worktree";
const PREFLIGHT_CLEANUP_TIMEOUT_MS = 60_000;
const WINDOWS_PREFLIGHT_BASE_DIR = "ocu";
const WINDOWS_BUILD_MAX_OLD_SPACE_MB = 4096;
@@ -215,10 +216,7 @@ async function removePathRecursive(target: string) {
.catch(() => {});
}
async function repairWindowsPreflightCleanup(worktreeDir: string, preflightRoot: string) {
if (process.platform !== "win32") {
return false;
}
async function repairPreflightCleanup(worktreeDir: string, preflightRoot: string) {
try {
await fs.rm(worktreeDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
await fs.rm(preflightRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
@@ -938,22 +936,25 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
break;
}
} finally {
const removeStep = await runStep(
step(
const removeStep = await runStep({
...step(
"preflight cleanup",
["git", "-C", gitRoot, "worktree", "remove", "--force", worktreeDir],
gitRoot,
),
);
timeoutMs: Math.min(timeoutMs, PREFLIGHT_CLEANUP_TIMEOUT_MS),
});
if (
removeStep.exitCode !== 0 &&
(await repairWindowsPreflightCleanup(worktreeDir, preflightRoot))
(await repairPreflightCleanup(worktreeDir, preflightRoot))
) {
removeStep.exitCode = 0;
const fallbackMessage =
process.platform === "win32"
? "windows fallback cleanup removed preflight tree"
: "fallback cleanup removed preflight tree";
removeStep.stderrTail = trimLogTail(
[removeStep.stderrTail, "windows fallback cleanup removed preflight tree"]
.filter(Boolean)
.join("\n"),
[removeStep.stderrTail, fallbackMessage].filter(Boolean).join("\n"),
MAX_LOG_CHARS,
);
}