diff --git a/packages/coding-agent/src/utils/git.ts b/packages/coding-agent/src/utils/git.ts index 1db42b6d4..16dcfbc6b 100644 --- a/packages/coding-agent/src/utils/git.ts +++ b/packages/coding-agent/src/utils/git.ts @@ -1126,9 +1126,14 @@ export async function commit(cwd: string, message: string, options: CommitOption return runChecked(cwd, args, { signal: options.signal, stdin: message }); } -/** Push the current branch. */ +/** Push the current branch (branch-scoped: never follows tags). */ export async function push(cwd: string, options: PushOptions = {}): Promise { - const args = ["push"]; + // `--no-follow-tags` overrides a user's `push.followTags = true`, which + // would otherwise ride every reachable annotated tag along with the + // branch — rejected refs ("permission denied") on remotes the user + // cannot tag (e.g. PR-head forks), failing the call after the branch + // itself already updated. Tool pushes push exactly the named refspec. + const args = ["push", "--no-follow-tags"]; if (options.forceWithLease) args.push("--force-with-lease"); if (options.remote) args.push(options.remote); if (options.refspec) args.push(options.refspec); diff --git a/packages/coding-agent/test/git-process-config.test.ts b/packages/coding-agent/test/git-process-config.test.ts index 516000402..65e5ee039 100644 --- a/packages/coding-agent/test/git-process-config.test.ts +++ b/packages/coding-agent/test/git-process-config.test.ts @@ -87,4 +87,27 @@ describe("git subprocess config", () => { "tracked.txt", ]); }); + + it("scopes pushes to the named refspec, never following tags", async () => { + const spawnCalls: SpawnCall[] = []; + vi.spyOn(Bun, "spawn").mockImplementation(createSpawnMock(spawnCalls)); + + await git.push("/work/pi", { remote: "fork", refspec: "HEAD:refs/heads/feature" }); + + // `--no-follow-tags` must override a user's `push.followTags = true`: + // implicit tag pushes are rejected on remotes the user cannot tag + // (e.g. PR-head forks) and fail the call after the branch updated. + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]?.cmd).toEqual([ + "git", + "-c", + "core.fsmonitor=false", + "-c", + "core.untrackedCache=false", + "push", + "--no-follow-tags", + "fork", + "HEAD:refs/heads/feature", + ]); + }); });