fix(update): fail the git updater when the final HEAD verification probe errors (#129036)

The git updater classified blocking step failures before appending the final
"git rev-parse HEAD (after)" probe, so a nonzero exit from that probe was
invisible to findBlockingGitFailure. The result could report status:"ok"
with a null after.sha even though the installed revision was never verified.

Route the failed probe through rollbackError("head-verification-failed"),
matching every other failure path, so the checkout is rolled back and no
after identity is fabricated.

Fixes #127412
This commit is contained in:
SunnyShu
2026-08-26 00:24:11 +08:00
committed by GitHub
parent b0e9fd6252
commit 336ce945f0
2 changed files with 40 additions and 0 deletions
+3
View File
@@ -551,6 +551,9 @@ export async function updateGitCheckout(params: {
const afterShaStep = await runStep(
step("git rev-parse HEAD (after)", ["git", "-C", gitRoot, "rev-parse", "HEAD"], gitRoot),
);
if (afterShaStep.exitCode !== 0) {
return await rollbackError("head-verification-failed");
}
if (
devTarget?.mode === "tracked" &&
devPreflight?.status === "ok" &&
+37
View File
@@ -1701,6 +1701,43 @@ describe("runGatewayUpdate", () => {
expect(calls).toContain(`git -C ${tempDir} reset --hard abc123`);
});
it("rolls back and reports error when the final HEAD verification probe fails", async () => {
await setupGitCheckout({ packageManager: "pnpm@8.0.0" });
await setupUiIndex();
const stableTag = "v1.0.1-1";
const doctorNodePath = await resolveStableNodePath(process.execPath);
const { runner, calls } = createRunner({
...buildStableTagResponses(stableTag),
[`git -C ${tempDir} rev-parse --abbrev-ref HEAD`]: { stdout: "main" },
"pnpm install": { stdout: "" },
"pnpm build": { stdout: "" },
"pnpm ui:build": { stdout: "" },
[`${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`]: {
stdout: "",
},
});
let revParseHeadCount = 0;
const runCommand = async (argv: string[]) => {
const key = argv.join(" ");
if (key === `git -C ${tempDir} rev-parse HEAD`) {
revParseHeadCount += 1;
if (revParseHeadCount === 2) {
return toCommandResult({ code: 1, stderr: "fatal: not a valid object name HEAD" });
}
}
return runner(argv);
};
const result = await runWithCommand(runCommand, { channel: "stable" });
expect(result.status).toBe("error");
expect(result.reason).toBe("head-verification-failed");
expect(result.after).toBeUndefined();
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 () => {
await setupGitCheckout({ packageManager: "pnpm@8.0.0" });
await setupUiIndex();