Skip to content

feat: GitHub Enterprise source control connector - #5089

Open
JorrinKievit wants to merge 40 commits into
pingdotgg:mainfrom
JorrinKievit:feat/github-enterprise-connector
Open

feat: GitHub Enterprise source control connector#5089
JorrinKievit wants to merge 40 commits into
pingdotgg:mainfrom
JorrinKievit:feat/github-enterprise-connector

Conversation

@JorrinKievit

@JorrinKievit JorrinKievit commented Jul 31, 2026

Copy link
Copy Markdown

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 gh CLI 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 :D

Rest of this is AI:

  • DiscoverySourceControlCliDiscoverySpec gains an optional expandInstances hook, so a single CLI probe can emit several rows. gh auth status --json hosts produces one github row plus one github-enterprise:<host> row per enterprise host. Every other provider keeps its existing one-row behavior.
  • Detection.ghe.com hosts and github-containing hostnames classify as enterprise. For hostnames that look like nothing in particular (git.acme.internal), the GitHub spec implements the existing refineUnknownRemote hook — the same escape hatch GitLab already uses — and claims a remote whose host matches an authenticated gh host.
  • CLI targeting — repo-less gh calls (repo view, repo create) have no remote to infer a host from, so GitHubCli.execute sets GH_HOST when a host is supplied. In-repo calls keep resolving the host from the git remote.
  • Threadinghost flows through lookup, clone, and publish. Publishing to github-enterprise without a host fails with a clear message rather than silently targeting github.com.
  • UI — one "GitHub Enterprise" card; when several hosts are authenticated a secondary radio row picks between them. Matches the radio-card pattern used everywhere else in settings rather than introducing a dropdown.
  • Consumers — PR URL parsing, PR template detection, command palette, add-project targets, and the mobile icon all handle the new kind.

Also fixes a latent bug the reclassification exposed: GitManager's PR-template lookup was gated on kind === "github", so a github.acme.com remote would have silently stopped finding .github/pull_request_template.md once it moved to the enterprise kind.

Why

gh already talks to enterprise hosts. The app just never asked it which ones. Today a GHES or GHEC-data-residency remote lands in the unknown bucket, 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 host on github: 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 single github row 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:

Source control settings showing a GitHub Enterprise connection Publish flow with GitHub Enterprise selected Host selection between authenticated enterprise hosts

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

🤖 Generated with Claude Code


Note

Medium Risk
Touches source-control discovery contracts (id on discovery items), git publish/clone paths, and GH_HOST behavior; 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-enterprise provider 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 expandInstances hook on CLI specs: gh auth status --json hosts yields one github row plus github-enterprise:<host> per enterprise host. Discovery items now carry stable id and optional host. Unknown remotes on an authenticated gh host can be refined to enterprise via refineUnknownRemote.

Server / CLI: GitHubCli accepts optional host and sets GH_HOST for repo-less calls; searchRepositories resolves 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 treat github-enterprise like github.

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/N path.

Contracts: host is 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

  • Introduces github-enterprise as a recognized SourceControlProviderKind in contracts, shared packages, server, and all client apps.
  • Discovery via the GitHub CLI (gh) now expands into one entry per authenticated enterprise host in addition to github.com, using a new expandInstances callback in the CLI discovery spec.
  • Server-side operations (lookupRepository, cloneRepository, publishRepository) require a non-empty host when provider is github-enterprise; bare repository names are resolved via a new searchRepositories CLI call.
  • The Publish Repository dialog in the web app lists discovered enterprise hosts as selectable options with per-host readiness, auth status, and setup guidance.
  • The Add Project flow (web and mobile) surfaces per-host enterprise targets alongside standard providers, with host-aware labels and readiness.
  • Pull request URL matching is broadened to accept any host, not just github.com, enabling enterprise PR references in the checkout flow.
  • Risk: SourceControlProviderDiscoveryItem now requires an id field; any existing serialized discovery items without id will fail schema validation.

Macroscope summarized 5ec0979.

Jorrin and others added 27 commits July 30, 2026 19:56
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>
@coderabbitai

coderabbitai Bot commented Jul 31, 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 Plus

Run ID: afb50f40-fa49-41c3-9311-43c80277e32f

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

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:XXL 1,000+ changed lines (additions + deletions). labels Jul 31, 2026

@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.

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

Comment thread apps/server/src/sourceControl/GitHubSourceControlProvider.ts Outdated
Comment thread apps/server/src/sourceControl/GitHubSourceControlProvider.ts Outdated
Comment thread apps/web/src/components/GitActionsControl.tsx
@JorrinKievit
JorrinKievit marked this pull request as ready for review July 31, 2026 09:51
Comment thread apps/web/src/components/GitActionsControl.tsx
@macroscopeapp

macroscopeapp Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Jorrin and others added 2 commits July 31, 2026 12:09
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>
Comment thread apps/web/src/components/GitActionsControl.logic.ts Outdated
Comment thread apps/server/src/sourceControl/GitHubSourceControlProvider.ts
… 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 {

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.

🟠 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.

Jorrin and others added 2 commits July 31, 2026 14:37
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>
Comment thread apps/server/src/sourceControl/GitHubSourceControlProvider.ts
Comment thread apps/web/src/components/GitActionsControl.tsx
JorrinKievit and others added 3 commits August 4, 2026 22:31
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>
Comment thread apps/server/src/sourceControl/GitHubSourceControlProvider.ts

@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 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

Comment thread apps/server/src/sourceControl/GitHubSourceControlProvider.ts Outdated
Comment thread apps/server/src/git/GitManager.ts
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>

@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 a766d6e. Configure here.

),
getRepositoryCloneUrls: (input) =>
ensureEnterpriseHost({ ...input, operation: "getRepositoryCloneUrls" }).pipe(
Effect.andThen(() => resolveRepositoryReference(input)),

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a766d6e. Configure here.

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

Labels

size:XXL 1,000+ 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.

[Feature]: GitHub Enterprise support in the source control connector

1 participant