Skip to content

feat: GitHub Enterprise source control connector - #5089

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

feat: GitHub Enterprise source control connector#5089
JorrinKievit wants to merge 31 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, 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-enterprise as 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 gh auth probe via expandInstances: one github row plus github-enterprise:<host> per non-github.com host, with stable id and optional host on discovery items. Unknown remotes can be refined to enterprise when the remote host matches an authenticated gh host.

Server / CLI: optional host on lookup, clone, and publish (required for enterprise without host → clear error). GitHubCli sets GH_HOST for host-scoped calls, adds searchRepositories, and derives enterprise clone URLs when repo create prints no URL. Enterprise bare repo names resolve through search with explicit ambiguity handling. PR templates in stacked git actions apply to github-enterprise as well as github.

Clients (web command palette, publish wizard with per-host radios, settings list keyed by id, mobile add-project) use shared AddProjectRemoteTarget / label/readiness helpers and pass host into 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

  • Introduces github-enterprise as a new SourceControlProviderKind across contracts, server, and client, enabling repository lookup, clone, and publish workflows against self-hosted GitHub Enterprise instances.
  • Extends the GitHub CLI integration to forward a host via GH_HOST env var and adds searchRepositories for resolving bare repo names on enterprise hosts.
  • Discovery via gh auth status now emits distinct provider rows per authenticated enterprise host, surfacing them in the Add Project and Publish Repository dialogs.
  • The Publish Repository dialog lists available enterprise hosts, shows per-host authentication readiness, and passes the selected host to the publish action.
  • Broadens GITHUB_PULL_REQUEST_URL_PATTERN to match PR URLs on any host, not just github.com.
  • Risk: detectSourceControlProviderFromRemoteUrl now classifies non-github.com GitHub-style hosts as github-enterprise instead of github, which is a breaking change for any code that assumed github.com-only classification.

Macroscope summarized d4ca396.

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: 0c66b113-e177-4a86-ae11-2f48f3d7ed47

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

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>

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

Comment thread apps/web/src/components/GitActionsControl.logic.ts Outdated
);
if (exact.length > 1) {
return { _tag: "ambiguous", candidates: exact.map((result) => result.fullName) };
}

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

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

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