mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-09 03:22:40 +00:00
fix(update): roll back failed git updates
This commit is contained in:
@@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/discovery: strip `-plugin` package suffixes when deriving plugin id hints so package names line up with manifest ids. (#85170) Thanks @JulyanXu.
|
||||
- Tlon: stop advertising a non-existent agent tool contract in the plugin manifest.
|
||||
- Telegram: preserve fenced code block languages through Markdown rendering so Telegram receives `language-*` code classes. (#85209) Thanks @leno23.
|
||||
- Windows updates: roll back git-backed updates to the previous checkout when dependency install, build, UI build, or doctor repair fails.
|
||||
- Windows installer: persist user-local portable Git on PATH and activate the repo-pinned pnpm version for git-backed installs and updates.
|
||||
- Windows installer: bootstrap a user-local portable Node.js when native Windows has no Node and no winget, Chocolatey, or Scoop, so first-run installs can continue on raw hosts.
|
||||
- Windows installer: extract the downloaded portable Node.js directory with native `tar` before falling back to .NET zip extraction, avoiding PowerShell 5.1 archive and path-length failures.
|
||||
|
||||
@@ -420,6 +420,7 @@ describe("runGatewayUpdate", () => {
|
||||
const stableTag = "v1.0.1-1";
|
||||
const { runner, calls } = createRunner({
|
||||
...buildStableTagResponses(stableTag),
|
||||
[`git -C ${tempDir} rev-parse --abbrev-ref HEAD`]: { stdout: "main" },
|
||||
"pnpm install": { code: 1, stderr: "ERR_PNPM_NETWORK" },
|
||||
});
|
||||
|
||||
@@ -429,6 +430,12 @@ describe("runGatewayUpdate", () => {
|
||||
expect(result.reason).toBe("deps-install-failed");
|
||||
expect(calls).not.toContain("pnpm build");
|
||||
expect(calls).not.toContain("pnpm ui:build");
|
||||
expect(calls).toContain(`git -C ${tempDir} reset --hard`);
|
||||
expect(calls).toContain(`git -C ${tempDir} checkout --force main`);
|
||||
expect(calls).toContain(`git -C ${tempDir} reset --hard abc123`);
|
||||
expect(calls.indexOf(`git -C ${tempDir} reset --hard`)).toBeLessThan(
|
||||
calls.indexOf(`git -C ${tempDir} checkout --force main`),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses pnpm highest resolution mode for update installs", async () => {
|
||||
@@ -591,6 +598,7 @@ describe("runGatewayUpdate", () => {
|
||||
const stableTag = "v1.0.1-1";
|
||||
const { runner, calls } = createRunner({
|
||||
...buildStableTagResponses(stableTag),
|
||||
[`git -C ${tempDir} rev-parse --abbrev-ref HEAD`]: { stdout: "main" },
|
||||
"pnpm install": { stdout: "" },
|
||||
"pnpm build": { code: 1, stderr: "tsc: error TS2345" },
|
||||
});
|
||||
@@ -601,6 +609,9 @@ describe("runGatewayUpdate", () => {
|
||||
expect(result.reason).toBe("build-failed");
|
||||
expect(calls).toContain("pnpm install");
|
||||
expect(calls).not.toContain("pnpm ui:build");
|
||||
expect(calls).toContain(`git -C ${tempDir} reset --hard`);
|
||||
expect(calls).toContain(`git -C ${tempDir} checkout --force main`);
|
||||
expect(calls).toContain(`git -C ${tempDir} reset --hard abc123`);
|
||||
});
|
||||
|
||||
it("uses stable tag when beta tag is older than release", async () => {
|
||||
@@ -2175,7 +2186,8 @@ describe("runGatewayUpdate", () => {
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("doctor-entry-missing");
|
||||
expect(result.steps.at(-1)?.name).toBe("openclaw doctor entry");
|
||||
expect(result.steps.some((step) => step.name === "openclaw doctor entry")).toBe(true);
|
||||
expect(result.steps.at(-1)?.name).toMatch(/^git rollback/);
|
||||
});
|
||||
|
||||
it("repairs UI assets when doctor run removes control-ui files", async () => {
|
||||
@@ -2222,5 +2234,6 @@ describe("runGatewayUpdate", () => {
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("ui-assets-missing");
|
||||
expect(result.steps.at(-1)?.name).toMatch(/^git rollback/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ export type UpdateInstallSurface =
|
||||
|
||||
function mapManagerResolutionFailure(
|
||||
reason: UpdatePackageManagerFailureReason,
|
||||
): UpdateRunResult["reason"] {
|
||||
): NonNullable<UpdateRunResult["reason"]> {
|
||||
return reason;
|
||||
}
|
||||
|
||||
@@ -766,7 +766,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
const beforeVersion = await readPackageVersion(gitRoot);
|
||||
const channel: UpdateChannel = opts.channel ?? "dev";
|
||||
const devTargetRef = channel === "dev" ? normalizeDevTargetRef(opts.devTargetRef) : null;
|
||||
const branch = channel === "dev" ? await readBranchName(runCommand, gitRoot, timeoutMs) : null;
|
||||
const branch = await readBranchName(runCommand, gitRoot, timeoutMs);
|
||||
const needsCheckoutMain = channel === "dev" && !devTargetRef && branch !== DEV_BRANCH;
|
||||
gitTotalSteps = channel === "dev" ? (needsCheckoutMain ? 11 : 10) : 9;
|
||||
const buildGitErrorResult = (reason: string): UpdateRunResult => ({
|
||||
@@ -786,6 +786,60 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const appendRecoveryStep = async (name: string, argv: string[]) => {
|
||||
const started = Date.now();
|
||||
const result = await runCommand(argv, { cwd: gitRoot, timeoutMs });
|
||||
const recoveryStep: UpdateStepResult = {
|
||||
name,
|
||||
command: argv.join(" "),
|
||||
cwd: gitRoot,
|
||||
durationMs: Date.now() - started,
|
||||
exitCode: result.code,
|
||||
stdoutTail: trimLogTail(result.stdout, MAX_LOG_CHARS),
|
||||
stderrTail: trimLogTail(result.stderr, MAX_LOG_CHARS),
|
||||
};
|
||||
steps.push(recoveryStep);
|
||||
return recoveryStep.exitCode === 0;
|
||||
};
|
||||
const rollbackGitCheckout = async () => {
|
||||
if (!beforeSha) {
|
||||
return;
|
||||
}
|
||||
await appendRecoveryStep("git rollback clean", ["git", "-C", gitRoot, "reset", "--hard"]);
|
||||
if (branch && branch !== "HEAD") {
|
||||
const checkedOutBranch = await appendRecoveryStep("git rollback checkout", [
|
||||
"git",
|
||||
"-C",
|
||||
gitRoot,
|
||||
"checkout",
|
||||
"--force",
|
||||
branch,
|
||||
]);
|
||||
if (checkedOutBranch) {
|
||||
await appendRecoveryStep("git rollback reset", [
|
||||
"git",
|
||||
"-C",
|
||||
gitRoot,
|
||||
"reset",
|
||||
"--hard",
|
||||
beforeSha,
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await appendRecoveryStep("git rollback checkout", [
|
||||
"git",
|
||||
"-C",
|
||||
gitRoot,
|
||||
"checkout",
|
||||
"--detach",
|
||||
beforeSha,
|
||||
]);
|
||||
};
|
||||
const buildGitErrorResultWithRollback = async (reason: string): Promise<UpdateRunResult> => {
|
||||
await rollbackGitCheckout();
|
||||
return buildGitErrorResult(reason);
|
||||
};
|
||||
|
||||
const statusCheck = await runStep(
|
||||
step(
|
||||
@@ -1212,15 +1266,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
"require-preferred",
|
||||
);
|
||||
if (manager.kind === "missing-required") {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: mapManagerResolutionFailure(manager.reason),
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback(mapManagerResolutionFailure(manager.reason));
|
||||
}
|
||||
try {
|
||||
const installEnv = resolveInstallEnv(manager.manager, manager.env);
|
||||
@@ -1247,15 +1293,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
}
|
||||
}
|
||||
if (finalDepsStep.exitCode !== 0) {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "deps-install-failed",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback("deps-install-failed");
|
||||
}
|
||||
|
||||
const buildStep = await runStep(
|
||||
@@ -1268,15 +1306,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
);
|
||||
steps.push(buildStep);
|
||||
if (buildStep.exitCode !== 0) {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "build-failed",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback("build-failed");
|
||||
}
|
||||
|
||||
const uiBuildStep = await runStep(
|
||||
@@ -1284,15 +1314,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
);
|
||||
steps.push(uiBuildStep);
|
||||
if (uiBuildStep.exitCode !== 0) {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "ui-build-failed",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback("ui-build-failed");
|
||||
}
|
||||
|
||||
const doctorEntry = path.join(gitRoot, "openclaw.mjs");
|
||||
@@ -1309,15 +1331,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
exitCode: 1,
|
||||
stderrTail: `missing ${doctorEntry}`,
|
||||
});
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "doctor-entry-missing",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback("doctor-entry-missing");
|
||||
}
|
||||
|
||||
// Use --fix so that doctor auto-strips unknown config keys introduced by
|
||||
@@ -1335,15 +1349,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
);
|
||||
steps.push(doctorStep);
|
||||
if (doctorStep.exitCode !== 0) {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "doctor-failed",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback("doctor-failed");
|
||||
}
|
||||
|
||||
const uiIndexHealth = await resolveControlUiDistIndexHealth({ root: gitRoot });
|
||||
@@ -1367,15 +1373,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
steps.push(repairStep);
|
||||
|
||||
if (repairResult.code !== 0) {
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "ui-build-failed",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback("ui-build-failed");
|
||||
}
|
||||
|
||||
const repairedUiIndexHealth = await resolveControlUiDistIndexHealth({ root: gitRoot });
|
||||
@@ -1390,15 +1388,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
exitCode: 1,
|
||||
stderrTail: `missing ${uiIndexPath}`,
|
||||
});
|
||||
return {
|
||||
status: "error",
|
||||
mode: "git",
|
||||
root: gitRoot,
|
||||
reason: "ui-assets-missing",
|
||||
before: { sha: beforeSha, version: beforeVersion },
|
||||
steps,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
return await buildGitErrorResultWithRollback("ui-assets-missing");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user