Skip to content

fix(github-projects): route gh to the repo's host in multi-host setups (#1715)#6583

Open
nwparker wants to merge 3 commits into
mainfrom
nwparker/fix-1715
Open

fix(github-projects): route gh to the repo's host in multi-host setups (#1715)#6583
nwparker wants to merge 3 commits into
mainfrom
nwparker/fix-1715

Conversation

@nwparker

@nwparker nwparker commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🧑‍🤝‍🧑 Impact & ELI5

1) What's broken today (ELI5). People who sign into more than one GitHub at once — the public github.com and a company-private "GitHub Enterprise" server — cannot use GitHub Projects in Orca for a repo that lives on the company server. Orca pops up a scary, wrong error: "Your gh token is missing the project scope…" even though the company login already has that permission. On top of that, the suggested fix names the wrong account, and pasting a company Project link is flat-out rejected. GitHub Issues already worked fine in the same setup, so it looks especially broken and confusing. This is a hard blocker (Projects is unusable), but only for the niche group running a true two-GitHub setup; everyone on plain github.com is unaffected.

2) What this PR does (ELI5). Think of gh (the GitHub command-line tool Orca calls under the hood) as a courier. For Issues, Orca handed the courier the repo folder so it could read the return address and deliver to the right post office. For Projects, Orca forgot to include the address, so the courier defaulted to the main github.com post office — the wrong one. This PR figures out the repo's real "post office" from its git remote and writes it explicitly on every Projects-related request (gh ... --hostname <that-server>), including the auth diagnostic and the paste-a-Project-link box. So Orca now talks to the GitHub server that actually owns your repo instead of always defaulting to public github.com. It is careful to skip non-GitHub remotes (GitLab, Bitbucket, Azure) so those are never mis-addressed.

3) Regression answer. No feature is disabled or degraded. Caches are deliberately partitioned by the repo routing target, so Project data fetched for one GitHub host is never reused for a same-named project on another host. On upgrade, the new cache keys naturally fetch fresh data the first time they are used; that is normal cache invalidation for correctness, not a user-visible regression. Plain github.com users now route to github.com explicitly, which preserves behavior and also protects them when gh's active/default host is set to a different GitHub host. Non-GitHub hosts are rejected or dropped before gh is invoked.


Summary

Fixes #1715. In multi-host gh setups (github.com + a GitHub Enterprise instance), GitHub Projects was unusable: discovery, view fetches, slug-addressed mutations, and the gh auth status diagnostic all issued hostless gh api graphql / gh auth status calls. gh therefore fell back to its globally-active host (usually github.com) instead of the repo's own host. The result:

  • Projects showed a false "Your gh token is missing the project scope…" error, even though the GHES token had project.
  • The auth-error remedy named the wrong host's account.
  • Pasting a GHES project URL (https://ghe.example.com/orgs/acme/projects/2) was rejected because the picker hardcoded github.com.

(Repo-scoped Issues already worked because they run with cwd=repoPath, which lets gh infer the host — Projects' hostless GraphQL calls did not.)

This threads an optional GitHubRepoTarget { repoPath, connectionId } from the renderer through IPC/RPC into the main process, resolves the repo's git-remote host, and passes gh api --hostname <host> (keeping cwd for repo-context placeholders and for gh issue close/reopen, which has no --hostname flag). Concretely:

  • normalizeGitHubApiHost / preferredGitHubApiHost added to the existing github-remote-identity-parsing.ts; a focused github-api-host-resolution.ts resolves and caches the repo's gh API host (prefers upstream then origin).
  • The resolver ignores known non-GitHub remotes (GitLab / Bitbucket / Azure) so ProjectV2 calls are never mis-routed; an unrecognized host is only used as a fallback after preferred github/ghe hosts.
  • targetToGhApiRoute + GhApiRoute in project-view/internals.ts; runGraphql/runRest now take a route; all discovery / view / count / mutation / auth-status gh calls are routed.
  • ProjectPicker accepts any host so GHES URLs parse; a pasted URL's host overrides the active-repo hint (resolveProjectRef). Host validation rejects a leading dash and the github.com@evil.example spoof.
  • Renderer caches (picker browse cache, slug-metadata cache, projectViewCacheKey/request key) are keyed by the routing target so the same owner/project on different hosts can't reuse each other's data.

This supersedes #2784 (community PR by @BorjaLL): re-implemented against current main (the original no longer applied across 8 files), reusing the existing remote-identity parser instead of adding a duplicate module, and dropping that PR's formatting-only churn. Original author credited via Co-authored-by. #2784 is left open.

Screenshots

No visual change. (The picker now accepts GHES project URLs and projects load on GHES; behavior is exercised by unit tests below since reproducing it needs a real second GHES host + dual gh auth.)

Testing

  • pnpm lint (oxlint, scoped to changed files — clean)
  • pnpm typecheck (node + web + cli tsgo configs — clean)
  • pnpm test (affected vitest suites — see PR comment for output)
  • pnpm build
  • Added or updated high-quality tests that would catch regressions

New/updated tests assert that GHES repo targets pass --hostname ghe.acme.internal + cwd:/repo on discovery, view, field, and slug mutations; that gitlab.com remotes pass no --hostname; that gh auth status runs --hostname for the repo's host; that parseProjectPaste captures/normalizes GHES hosts, rejects known non-GitHub providers, and rejects the @-spoof; and that same-named owners on github.com/GHES do not share ProjectV2 owner/capability cache probes.

AI Review Report

Reviewed the diff adversarially for correctness, edge cases, and cross-platform behavior. Risks checked:

  • Cross-platform: no hardcoded path separators or metaKey; host resolution uses git remote parsing only; gh issue close/reopen routed via cwd (no --hostname flag) so it works identically on macOS/Linux/Windows. No new platform-conditional code paths.
  • SSH / remote runtime: GitHubRepoTarget carries connectionId; when set, the path is read through the SSH git provider and local cwd is omitted (consistent with existing ghRepoExecOptions). RPC/IPC both thread the target so environment-runtime callers route correctly.
  • Other git providers: the resolver explicitly returns null for GitLab/Bitbucket/Azure hosts, so GitLab remotes keep gh's default host (verified by test) rather than being treated as GHES.
  • Lint: splitting getGitHubApiHostForRepo into github-api-host-resolution.ts kept github-repository-identity.ts under the 300-line limit instead of adding a max-lines disable.

Security Audit

  • Command execution: all gh calls use execFile (array argv, no shell) — no shell injection surface. --hostname values come only from normalizeGitHubApiHost / normalizeProjectUrlHost, which allow [a-z0-9.-] + optional :port, reject a leading dash (can't be mistaken for a gh flag), and reject @/whitespace (blocks the github.com@evil.example URL-host spoof). No flag-injection or SSRF-style host smuggling.
  • Input handling: pasted project URLs are validated host-first; an invalid host returns null before any gh call. ProjectV2 is GitHub-only, so non-GitHub hosts are dropped rather than queried.
  • No new dependencies, eval, secret access, network calls, or path traversal. IPC/RPC zod schemas mark both target fields optional and nullable, preserving back-compat for callers with no repo context.
  • Re-audited the adopted community code as if hostile: no exfiltration, no suspicious endpoints, no broadened token/scope access — it only changes which already-authenticated host a call targets.

Notes

Made with Orca 🐋

#1715)

GitHub Projects discovery, view fetches, slug mutations, and the gh auth
diagnostic all issued hostless `gh api graphql` / `gh auth status` calls,
so gh fell back to its globally-active host (typically github.com) instead
of the repo's host. In a github.com + GHES setup this made Projects report
a false "missing project scope" error and rejected pasted GHES project URLs.

Thread an optional GitHubRepoTarget {repoPath, connectionId} from the
renderer through IPC/RPC, resolve the repo's git-remote host, and pass
`gh api --hostname <host>` (with cwd for repo-context placeholders). The
host resolver ignores known non-GitHub remotes (GitLab/Bitbucket/Azure) so
ProjectV2 calls are never mis-routed. ProjectPicker now accepts any host so
GHES project URLs parse, and the pasted URL's host overrides the active-repo
hint. Renderer caches are keyed by the routing target so the same
owner/project on different hosts can't reuse each other's data.

Supersedes #2784 (community PR re-implemented against current main).

Fixes #1715

Co-authored-by: Borja <b.llanderas@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@nwparker, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 27645120-5c7b-4c6c-a588-2d60934ab7dc

📥 Commits

Reviewing files that changed from the base of the PR and between 8c49426 and 1c9a2ce.

📒 Files selected for processing (32)
  • src/main/github/auth-diagnose-routing.test.ts
  • src/main/github/auth-diagnose.ts
  • src/main/github/gh-utils.test.ts
  • src/main/github/gh-utils.ts
  • src/main/github/github-api-host-resolution.ts
  • src/main/github/github-remote-identity-parsing.ts
  • src/main/github/github-repository-identity.ts
  • src/main/github/project-view-routing.test.ts
  • src/main/github/project-view.test.ts
  • src/main/github/project-view.ts
  • src/main/github/project-view/internals.ts
  • src/main/github/project-view/mutations.test.ts
  • src/main/github/project-view/mutations.ts
  • src/main/ipc/github.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/github.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/GitHubItemDialog.tsx
  • src/renderer/src/components/github-item-dialog-source-boundary.test.ts
  • src/renderer/src/components/github-project/GhAuthErrorHelp.tsx
  • src/renderer/src/components/github-project/ProjectCell.tsx
  • src/renderer/src/components/github-project/ProjectPicker.tsx
  • src/renderer/src/components/github-project/ProjectViewWrapper.tsx
  • src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx
  • src/renderer/src/components/github-project/slug-dialog/Comments.tsx
  • src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx
  • src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx
  • src/renderer/src/hooks/useGitHubSlugMetadata.ts
  • src/renderer/src/lib/github-active-repo-target.ts
  • src/renderer/src/store/slices/github.ts
  • src/shared/github-project-types.ts

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.

@nwparker

Copy link
Copy Markdown
Contributor Author

Test & gate evidence

Routing unit tests (the regression guards)

vitest run src/main/github/project-view-routing.test.ts src/main/github/auth-diagnose-routing.test.ts src/main/github/gh-utils.test.ts src/main/github/project-view.test.ts src/main/github/auth-diagnose.test.ts src/main/github/project-view/mutations.test.ts src/renderer/src/store/slices/github.test.ts

 Test Files  7 passed (7)
      Tests  209 passed (209)

Key assertions covered:

  • project-view-routing.test.ts: GHES repo target → every discovery/view/field/slug gh call gets --hostname ghe.acme.internal + cwd:/repo; a gitlab.com remote gets no --hostname.
  • auth-diagnose-routing.test.ts: diagnoseGhAuth({ repoPath }) runs ['auth','status','--hostname','ghe.acme.internal'] with cwd:/repo and reports the GHES account as active.
  • gh-utils.test.ts: getGitHubApiHostForRepo resolves the GHES host (upstream→origin) and returns null for gitlab.com.
  • project-view.test.ts: parseProjectPaste captures host for github.com and GHES URLs (incl. :8443), and rejects https://github.com@evil.example/orgs/acme/projects/1.

Full github + rpc-methods regression sweep

vitest run src/main/github/ src/main/runtime/rpc/methods/654 passed (53 files).

Local gate

  • tsgo --noEmit -p config/tsconfig.node.json → clean
  • tsgo --noEmit -p config/tsconfig.tc.web.json → clean
  • tsgo --noEmit -p config/tsconfig.tc.cli.json → clean
  • oxlint <changed files> → 0 warnings / 0 errors
  • oxfmt --write <changed files> → formatted, no drift

Before / after reasoning

Before: listAccessibleProjects() and diagnoseGhAuth() built ['api','graphql',…] / ['auth','status'] with no --hostname and no cwd, so gh used its globally-active host. On a GHES repo whose default host is github.com, the GraphQL hit github.com (where the token lacks GHES project scope) → false "missing project scope". Pasted GHES URLs were rejected by url.hostname !== 'github.com'.

After: the renderer passes the active repo's {repoPath, connectionId}; main resolves the git-remote host and passes gh api --hostname <host> (+cwd). GHES projects load, the auth remedy names the correct host, and GHES project URLs parse. Non-GitHub remotes (GitLab/Bitbucket/Azure) resolve to null and keep gh's default host, so nothing is mis-routed.

@nwparker

nwparker commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Mergeability follow-up

Pushed 1c9a2ce719, merging current origin/main (8c49426224) into nwparker/fix-1715 and resolving the GitHubItemDialog.tsx conflict.

The conflict resolution keeps both sides:

  • main's source-context runtime routing for GitHub task sources
  • this PR's repo-target hint for Project slug mutations, now derived from the mutation repo id when possible and falling back to the active repo

Cache note: route-targeted renderer caches intentionally fetch fresh data when their routing key changes. That is normal cache invalidation for host isolation, not an unresolved PR tradeoff; sharing those entries would reintroduce the bug class because same owner/project numbers can exist on different GitHub hosts. Explicit --hostname github.com is also intentional because it preserves github.com behavior and protects github.com repos when gh's active/default host is another GitHub host.

Validation after merge:

  • pnpm exec vitest run --config config/vitest.config.ts src/main/github/project-view-routing.test.ts src/main/github/auth-diagnose-routing.test.ts src/main/github/gh-utils.test.ts src/main/github/project-view.test.ts src/main/github/auth-diagnose.test.ts src/main/github/project-view/mutations.test.ts src/renderer/src/store/slices/github.test.ts src/renderer/src/components/github-item-dialog-source-boundary.test.ts -> 8 files / 223 tests passed
  • pnpm exec vitest run --config config/vitest.config.ts src/main/github/ src/main/runtime/rpc/methods/ -> 55 files / 678 tests passed
  • pnpm run typecheck:node -> clean
  • pnpm run typecheck:web -> clean
  • pnpm run typecheck:cli -> clean
  • pnpm exec oxlint src/main/github/project-view.ts src/main/github/project-view.test.ts src/main/github/project-view-routing.test.ts src/renderer/src/components/GitHubItemDialog.tsx -> clean
  • git diff --check origin/main...HEAD -> clean

GitHub mergeability after push: REST reports mergeable: true at head 1c9a2ce719; mergeable_state is unstable only because verify is still pending (Wayland check passed).

…583-zero-tradeoff

# Conflicts:
#	src/renderer/src/components/GitHubItemDialog.tsx
@BorjaLL

BorjaLL commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for carrying this the rest of the way @nwparker. From the #1715 side this looks right: repo-scoped host routing, with the Project-slug mutations deriving the target from the mutation repo id and falling back cleanly. LGTM. Closing my #2784 as superseded by this.

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.

[Bug]: GitHub Projects uses the wrong gh host in multi-host setups and incorrectly reports missing project scope

2 participants