Skip to content

fix(code): pre-configure worktree with GitHub noreply email#2342

Open
lost-particles wants to merge 1 commit into
PostHog:mainfrom
lost-particles:fix/preconfigure-noreply-identity-2156
Open

fix(code): pre-configure worktree with GitHub noreply email#2342
lost-particles wants to merge 1 commit into
PostHog:mainfrom
lost-particles:fix/preconfigure-noreply-identity-2156

Conversation

@lost-particles
Copy link
Copy Markdown
Contributor

Closes #2156

Summary

  • When the user has "Keep my email address private" enabled on GitHub, the first git push is rejected with GH007 ("Your push would publish a private email address"). The agent's recovery from this was inconsistent — sometimes it discovered the noreply email via gh/git history and amended the commit, sometimes it gave up and asked the user to either toggle the GitHub setting or paste the noreply.
  • This change makes the case deterministic by pre-configuring the new worktree with the user's GitHub noreply identity at creation time, so the rejection never happens. Lookup is via gh api user; when GitHub reports a public email, the worktree is left untouched.
  • The config is scoped to the worktree via extensions.worktreeConfig + git config --worktree, so the user's main repo identity is never modified.

Behavior

User state Outcome
Email privacy on + gh authenticated Worktree gets <id>+<login>@users.noreply.github.com; push succeeds without agent recovery
Public email on GitHub gh api user returns a non-null email → no-op, worktree unchanged
gh missing / not authenticated Debug-log + no-op; existing agent-side recovery still applies as a fallback

The pre-configuration is best-effort and wrapped in try/catch — any failure logs at debug level and does not break workspace creation.

Test plan

  • pnpm --filter code test src/main/services/workspace/githubIdentity.test.ts — 9 new tests cover the pure helper, mocked gh failure modes (not installed, malformed JSON, missing fields), and a real-git integration test verifying the noreply lands only on the worktree (main repo identity unchanged).
  • pnpm --filter code test src/main — all 497 existing main-process tests still pass, including the archive integration test that exercises real worktree creation.
  • pnpm --filter code exec biome check on changed files — clean.
  • Manual: with a GitHub account that has email privacy enabled and gh auth login done, create a new task workspace and run git config user.email in the resulting worktree — should be <id>+<login>@users.noreply.github.com. The main repo's git config --local user.email should be unchanged.
  • Manual: with a GitHub account that exposes a public email, repeat — worktree user.email should fall through to the user's global config, not the noreply.

Notes

…rivate

Closes PostHog#2156

When the user has "Keep my email address private" enabled on GitHub,
commits authored with their real email get rejected on push with GH007
("Your push would publish a private email address"). The agent previously
had to recover from this error, and its behavior was inconsistent — sometimes
it discovered the noreply email and amended the commit, sometimes it fell
back to asking the user to either toggle the GitHub setting or paste the
noreply address.

This fix prevents the rejection from happening in the first place. After
creating a worktree, we look up the authenticated GitHub user via `gh api
user` and, when their public email is null (privacy enabled), set
`user.name` / `user.email` on the worktree to the
`<id>+<login>@users.noreply.github.com` form. The config is scoped to the
worktree via `extensions.worktreeConfig`, so the user's main repo identity
is untouched.

The pre-configuration is best-effort: if `gh` is missing, unauthenticated,
or the user has a public email, it's a no-op and the existing agent-side
recovery still applies.
@lost-particles
Copy link
Copy Markdown
Contributor Author

Hi maintainers, this PR addresses #2156. Whenever convenient, could someone from the team take a look and let me know if the approach (pre-configuring the worktree with the GitHub noreply identity at creation time) lines up with how you would like to solve this? Happy to iterate on scope, naming, or placement based on your feedback. Thanks!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

Comments Outside Diff (1)

  1. apps/code/src/main/services/workspace/service.ts, line 1061-1064 (link)

    P1 Missing noreply pre-configuration in promotion path

    promoteToWorktree creates a real worktree (line 1061) but never calls preconfigureNoreplyIdentity, so any task that is promoted from local mode to a worktree still gets a bare worktree with no noreply identity. The first git push from that worktree will still hit GH007 for users with email privacy enabled, defeating the stated goal of making the case "deterministic".

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/code/src/main/services/workspace/service.ts
    Line: 1061-1064
    
    Comment:
    **Missing noreply pre-configuration in promotion path**
    
    `promoteToWorktree` creates a real worktree (line 1061) but never calls `preconfigureNoreplyIdentity`, so any task that is promoted from local mode to a worktree still gets a bare worktree with no noreply identity. The first `git push` from that worktree will still hit GH007 for users with email privacy enabled, defeating the stated goal of making the case "deterministic".
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/code/src/main/services/workspace/service.ts:1061-1064
**Missing noreply pre-configuration in promotion path**

`promoteToWorktree` creates a real worktree (line 1061) but never calls `preconfigureNoreplyIdentity`, so any task that is promoted from local mode to a worktree still gets a bare worktree with no noreply identity. The first `git push` from that worktree will still hit GH007 for users with email privacy enabled, defeating the stated goal of making the case "deterministic".

### Issue 2 of 2
apps/code/src/main/services/workspace/githubIdentity.test.ts:19-100
**Prefer parameterised tests**

The `computeNoreplyIdentity` and `fetchGitHubUserInfo` suites each have four independent `it()` cases that vary only in input/output — these are a natural fit for `it.each` tables. Parameterised tests are the convention in this codebase and keep the intent easier to scan as the number of cases grows.

Reviews (1): Last reviewed commit: "fix(code): pre-configure worktree with G..." | Re-trigger Greptile

Comment on lines +19 to +100
describe("computeNoreplyIdentity", () => {
it("returns noreply identity when GitHub user has email privacy enabled", () => {
expect(
computeNoreplyIdentity({
id: 12345,
login: "octocat",
name: "Octo Cat",
email: null,
}),
).toEqual({
name: "Octo Cat",
email: "12345+octocat@users.noreply.github.com",
});
});

it("falls back to login when name is missing", () => {
expect(
computeNoreplyIdentity({ id: 12345, login: "octocat", email: null }),
).toEqual({
name: "octocat",
email: "12345+octocat@users.noreply.github.com",
});
});

it("returns null when GitHub user has a public email", () => {
expect(
computeNoreplyIdentity({
id: 12345,
login: "octocat",
email: "octo@example.com",
}),
).toBeNull();
});

it("returns null when id or login is missing", () => {
expect(
computeNoreplyIdentity({
id: 0,
login: "octocat",
email: null,
}),
).toBeNull();
expect(
computeNoreplyIdentity({
id: 12345,
login: "",
email: null,
}),
).toBeNull();
});
});

describe("fetchGitHubUserInfo", () => {
it("returns parsed user info when gh api user succeeds", async () => {
const runGh = vi
.fn()
.mockResolvedValue(
'{"id":12345,"login":"octocat","name":"Octo Cat","email":null}',
);
const result = await fetchGitHubUserInfo({ runGh });
expect(result).toEqual({
id: 12345,
login: "octocat",
name: "Octo Cat",
email: null,
});
expect(runGh).toHaveBeenCalledWith(["api", "user"]);
});

it("returns null when gh fails (not installed / not authenticated)", async () => {
const runGh = vi.fn().mockRejectedValue(new Error("gh: command not found"));
expect(await fetchGitHubUserInfo({ runGh })).toBeNull();
});

it("returns null when response is malformed JSON", async () => {
const runGh = vi.fn().mockResolvedValue("not json");
expect(await fetchGitHubUserInfo({ runGh })).toBeNull();
});

it("returns null when required fields are missing", async () => {
const runGh = vi.fn().mockResolvedValue('{"login":"octocat"}');
expect(await fetchGitHubUserInfo({ runGh })).toBeNull();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer parameterised tests

The computeNoreplyIdentity and fetchGitHubUserInfo suites each have four independent it() cases that vary only in input/output — these are a natural fit for it.each tables. Parameterised tests are the convention in this codebase and keep the intent easier to scan as the number of cases grows.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/workspace/githubIdentity.test.ts
Line: 19-100

Comment:
**Prefer parameterised tests**

The `computeNoreplyIdentity` and `fetchGitHubUserInfo` suites each have four independent `it()` cases that vary only in input/output — these are a natural fit for `it.each` tables. Parameterised tests are the convention in this codebase and keep the intent easier to scan as the number of cases grows.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent inconsistently auto-detects GitHub noreply email when push is blocked

1 participant