mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 13:21:08 +00:00
fix(update): avoid broad tag fetches for dev updates (#84737)
Summary: - The PR changes dev-channel git updates to fetch branches with `--no-tags`, adds targeted fetching for explicit dev tag refs, updates update-runner tests, and adds a changelog entry. - Reproducibility: yes. Current main source shows dev updates still run a broad tag fetch, and the PR body sup ... al local bare-remote moved-tag reproducer showing that command fails before the branch update can continue. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(update): avoid broad tag fetches for dev updates Validation: - ClawSweeper review passed for head733680b1bc. - Required merge gates passed before the squash merge. Prepared head SHA:733680b1bcReview: https://github.com/openclaw/openclaw/pull/84737#issuecomment-4503692161 Co-authored-by: Ruben Cuevas <hi@rubencu.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/update: avoid fetching unrelated tags during dev-channel git updates so moved release tags do not block branch-based updates. (#84737) Thanks @rubencu.
|
||||
- MiniMax: store OAuth token expiry as an absolute millisecond timestamp so OAuth profiles no longer appear expired on every request. (#83480) Thanks @NianJiuZst.
|
||||
- Agents/Anthropic: strip missing or blank thinking signatures for signed-thinking providers even when recovery supplies a narrow replay policy without signature preservation. Fixes #84430. (#84448) Thanks @NianJiuZst.
|
||||
- Agents/channels: send a visible notice when an aborted main session cannot be resumed after restart, including Telegram group targets. (#85805) Thanks @pfrederiksen.
|
||||
|
||||
@@ -386,7 +386,7 @@ describe("runGatewayUpdate", () => {
|
||||
{ name: "target ref", options: { devTargetRef: "main" } },
|
||||
] as const)("stops dev update when fetch fails before resolving $name", async ({ options }) => {
|
||||
await setupGitCheckout();
|
||||
const fetchCommand = `git -C ${tempDir} fetch --all --prune --tags`;
|
||||
const fetchCommand = `git -C ${tempDir} fetch --all --prune --no-tags`;
|
||||
const { runner, calls } = createRunner({
|
||||
...buildGitWorktreeProbeResponses(),
|
||||
[fetchCommand]: {
|
||||
@@ -403,6 +403,102 @@ describe("runGatewayUpdate", () => {
|
||||
expect(calls.slice(calls.indexOf(fetchCommand) + 1)).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("does not fetch tags for dev updates", async () => {
|
||||
await setupGitPackageManagerFixture();
|
||||
const upstreamSha = "upstream123";
|
||||
const doctorNodePath = await resolveStableNodePath(process.execPath);
|
||||
const doctorCommand = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
|
||||
const { runner, calls } = createRunner({
|
||||
...buildGitWorktreeProbeResponses(),
|
||||
[`git -C ${tempDir} fetch --all --prune --no-tags`]: { stdout: "" },
|
||||
[`git -C ${tempDir} rev-parse --abbrev-ref --symbolic-full-name @{upstream}`]: {
|
||||
stdout: "origin/main",
|
||||
},
|
||||
[`git -C ${tempDir} rev-parse @{upstream}`]: { stdout: upstreamSha },
|
||||
[`git -C ${tempDir} rev-list --max-count=10 ${upstreamSha}`]: {
|
||||
stdout: `${upstreamSha}\n`,
|
||||
},
|
||||
[`git -C ${tempDir} rebase ${upstreamSha}`]: { stdout: "" },
|
||||
"pnpm --version": { stdout: "10.0.0" },
|
||||
"pnpm install": { stdout: "" },
|
||||
"pnpm build": { stdout: "" },
|
||||
"pnpm ui:build": { stdout: "" },
|
||||
[doctorCommand]: { stdout: "" },
|
||||
});
|
||||
|
||||
const result = await runWithRunner(runner, { channel: "dev" });
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
expect(calls).toContain(`git -C ${tempDir} fetch --all --prune --no-tags`);
|
||||
expect(calls).not.toContain(`git -C ${tempDir} fetch --all --prune --tags`);
|
||||
});
|
||||
|
||||
it("fetches only the requested tag for explicit dev tag target refs", async () => {
|
||||
await setupGitPackageManagerFixture();
|
||||
const targetSha = "2222222222222222222222222222222222222222";
|
||||
const doctorNodePath = await resolveStableNodePath(process.execPath);
|
||||
const doctorCommand = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
|
||||
const { runner, calls } = createRunner({
|
||||
...buildGitWorktreeProbeResponses(),
|
||||
[`git -C ${tempDir} fetch --all --prune --no-tags`]: { stdout: "" },
|
||||
[`git -C ${tempDir} remote`]: { stdout: "origin\n" },
|
||||
[`git -C ${tempDir} fetch origin +refs/tags/v2026.5.19-beta.2:refs/tags/v2026.5.19-beta.2`]: {
|
||||
stdout: "",
|
||||
},
|
||||
[`git -C ${tempDir} rev-parse refs/tags/v2026.5.19-beta.2^{}`]: {
|
||||
stdout: `${targetSha}\n`,
|
||||
},
|
||||
[`git -C ${tempDir} rev-list --max-count=10 ${targetSha}`]: {
|
||||
stdout: `${targetSha}\n`,
|
||||
},
|
||||
[`git -C ${tempDir} checkout --detach ${targetSha}`]: { stdout: "" },
|
||||
"pnpm --version": { stdout: "10.0.0" },
|
||||
"pnpm install": { stdout: "" },
|
||||
"pnpm build": { stdout: "" },
|
||||
"pnpm ui:build": { stdout: "" },
|
||||
[doctorCommand]: { stdout: "" },
|
||||
});
|
||||
|
||||
const result = await runWithRunner(runner, {
|
||||
channel: "dev",
|
||||
devTargetRef: "refs/tags/v2026.5.19-beta.2",
|
||||
});
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
expect(calls).toContain(`git -C ${tempDir} fetch --all --prune --no-tags`);
|
||||
expect(calls).not.toContain(`git -C ${tempDir} fetch --all --prune --tags`);
|
||||
expect(calls).toContain(
|
||||
`git -C ${tempDir} fetch origin +refs/tags/v2026.5.19-beta.2:refs/tags/v2026.5.19-beta.2`,
|
||||
);
|
||||
expect(calls).toContain(`git -C ${tempDir} rev-parse refs/tags/v2026.5.19-beta.2^{}`);
|
||||
});
|
||||
|
||||
it("does not resolve stale local dev tag target refs after targeted tag fetch failure", async () => {
|
||||
await setupGitCheckout();
|
||||
const { runner, calls } = createRunner({
|
||||
...buildGitWorktreeProbeResponses(),
|
||||
[`git -C ${tempDir} fetch --all --prune --no-tags`]: { stdout: "" },
|
||||
[`git -C ${tempDir} remote`]: { stdout: "origin\n" },
|
||||
[`git -C ${tempDir} fetch origin +refs/tags/v2026.5.19-beta.2:refs/tags/v2026.5.19-beta.2`]: {
|
||||
code: 1,
|
||||
stderr: "would clobber existing tag",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runWithRunner(runner, {
|
||||
channel: "dev",
|
||||
devTargetRef: "refs/tags/v2026.5.19-beta.2",
|
||||
});
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("no-target-sha");
|
||||
expect(calls).toContain(
|
||||
`git -C ${tempDir} fetch origin +refs/tags/v2026.5.19-beta.2:refs/tags/v2026.5.19-beta.2`,
|
||||
);
|
||||
expect(calls).not.toContain(`git -C ${tempDir} rev-parse refs/tags/v2026.5.19-beta.2^{}`);
|
||||
expect(calls).not.toContain(`git -C ${tempDir} rev-parse refs/tags/v2026.5.19-beta.2`);
|
||||
});
|
||||
|
||||
it("aborts rebase on failure", async () => {
|
||||
await setupGitCheckout();
|
||||
const { runner, calls } = createRunner({
|
||||
@@ -537,7 +633,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
@@ -758,7 +854,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
@@ -873,7 +969,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
@@ -970,7 +1066,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
@@ -1084,7 +1180,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
@@ -1182,7 +1278,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
@@ -1275,7 +1371,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
@@ -1367,7 +1463,7 @@ describe("runGatewayUpdate", () => {
|
||||
if (key === `git -C ${tempDir} status --porcelain -- :!dist/control-ui/`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --tags`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse ${targetSha}`) {
|
||||
@@ -1447,7 +1543,7 @@ describe("runGatewayUpdate", () => {
|
||||
if (key === `git -C ${tempDir} status --porcelain -- :!dist/control-ui/`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --tags`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse refs/remotes/origin/main`) {
|
||||
@@ -1530,7 +1626,7 @@ describe("runGatewayUpdate", () => {
|
||||
if (key === `git -C ${gitRoot} status --porcelain -- :!dist/control-ui/`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${gitRoot} fetch --all --prune --tags`) {
|
||||
if (key === `git -C ${gitRoot} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${gitRoot} rev-parse refs/remotes/origin/main`) {
|
||||
@@ -1606,7 +1702,7 @@ describe("runGatewayUpdate", () => {
|
||||
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`) {
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --no-tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
|
||||
@@ -442,6 +442,11 @@ function looksLikeFullCommitSha(value: string): boolean {
|
||||
return /^[0-9a-f]{40}$/i.test(value.trim());
|
||||
}
|
||||
|
||||
function resolveTagFetchRef(candidate: string): string | null {
|
||||
const ref = candidate.endsWith("^{}") ? candidate.slice(0, -"^{}".length) : candidate;
|
||||
return ref.startsWith("refs/tags/") ? ref : null;
|
||||
}
|
||||
|
||||
function buildDevTargetRefResolutionCandidates(devTargetRef: string): string[] {
|
||||
const trimmed = devTargetRef.trim();
|
||||
const candidates: string[] = [];
|
||||
@@ -573,7 +578,26 @@ function isSupersededInstallFailure(
|
||||
}
|
||||
|
||||
function findBlockingGitFailure(steps: readonly UpdateStepResult[]): UpdateStepResult | undefined {
|
||||
return steps.find((step) => step.exitCode !== 0 && !isSupersededInstallFailure(step, steps));
|
||||
return steps.find(
|
||||
(step, index) =>
|
||||
step.exitCode !== 0 &&
|
||||
!isSupersededInstallFailure(step, steps) &&
|
||||
!isSupersededTargetRefFailure(step, steps.slice(index + 1)),
|
||||
);
|
||||
}
|
||||
|
||||
function isSupersededTargetRefFailure(
|
||||
step: UpdateStepResult,
|
||||
followingSteps: readonly UpdateStepResult[],
|
||||
): boolean {
|
||||
const isTargetRefProbe = step.name.startsWith("git rev-parse ");
|
||||
const isTargetTagFetch = step.name.startsWith("git fetch ") && step.name.includes(" refs/tags/");
|
||||
if (!isTargetRefProbe && !isTargetTagFetch) {
|
||||
return false;
|
||||
}
|
||||
return followingSteps.some(
|
||||
(candidate) => candidate.name.startsWith("git rev-parse ") && candidate.exitCode === 0,
|
||||
);
|
||||
}
|
||||
|
||||
function mergeCommandEnvironments(
|
||||
@@ -876,7 +900,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
|
||||
const fetchFailure = await runRequiredGitStep(
|
||||
"git fetch",
|
||||
["git", "-C", gitRoot, "fetch", "--all", "--prune", "--tags"],
|
||||
["git", "-C", gitRoot, "fetch", "--all", "--prune", "--no-tags"],
|
||||
"fetch-failed",
|
||||
);
|
||||
if (fetchFailure) {
|
||||
@@ -887,6 +911,35 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
if (devTargetRef) {
|
||||
let targetSha: string | null = null;
|
||||
for (const targetRefCandidate of buildDevTargetRefResolutionCandidates(devTargetRef)) {
|
||||
const tagFetchRef = resolveTagFetchRef(targetRefCandidate);
|
||||
if (tagFetchRef) {
|
||||
const remoteListStep = await runStep(
|
||||
step("git remote", ["git", "-C", gitRoot, "remote"], gitRoot),
|
||||
);
|
||||
steps.push(remoteListStep);
|
||||
const remotes = (remoteListStep.stdoutTail ?? "")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
let fetchedTag = false;
|
||||
for (const remote of remotes) {
|
||||
const targetTagFetchStep = await runStep(
|
||||
step(
|
||||
`git fetch ${remote} ${tagFetchRef}`,
|
||||
["git", "-C", gitRoot, "fetch", remote, `+${tagFetchRef}:${tagFetchRef}`],
|
||||
gitRoot,
|
||||
),
|
||||
);
|
||||
steps.push(targetTagFetchStep);
|
||||
if (targetTagFetchStep.exitCode === 0) {
|
||||
fetchedTag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (remotes.length > 0 && !fetchedTag) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const targetShaStep = await runStep(
|
||||
step(
|
||||
`git rev-parse ${targetRefCandidate}`,
|
||||
|
||||
Reference in New Issue
Block a user