feat: GitHub Enterprise source control connector - #5089
Conversation
Separate `github-enterprise` provider kind supporting multiple enterprise hosts, derived from `gh auth status`. Reuses the existing `gh` CLI integration; host targeting via GH_HOST for repo-less operations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve TDD tasks from contract through mobile, each ending in a committed, independently testable deliverable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every test file the plan touches already exists, and the repo mixes @effect/vitest with vite-plus/test per file. Tasks 2 and 8 said to create files that are already there.
Review feedback: fan one card per enterprise host was rejected in favor of a single "GitHub Enterprise" card plus a secondary host radio row, matching the existing card idiom instead of introducing a dropdown.
…vailable buildAddProjectRemoteTargets(null) was dropping the four base provider kinds entirely, leaving users with only the url option until discovery resolves. Seed them as unready fallback targets instead, matching the pre-task behavior and hint text.
expandGitHubInstances stamps the host on the github row, so the Task 3 assertion of an absent host has been stale since it landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test title claimed an unauthenticated row, but exit code 0 with unparseable JSON falls through parseGitHubAuth to the "unknown" branch. Assert auth.status explicitly so the title and the assertion agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An `unknown` remote's provider name is the host including its port, so a GHES server at git.corp.com:8443 never matched the port-less host that `gh auth status` reports. Normalize both sides before comparing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Template detection was gated on the `github` kind, so enterprise repos with .github/pull_request_template.md silently generated PR bodies that ignored it. Hosts previously classified as `github` regressed when they started resolving to `github-enterprise`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every target inherited its discovery row's host, so the plain github target started sending host=github.com, which made GitHubCli set GH_HOST and copy process.env onto the highest-traffic gh repo view path for no behavioural gain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The enterprise publish card hard-coded ready:true, so an expired token on git.corp.com showed no Setup Required badge and could auto-select as the first ready provider, failing with a raw gh error. Readiness now comes from the row for that host, and the shared resolver takes a host so two enterprise connections no longer answer for each other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A bare name (e.g. "core") on an enterprise host resolves against the caller's personal namespace via `gh repo view`, which is usually empty. Bare names now fall back to `gh search repos`, preferring an exact repo-name match over search rank, only for github-enterprise providers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These were working notes for the implementation, not something the repo needs to carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
There was a problem hiding this comment.
Reviewed the Effect service changes (GitHubCli, GitHubSourceControlProvider, SourceControlProvider*, registry/discovery). Service definition order, make/layer exports, namespace imports, environment-based dependency acquisition, and Schema.TaggedErrorClass usage all look consistent with the conventions. One error-attribute safety issue noted inline.
Posted via Macroscope — Effect Service Conventions
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. This PR introduces a new feature (GitHub Enterprise source control connector) with significant new logic and UI flows. Additionally, two high-severity unresolved review comments identify potential bugs in repository resolution and host selection logic that warrant human attention. You can customize Macroscope's approvability policy. Learn more. |
Several owners can hold the same repository name on an enterprise host. Search ranking is no basis for choosing between them, so resolving a bare name could silently target a repository the user never asked for. Report the candidates instead and let the caller supply the full owner/repo path. Also route the not-found detail through transportSafeSourceControlErrorValue so it matches the sanitized repository attribute on the same error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cated The publish dialog picked the alphabetically first enterprise host. When that host was not authenticated its card rendered as Setup Required rather than a radio, and the host picker only exists once the card is selected, so an authenticated host on the same machine became unreachable. Default to a ready host, and render unauthenticated hosts in the picker as Setup Required so selecting one cannot re-enter the same dead end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 17f7304. Configure here.
| ); | ||
| if (exact.length > 1) { | ||
| return { _tag: "ambiguous", candidates: exact.map((result) => result.fullName) }; | ||
| } |
There was a problem hiding this comment.
🟠 High sourceControl/GitHubSourceControlProvider.ts:234
resolveRepositoryReference clones the wrong repository when an enterprise search for a bare name like core returns only fuzzy matches (e.g. owner/core-documentation). pickRepositorySearchMatch falls back to results[0] when no result's repo-name segment exactly equals the query, so resolveRepositoryReference silently resolves to an unrelated repo instead of reporting that core was not found. Consider returning _tag: "none" when no exact repo-name match exists, rather than falling back to the first search result.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/GitHubSourceControlProvider.ts around line 234:
`resolveRepositoryReference` clones the wrong repository when an enterprise search for a bare name like `core` returns only fuzzy matches (e.g. `owner/core-documentation`). `pickRepositorySearchMatch` falls back to `results[0]` when no result's repo-name segment exactly equals the query, so `resolveRepositoryReference` silently resolves to an unrelated repo instead of reporting that `core` was not found. Consider returning `_tag: "none"` when no exact repo-name match exists, rather than falling back to the first search result.
There was a problem hiding this comment.
Tightened in d4ca396, though not quite to _tag: "none".
Accepting a bare name at all is the feature here — enterprise users type core, not team-a/core — so refusing everything without an exact repo-name match would remove the reason the search path exists. What was actually wrong is narrower: ranking between near matches. results[0] was search relevance standing in for a decision only the caller can make.
So: exactly one exact match wins; a sole near match still resolves; anything else fails with the candidates listed. widget against Sollit/widget-service + Sollit/mywidget now errors instead of silently taking the first, which is the case your example describes. The old test asserting that arbitrary pick is replaced by two tests covering both branches.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
… ready A host selected while authenticated can lose that state on a later discovery pass. Holding onto it left the publish card in its Setup Required state with the host picker unmounted, which is the same dead end an unauthenticated default produced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| selectedHost: string | null; | ||
| availableHosts: ReadonlyArray<string>; | ||
| readyHosts: ReadonlyArray<string>; | ||
| }): string | null { |
There was a problem hiding this comment.
🟠 High components/GitActionsControl.logic.ts:469
resolveSelectedEnterpriseHost returns a fallback host without persisting it, so a stale selectedHost silently retakes priority on the next call. When the selected host becomes unready, the function returns a ready fallback; but on a later discovery pass where the original host becomes ready again, the unchanged selectedHost causes the result to switch back to the original host automatically. In the publish dialog this can change currentPublishProvider.host while the user is on a later wizard step, so submission may target the originally-selected host even though the UI previously fell back to a different one. The fallback should become the persisted selection, or stale selections must not regain priority once they've been superseded.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/GitActionsControl.logic.ts around line 469:
`resolveSelectedEnterpriseHost` returns a fallback host without persisting it, so a stale `selectedHost` silently retakes priority on the next call. When the selected host becomes unready, the function returns a ready fallback; but on a later discovery pass where the original host becomes ready again, the unchanged `selectedHost` causes the result to switch back to the original host automatically. In the publish dialog this can change `currentPublishProvider.host` while the user is on a later wizard step, so submission may target the originally-selected host even though the UI previously fell back to a different one. The fallback should become the persisted selection, or stale selections must not regain priority once they've been superseded.
A sole near match still resolves, which is what makes a bare name useful. Picking between several of them was search ranking standing in for a decision only the caller can make, so report the candidates instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Closes #5087
What Changed
I solely use GitHub Enterprise, and noticed it was missing here. Figured it wouldn't be that big of a change to support, since they both go through the
ghCLI anyway. I haven't recorded a video yet because of privacy company reasons. But can try to record a video later with blurred out data. Also if you don't agree, or this is too risky of a change, feel free to close it :DRest of this is AI:
SourceControlCliDiscoverySpecgains an optionalexpandInstanceshook, so a single CLI probe can emit several rows.gh auth status --json hostsproduces onegithubrow plus onegithub-enterprise:<host>row per enterprise host. Every other provider keeps its existing one-row behavior..ghe.comhosts andgithub-containing hostnames classify as enterprise. For hostnames that look like nothing in particular (git.acme.internal), the GitHub spec implements the existingrefineUnknownRemotehook — the same escape hatch GitLab already uses — and claims a remote whose host matches an authenticatedghhost.ghcalls (repo view,repo create) have no remote to infer a host from, soGitHubCli.executesetsGH_HOSTwhen a host is supplied. In-repo calls keep resolving the host from the git remote.hostflows through lookup, clone, and publish. Publishing togithub-enterprisewithout a host fails with a clear message rather than silently targeting github.com.Also fixes a latent bug the reclassification exposed:
GitManager's PR-template lookup was gated onkind === "github", so agithub.acme.comremote would have silently stopped finding.github/pull_request_template.mdonce it moved to the enterprise kind.Why
ghalready talks to enterprise hosts. The app just never asked it which ones. Today a GHES or GHEC-data-residency remote lands in theunknownbucket, so PR listing, PR creation, publish, and clone are all unavailable to anyone whose work repos live on a company instance — even though the CLI underneath supports them fully.Separate kind rather than an optional
hostongithub: people are commonly authenticated against github.com plus one or more enterprise hosts at the same time, and discovery rows and settings entries are keyed by kind, so a singlegithubrow cannot represent them. This is also the shape Cursor uses — GitHub and GitHub Enterprise as distinct connections, several enterprise connections allowed.UI Changes
There is no meaningful "before" for these screens — the enterprise host does not appear at all today, so the before state is its absence. After:
Checklist
🤖 Generated with Claude Code
Note
Medium Risk
Touches source-control discovery, gh CLI host targeting, and repository lookup/publish paths; contract discovery items now require
id, but the change is well-tested and scoped to enterprise GitHub rather than core auth.Overview
Introduces
github-enterpriseas a separate source-control kind so users authenticated to github.com and one or more enterprise hosts each get their own discovery row, clone/publish target, and readiness state.Discovery expands the
ghauth probe viaexpandInstances: onegithubrow plusgithub-enterprise:<host>per non-github.com host, with stableidand optionalhoston discovery items. Unknown remotes can be refined to enterprise when the remote host matches an authenticatedghhost.Server / CLI: optional
hoston lookup, clone, and publish (required for enterprise without host → clear error).GitHubClisetsGH_HOSTfor host-scoped calls, addssearchRepositories, and derives enterprise clone URLs whenrepo createprints no URL. Enterprise bare repo names resolve through search with explicit ambiguity handling. PR templates in stacked git actions apply togithub-enterpriseas well asgithub.Clients (web command palette, publish wizard with per-host radios, settings list keyed by
id, mobile add-project) use sharedAddProjectRemoteTarget/ label/readiness helpers and passhostinto repository lookup.Shared: remote URL heuristics split github.com from
.ghe.com/github-in-hostname enterprise; PR URL parsing accepts PR links on any host, not only github.com.Reviewed by Cursor Bugbot for commit d4ca396. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add GitHub Enterprise as a supported source control provider
github-enterpriseas a newSourceControlProviderKindacross contracts, server, and client, enabling repository lookup, clone, and publish workflows against self-hosted GitHub Enterprise instances.hostviaGH_HOSTenv var and addssearchRepositoriesfor resolving bare repo names on enterprise hosts.gh auth statusnow emits distinct provider rows per authenticated enterprise host, surfacing them in the Add Project and Publish Repository dialogs.GITHUB_PULL_REQUEST_URL_PATTERNto match PR URLs on any host, not justgithub.com.detectSourceControlProviderFromRemoteUrlnow classifies non-github.comGitHub-style hosts asgithub-enterpriseinstead ofgithub, which is a breaking change for any code that assumed github.com-only classification.Macroscope summarized d4ca396.