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. New feature adding GitHub Enterprise source control support with significant cross-cutting changes to discovery, provider registration, and UI. Three unresolved high-severity review comments identify potential bugs in repository resolution and enterprise 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>
… 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>
Without a host `gh search repos` answers for github.com, so a bare name meant for an enterprise instance resolved against public GitHub. The repository service already refuses this, but the provider is reachable on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were introduced by this branch and were the only publish state that survived a close, so reopening the wizard from another repository could still be pointed at the previous enterprise host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
One finding: a caller-supplied host is interpolated raw into a caller-visible SourceControlProviderError.detail, while the sibling repository value on the same error is sanitized. Everything else in the new makeProvider/discovery-expansion work follows the service conventions (subpath namespace imports, yield* GitHubCli.GitHubCli for dependency acquisition, make/layer retained, Schema.TaggedErrorClass with structural attributes and a preserved cause).
Posted via Macroscope — Effect Service Conventions
The previous guard only covered bare names, so an owner/repo lookup or a repository creation on the enterprise provider with no host still reached `gh` without GH_HOST and answered for github.com. Move the check ahead of both operations. Also sanitize the host before interpolating it into the error detail, to match the repository value on the same error. 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 a766d6e. Configure here.
| ), | ||
| getRepositoryCloneUrls: (input) => | ||
| ensureEnterpriseHost({ ...input, operation: "getRepositoryCloneUrls" }).pipe( | ||
| Effect.andThen(() => resolveRepositoryReference(input)), |
There was a problem hiding this comment.
Blocks in-repo enterprise clone lookup
High Severity
ensureEnterpriseHost rejects every github-enterprise getRepositoryCloneUrls call that omits host, but in-repo callers such as GitManager resolve the enterprise provider from the git remote and invoke lookup with only cwd and owner/repo, expecting gh to use the clone context. Those cross-repo PR flows now fail with “Choose a GitHub Enterprise host before continuing” instead of resolving clone URLs.
Reviewed by Cursor Bugbot for commit a766d6e. Configure here.


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 contracts (
idon discovery items), git publish/clone paths, andGH_HOSTbehavior; mistakes could target the wrong host or break cached discovery payloads, but enterprise host is required before mutating operations and coverage is extensive in tests.Overview
GitHub Enterprise is introduced as its own
github-enterpriseprovider kind so users authenticated to github.com and one or more enterprise hosts each get a distinct discovery row and UI target.Discovery gains an optional
expandInstanceshook on CLI specs:gh auth status --json hostsyields onegithubrow plusgithub-enterprise:<host>per enterprise host. Discovery items now carry stableidand optionalhost. Unknown remotes on an authenticatedghhost can be refined to enterprise viarefineUnknownRemote.Server / CLI:
GitHubCliaccepts optionalhostand setsGH_HOSTfor repo-less calls;searchRepositoriesresolves bare repo names on enterprise. Enterprise lookup/clone/publish require a host or fail explicitly instead of defaulting to github.com. PR template detection and stacked PR flows treatgithub-enterpriselikegithub.Clients: Add-project and command palette use per-target readiness/labels (host shown for enterprise). Publish dialog adds an enterprise card with a host radio picker and per-host auth readiness. Remote URL heuristics split github.com from
.ghe.com/github-in-hostname enterprise; PR URL parsing accepts any host’s/pull/Npath.Contracts:
hostis optional on repository lookup, clone, and publish inputs.Reviewed by Cursor Bugbot for commit 5ec0979. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add GitHub Enterprise as a source control provider across server, web, and mobile
github-enterpriseas a recognizedSourceControlProviderKindin contracts, shared packages, server, and all client apps.gh) now expands into one entry per authenticated enterprise host in addition to github.com, using a newexpandInstancescallback in the CLI discovery spec.lookupRepository,cloneRepository,publishRepository) require a non-emptyhostwhen provider isgithub-enterprise; bare repository names are resolved via a newsearchRepositoriesCLI call.github.com, enabling enterprise PR references in the checkout flow.SourceControlProviderDiscoveryItemnow requires anidfield; any existing serialized discovery items withoutidwill fail schema validation.Macroscope summarized 5ec0979.