Skip to content

Target pull requests at upstream repositories#3900

Open
jakeleventhal wants to merge 16 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/target-upstream-prs
Open

Target pull requests at upstream repositories#3900
jakeleventhal wants to merge 16 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/target-upstream-prs

Conversation

@jakeleventhal

@jakeleventhal jakeleventhal commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • resolve a GitHub repository's fork parent before pull request operations
  • scope open-PR lookup, default-branch resolution, and PR creation to that upstream repository
  • cache base-repository resolution briefly and cover fork-origin workflows with regression tests

Why

When a T3 Code checkout used a fork as origin, the commit/push/create-PR flow and the native create-PR action allowed GitHub CLI to infer the target repository from that fork. As a result, new PRs could be opened against the fork instead of its upstream source repository.

This change uses GitHub's parent repository metadata and passes the resolved repository explicitly, while preserving same-repository behavior for non-forks.

Validation

  • vp check
  • vp run typecheck
  • focused GitHub CLI and GitManager tests: 13 passed
  • live fork-parent resolution verified for jakeleventhal/t3code to pingdotgg/t3code

Note

Target pull requests at upstream repositories when origin is a fork

  • PR creation from forks now uses the GitHub API (gh api) to POST against the upstream base repository, with the head selector qualified by owner to support organization-owned forks.
  • All gh CLI calls (list, view, checkout, default branch) are scoped to the resolved base repository via --repo, using a cached gh repo view context resolution.
  • resolveBaseRangeRef in GitManager.ts fetches the base commit from the upstream remote when the origin URL differs from the target repository's clone URLs, falling back to local tracking if the fetch fails.
  • getTargetRepositoryCloneUrls is added to the SourceControlProvider interface and implemented in GitHubSourceControlProvider to supply upstream clone URLs to consumers.
  • parseGitHubRepositoryNameWithOwnerFromRemoteUrl now supports GitHub Enterprise URL formats and rejects non-GitHub remotes.
  • Risk: listPullRequests now fetches up to 100 open PR candidates (previously 1), increasing gh API call cost when searching for an existing open PR.

Macroscope summarized 2edec75.


Note

Medium Risk
Changes core PR create/list/checkout and diff-base resolution for fork workflows; mistakes could open PRs on the wrong repo or break PR generation, but behavior is heavily regression-tested.

Overview
When origin is a fork, commit/push/create-PR flows now resolve the parent (upstream) repository and run GitHub CLI against that repo instead of inferring targets from the fork.

GitHubCli caches per-cwd base/head context from gh repo view (parent vs current repo). PR list, view, checkout, and default branch pass --repo to the base repo. Fork PR creation uses gh api with explicit head, head_repo, and host; same-repo creation still uses gh pr create with qualified heads. listOpenPullRequests becomes listPullRequests with configurable state; list decoding is centralized.

GitManager raises open-PR candidate scans to 100, tightens matchesBranchHeadContext for cross-repo mismatches, and resolveBaseRangeRef fetches the upstream base via getTargetRepositoryCloneUrls (new on the GitHub provider/registry) so generated PR content uses the upstream diff range; failed upstream fetch on forks aborts before generating content.

parseGitHubRepositoryNameWithOwnerFromRemoteUrl in shared git now supports GitHub Enterprise hosts and ignores non-GitHub remotes. Tests cover fork create/reuse, GHE, and upstream range behavior.

Reviewed by Cursor Bugbot for commit 2edec75. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d0e3946f-f4d9-41a3-bc5b-d96ab9f5a519

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 11, 2026
@jakeleventhal jakeleventhal marked this pull request as ready for review July 11, 2026 22:10
Comment thread apps/server/src/sourceControl/GitHubCli.ts
Comment thread apps/server/src/sourceControl/GitHubCli.ts
Comment thread apps/server/src/sourceControl/GitHubCli.ts
@macroscopeapp

macroscopeapp Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces new fork-to-upstream PR targeting functionality with significant runtime behavior changes. Multiple unresolved P1/P2 review comments identify potential bugs in CLI argument handling, GitHub Enterprise support, and error handling that should be addressed before merging.

You can customize Macroscope's approvability policy. Learn more.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb9cf66da7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +463 to +464
"--head",
input.headSelector,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Qualify fork heads when targeting upstream

When origin is a fork, resolveBranchHeadContext still treats the branch as non-cross-repo and passes a bare branch name as input.headSelector; after this change the same gh pr create call now adds --repo for the upstream parent, so --head feature is resolved against the upstream owner instead of the fork. The GitHub CLI docs say --head uses <user>:<branch> to select a head repo, and the CLI maintainers clarify that a bare head defaults to the --repo owner, so branches pushed only to the fork will fail to create a PR (or target an upstream branch with the same name).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 7f560fc — bare --head values are qualified with the fork owner before gh pr create/pr list against the upstream --repo.

"number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",
"nameWithOwner,parent",
"--jq",
'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the GitHub Enterprise host in repo overrides

For GitHub Enterprise checkouts, this resolver returns only owner/repo for both fork parents and same-repo cases, but the later --repo override needs the documented [HOST/]OWNER/REPO form to keep commands on the enterprise host. In an environment authenticated to both github.com and github.company.com (or where github.com is the default host), gh pr list/create and gh repo view will resolve this hostless value against the wrong host, regressing PR creation/default-branch lookup for GHES repositories.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in f4f6e83 — base/head repository resolution now emits HOST/OWNER/REPO from the repo URL so --repo stays on the enterprise host.

jakeleventhal and others added 4 commits July 11, 2026 22:54
When --repo targets a fork parent, bare --head values resolve against the upstream owner. Qualify with owner:branch from the local head repository so create/list hit the fork branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
listChangeRequests for non-open states, getPullRequest, and checkoutPullRequest still used the fork cwd. Route them through the resolved base repository so fork checkouts can list and open upstream PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Fork checkouts are not local cross-repo remotes, so open PRs against the upstream parent were filtered out as isCrossRepository. Keep matching when the PR head repository aligns with the expected fork head.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve base/head repositories as HOST/OWNER/REPO from the repo URL so gh commands stay on the authenticated enterprise host instead of defaulting to github.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 12, 2026
Comment thread apps/server/src/sourceControl/GitHubCli.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4f6e834bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"--base",
input.baseBranch,
"--head",
qualifyPullRequestHead(context, input.headSelector),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid org-qualified heads for org forks

When the fork is owned by an organization, this path now always sends --head org:branch after qualifyPullRequestHead prefixes the fork owner. The GitHub CLI manual for gh pr create says --head supports <user>:<branch> and that using an organization as <user> is not supported, so upstream PR creation from org-owned forks fails even though the branch was pushed and --repo points at the correct base repository; consider detecting the owner type or using a fallback that can specify an org fork head repository.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 10aeb3888 — cross-repository PR creation now uses the GitHub API with explicit head_repo, which supports organization-owned forks.

Comment on lines +281 to 283
} else if (pr.isCrossRepository === true) {
if (!expectedHeadRepository || !prHeadRepository) {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match GHES fork PRs before creating duplicates

For self-hosted GitHub fork checkouts, resolveRemoteRepositoryContext leaves expectedHeadRepository empty because parseGitHubRepositoryNameWithOwnerFromRemoteUrl only recognizes github.com remotes, while the new upstream-targeted listPullRequests can still return isCrossRepository: true PRs from the fork. This branch rejects those matches whenever the expected repo is missing, so create_pr on an existing GHES upstream PR will miss it and proceed to gh pr create (or, after creation, fail to recover the PR URL/number).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 849a0d275 — GitHub Enterprise remote URLs now resolve their owner/repository identity and existing cross-repository PRs are matched without duplicate creation.

"pr",
"list",
"--head",
qualifyPullRequestHead(context, input.headSelector),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Don't owner-qualify gh pr list heads

For fork checkouts this rewrites even the bare branch selector from findOpenPr/findLatestPr to owner:branch, but I checked the gh pr list manual and its --head option explicitly says the <owner>:<branch> syntax is not supported. That means the upstream PR lookup for fork-origin branches can fail or return no matches before create_pr runs, so existing upstream PRs are missed and the post-create lookup may not recover the PR URL/number; use an unqualified head plus another filter/search for the head repository instead of sharing the gh pr create selector here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in e9cd27e52gh pr list --head now receives the original unqualified branch selector, with repository identity still verified from the returned PR metadata.

args: [
"repo",
"view",
context.baseRepository,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Generate fork PR bodies against the upstream base

For a fork checkout whose origin/main is stale behind the upstream repository, this now returns the upstream default branch name, but GitManager.resolveBaseRangeRef still resolves that bare branch through resolvePrimaryRemoteName (normally origin) before generating the PR title/body. The PR is created against the upstream repo, yet the generated range is based on the fork's remote-tracking branch, so the body can include unrelated upstream commits or omit the actual upstream diff; the upstream base ref needs to be fetched/resolved from the same repository used here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 9860b6e65 — PR content generation now fetches and resolves the target repository's base branch instead of comparing against a stale fork origin.

Comment thread apps/server/src/git/GitManager.ts Outdated
Comment thread apps/server/src/sourceControl/GitHubCli.ts
Comment thread apps/server/src/git/GitManager.ts
Comment thread apps/server/src/git/GitManager.test.ts
})
.pipe(Effect.map((resolved) => resolved.commitSha));
});
const targetBaseRangeRef = targetMatchesOrigin

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.

🟡 Medium git/GitManager.ts:1137

When targetMatchesOrigin is false (fork workflow), resolveBaseRangeRef lets failures from adding/fetching the upstream remote abort the entire operation — the primary-remote fallback below is never reached. In the matching-origin case the same fetch is wrapped with Effect.orElseSucceed(() => null), so a transient network or auth failure degrades gracefully, but in the fork case a similar failure propagates and prevents PR creation even though the fallback could still resolve the base range. Consider applying the same Effect.orElseSucceed(() => null) wrapper to resolveTargetBaseRangeRef in the non-matching branch.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/git/GitManager.ts around line 1137:

When `targetMatchesOrigin` is `false` (fork workflow), `resolveBaseRangeRef` lets failures from adding/fetching the `upstream` remote abort the entire operation — the primary-remote fallback below is never reached. In the matching-origin case the same fetch is wrapped with `Effect.orElseSucceed(() => null)`, so a transient network or auth failure degrades gracefully, but in the fork case a similar failure propagates and prevents PR creation even though the fallback could still resolve the base range. Consider applying the same `Effect.orElseSucceed(() => null)` wrapper to `resolveTargetBaseRangeRef` in the non-matching branch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a6246954b — the regression test now makes the intended fork behavior explicit: if the upstream base cannot be fetched, PR content generation stops rather than silently using a stale fork origin. Applying the suggested fallback would reintroduce the incorrect-base bug identified in the preceding review.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@macroscopeapp macroscopeapp Bot left a comment

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.

One convention issue found in the new fork-detection path added to GitHubCli.make. See the inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/sourceControl/GitHubCli.ts Outdated

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2edec75. Configure here.

});
const targetBaseRangeRef = targetMatchesOrigin
? yield* resolveTargetBaseRangeRef.pipe(Effect.orElseSucceed(() => null))
: yield* resolveTargetBaseRangeRef;

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.

Same-repo check misses SSH aliases

Medium Severity

targetMatchesOrigin equates remotes via full normalizeGitRemoteUrl keys, so an SSH config host alias on origin (for example git@github.com-work:owner/repo) never matches GitHub’s canonical clone URLs. That same-repo checkout is treated like a fork, and a failed fetch of the canonical remote no longer falls back to the local tracking ref—create_pr fails instead.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2edec75. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2edec75de1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"-f",
`head_repo=${head.name}`,
"-f",
`base=${input.baseBranch}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the upstream default when targeting fork parents

When a fork checkout has a local branch tracking a differently named fork branch (for example local-work with upstream origin/feature-x), GitManager.resolveBaseBranch still returns that upstream branch name because the head context is considered non-cross-repo relative to origin. This new API path then sends that value as base against the parent repository, so PR creation fails if the parent has no feature-x branch or targets the wrong parent branch if it does; fork-parent PRs should ignore the fork tracking branch and use the target repository's default/configured base.

Useful? React with 👍 / 👎.

Comment on lines +1137 to +1139
const targetBaseRangeRef = targetMatchesOrigin
? yield* resolveTargetBaseRangeRef.pipe(Effect.orElseSucceed(() => null))
: yield* resolveTargetBaseRangeRef;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back for same-repo alternate remotes

For same-repo GitHub checkouts whose origin is a valid alternate URL, such as ssh://git@ssh.github.com:443/owner/repo.git or a corporate mirror/rewrite, targetMatchesOrigin is false because the comparison only recognizes the two clone URLs returned by gh repo view. This branch then treats a failed fetch of the canonical target URL as fatal instead of falling back to the existing origin/<base> tracking ref, so create_pr can abort before content generation even though the configured origin is usable; only fork-parent targets need this strict no-fallback behavior.

Useful? React with 👍 / 👎.

"-f",
`title=${input.title}`,
"-f",
`head=${head.owner}:${headBranch}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the selected head repository for API PRs

When origin is a fork but the current branch is pushed to a different remote (for example an upstream/parent or another fork remote), GitManager passes a qualified selector for that actual head repository, but this API path discards the selector owner and always uses context.headRepository from gh repo view of the checkout. In that setup the request is sent as originOwner:branch, so PR creation fails if the branch only exists on the selected remote or opens from the wrong fork if a same-named branch exists on origin.

Useful? React with 👍 / 👎.

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

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant