Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
14a57e8
docs: add GitHub Enterprise connector design spec
Jul 30, 2026
80b2dcf
docs: add GitHub Enterprise connector implementation plan
Jul 30, 2026
096d0a9
feat(contracts): add github-enterprise kind and discovery identity
Jul 30, 2026
1932ee2
docs: correct test-file assumptions in the enterprise connector plan
Jul 30, 2026
456f569
feat(shared): detect and present GitHub Enterprise hosts
Jul 30, 2026
025331c
feat(server): let a discovery spec expand into multiple provider rows
Jul 30, 2026
13f22a2
test(server): assert discovery id/host defaults on all three probe paths
Jul 30, 2026
5e653f4
feat(server): expand gh auth hosts into enterprise connections
Jul 30, 2026
48f0e9e
test(server): pin dedup of duplicate accounts on one enterprise host
Jul 30, 2026
6a4a5cf
feat(server): target enterprise hosts with GH_HOST in GitHubCli
Jul 30, 2026
e9a555e
feat(server): register github-enterprise provider on the gh CLI
Jul 30, 2026
c1eb013
feat(server): route repository operations to an enterprise host
Jul 30, 2026
5a085b1
fix(web): accept enterprise pull request urls
Jul 30, 2026
f4d1dca
feat(web): list each GitHub Enterprise connection in settings
Jul 30, 2026
d91c243
feat(web): publish repositories to a GitHub Enterprise host
Jul 30, 2026
e82c346
fix(web): collapse enterprise publish picker to one card with a host row
Jul 30, 2026
59a602e
feat(client-runtime): derive add-project sources from discovered conn…
Jul 30, 2026
8ee1bf8
fix(client-runtime): keep base provider targets when discovery is una…
Jul 30, 2026
9fff751
feat(web,mobile): offer GitHub Enterprise connections when adding a p…
Jul 30, 2026
3b1c8e9
test(server): expect the github discovery row to carry github.com
Jul 30, 2026
649f4da
test(server): assert unknown auth on unparseable gh output
Jul 30, 2026
be2e737
fix(server): refine GHES remotes served on a non-default port
Jul 30, 2026
f651ea2
fix(server): detect PR templates for GitHub Enterprise repos
Jul 30, 2026
dc6974a
fix(client-runtime): carry a host only on enterprise add-project targets
Jul 30, 2026
3a6d3a9
fix(web): resolve enterprise publish readiness from its discovery row
Jul 30, 2026
392d914
feat(server): resolve bare GitHub Enterprise repo names via search
Jul 31, 2026
a8c27aa
chore: drop local planning docs from the branch
Jul 31, 2026
a3de567
fix(server): report ambiguous bare enterprise repository names
Jul 31, 2026
17f7304
fix(web): keep enterprise publish reachable when a host is unauthenti…
Jul 31, 2026
7a8e885
fix(web): drop a stale enterprise host selection when another host is…
Jul 31, 2026
d4ca396
fix(server): stop ranking near matches for bare enterprise names
Jul 31, 2026
cc26767
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 3, 2026
929f9dd
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 4, 2026
e165549
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 4, 2026
aff4c5e
fix(server): refuse a bare enterprise name when no host is known
Aug 4, 2026
3b568e2
fix(web): clear the publish provider and host when the dialog closes
Aug 4, 2026
a766d6e
fix(server): require a host for every enterprise repository operation
Aug 4, 2026
d63dd1e
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 5, 2026
03429e7
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 5, 2026
5ec0979
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 6, 2026
3df238e
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 7, 2026
32c0eee
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 8, 2026
4a04543
fix(server): keep in-repo enterprise lookups working without a host
Aug 8, 2026
ca71246
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 10, 2026
9749369
Merge branch 'main' into feat/github-enterprise-connector
Aug 10, 2026
8803619
feat(pull-request): read change requests on GitHub Enterprise hosts
Aug 10, 2026
cd3ebff
Merge remote branch update
Aug 10, 2026
d6d1afb
fix(shared): keep github.com's own endpoints out of the enterprise kind
Aug 10, 2026
ca4ac27
fix(pull-request): read the viewer from the host it belongs to
Aug 10, 2026
067ae51
refactor(source-control): move the search failure sentence beside its…
Aug 10, 2026
b88d5a4
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 10, 2026
84c291e
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 10, 2026
6d6af45
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 11, 2026
4ccf24e
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 11, 2026
c61aef7
fix(web): open a pull request link on any GitHub Enterprise host
Aug 11, 2026
4f3bcc4
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 12, 2026
b17677b
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Aug 12, 2026
0c65b3a
Merge branch 'main' into feat/github-enterprise-connector
Sep 12, 2026
b99748b
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Sep 12, 2026
cae49b0
fix(source-control): address review findings on the enterprise connector
Sep 12, 2026
9cbad83
Merge branch 'main' into feat/github-enterprise-connector
JorrinKievit Sep 13, 2026
a5b753e
Merge remote-tracking branch 'upstream/main' into feat/github-enterpr…
Sep 13, 2026
9089687
Merge remote-tracking branch 'upstream/main' into feat/github-enterpr…
Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/mobile/src/components/SourceControlIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { withUniwind } from "uniwind";

const ThemedSvg = withUniwind(Svg);

export type SourceControlIconKind = "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops";
export type SourceControlIconKind =
| "github"
| "github-enterprise"
| "gitlab"
| "forgejo"
| "bitbucket"
| "azure-devops";

export function SourceControlIcon(props: {
readonly kind: SourceControlIconKind;
Expand All @@ -28,6 +34,7 @@ export function SourceControlIcon(props: {
</Svg>
);
case "github":
case "github-enterprise":
return (
<ThemedSvg
width={size}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import type { StaticScreenProps } from "@react-navigation/native";
import { NativeStackScreenOptions } from "../../native/StackHeader";
import { addProjectRemoteSourceLabel } from "@t3tools/client-runtime/operations/projects";
import { addProjectRemoteTargetLabel } from "@t3tools/client-runtime/operations/projects";

import { AddProjectRepositoryScreen } from "./AddProjectScreen";

type AddProjectRepositoryRouteParams = {
readonly environmentId?: string | string[];
readonly source?: string | string[];
readonly host?: string | string[];
};

export function AddProjectRepositoryRoute({
route,
}: StaticScreenProps<AddProjectRepositoryRouteParams>) {
const params = route.params ?? {};
const source = Array.isArray(params.source) ? params.source[0] : params.source;
const host = Array.isArray(params.host) ? params.host[0] : params.host;
const title =
source === "github" ||
source === "github-enterprise" ||
source === "gitlab" ||
source === "forgejo" ||
source === "bitbucket" ||
source === "azure-devops"
? addProjectRemoteSourceLabel(source)
? addProjectRemoteTargetLabel({ id: source, source, host: host ?? null })
: "Git URL";

return (
Expand Down
56 changes: 35 additions & 21 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
addProjectRemoteSourceLabel,
addProjectRemoteSourcePathHint,
addProjectRemoteSourceProvider,
addProjectRemoteTargetLabel,
addProjectRemoteTargetReadiness,
buildAddProjectRemoteSourceReadiness,
buildAddProjectRemoteTargets,
buildProjectCreateCommand,
canCreateProjectInEnvironment,
findExistingAddProject,
Expand All @@ -15,6 +17,7 @@ import {
resolveAddProjectPath,
sortAddProjectProviderSources,
type AddProjectRemoteSource,
type AddProjectRemoteTarget,
} from "@t3tools/client-runtime/operations/projects";
import {
connectionStatusText,
Expand Down Expand Up @@ -111,6 +114,7 @@ function sourceFromParam(value: string | string[] | undefined): AddProjectRemote
if (
source === "url" ||
source === "github" ||
source === "github-enterprise" ||
source === "gitlab" ||
source === "forgejo" ||
source === "bitbucket" ||
Expand Down Expand Up @@ -420,24 +424,22 @@ function EmptyEnvironmentState() {
}

function SourceControlRow(props: {
readonly source: AddProjectRemoteSource;
readonly target: AddProjectRemoteTarget;
readonly selectedEnvironmentId: EnvironmentId;
readonly ready: boolean;
readonly hint: string;
readonly isFirst: boolean;
}) {
const navigation = useNavigation();
const title =
props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`;
const label = addProjectRemoteTargetLabel(props.target);
const title = props.target.source === "url" ? "Git URL" : `${label} repository`;
const subtitle =
props.source === "url"
? "Clone from a remote URL"
: `Clone ${addProjectRemoteSourceLabel(props.source)} ${props.hint}`;
props.target.source === "url" ? "Clone from a remote URL" : `Clone ${label} ${props.hint}`;
const icon =
props.source === "url" ? (
props.target.source === "url" ? (
<SymbolView name="link" size={17} tintColorClassName={"accent-icon"} type="monochrome" />
) : (
<SourceControlIcon kind={props.source} size={18} colorClassName="accent-icon" />
<SourceControlIcon kind={props.target.source} size={18} colorClassName="accent-icon" />
);

if (!props.ready) {
Expand All @@ -456,7 +458,8 @@ function SourceControlRow(props: {
navigation.dispatch(
StackActions.push("AddProjectRepository", {
environmentId: props.selectedEnvironmentId,
source: props.source,
source: props.target.source,
...(props.target.host ? { host: props.target.host } : {}),
}),
)
}
Expand All @@ -480,6 +483,10 @@ export function AddProjectSourceScreen() {
() => buildAddProjectRemoteSourceReadiness(discoveryState.data),
[discoveryState.data],
);
const targets = useMemo(
() => buildAddProjectRemoteTargets(discoveryState.data),
[discoveryState.data],
);

return (
<AddProjectShell>
Expand Down Expand Up @@ -552,22 +559,26 @@ export function AddProjectSourceScreen() {
)
}
/>
{(["url", ...sortAddProjectProviderSources(readiness)] as AddProjectRemoteSource[]).map(
(candidate) => (
{[
{ id: "url", source: "url", host: null } as AddProjectRemoteTarget,
...sortAddProjectProviderSources(readiness, targets),
].map((target) => {
const targetReadiness = addProjectRemoteTargetReadiness(readiness, target.id);
return (
<SourceControlRow
key={candidate}
source={candidate}
key={target.id}
target={target}
selectedEnvironmentId={selectedEnvironment.environmentId}
ready={readiness[candidate].ready}
ready={targetReadiness.ready}
hint={
readiness[candidate].ready
? addProjectRemoteSourcePathHint(candidate)
: (readiness[candidate].hint ?? "")
targetReadiness.ready
? addProjectRemoteSourcePathHint(target.source)
: (targetReadiness.hint ?? "")
}
isFirst={false}
/>
),
)}
);
})}
</ListSection>
{discoveryState.isPending ? (
<ActivityIndicator colorClassName={"accent-icon-muted"} />
Expand Down Expand Up @@ -667,13 +678,15 @@ function useEnvironmentFromParam(
export function AddProjectRepositoryScreen(props: {
readonly environmentId?: string | string[];
readonly source?: string | string[];
readonly host?: string | string[];
}) {
const lookupRepositoryQuery = useAtomQueryRunner(sourceControlEnvironment.repository, {
reportFailure: false,
});
const navigation = useNavigation();
const environment = useEnvironmentFromParam(props.environmentId);
const source = sourceFromParam(props.source);
const host = stringParam(props.host);
const [repositoryInput, setRepositoryInput] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -703,6 +716,7 @@ export function AddProjectRepositoryScreen(props: {
input: {
provider,
repository: repositoryInput.trim(),
...(host ? { host } : {}),
},
});
if (AsyncResult.isFailure(result)) {
Expand All @@ -720,7 +734,7 @@ export function AddProjectRepositoryScreen(props: {
);
}
setIsSubmitting(false);
}, [environment, isSubmitting, lookupRepositoryQuery, repositoryInput, navigation, source]);
}, [environment, host, isSubmitting, lookupRepositoryQuery, repositoryInput, navigation, source]);

return (
<AddProjectShell>
Expand Down
73 changes: 72 additions & 1 deletion apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,14 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
cause: new Error(`Unexpected repository create: ${input.repository}`),
}),
),
searchRepositories: (input) =>
Effect.fail(
new GitHubCli.GitHubCliCommandError({
command: "gh",
cwd: input.cwd,
cause: new Error(`Unexpected repository search: ${input.query}`),
}),
),
checkoutPullRequest: (input) =>
execute({
cwd: input.cwd,
Expand Down Expand Up @@ -635,6 +643,7 @@ function makeManager(input?: {
textGeneration?: Partial<FakeGitTextGeneration>;
serverSettings?: Parameters<typeof ServerSettings.layerTest>[0];
setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"];
providerKind?: "github" | "github-enterprise";
gitConfigReads?: string[];
}) {
const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);
Expand Down Expand Up @@ -672,7 +681,7 @@ function makeManager(input?: {
const sourceControlRegistryLayer = Layer.effect(
SourceControlProviderRegistry.SourceControlProviderRegistry,
(input?.sourceControlProvider === undefined
? GitHubSourceControlProvider.make
? GitHubSourceControlProvider.makeProvider(input?.providerKind ?? "github")
: Effect.succeed(input.sourceControlProvider)
).pipe(
Effect.map((provider) =>
Expand Down Expand Up @@ -4071,6 +4080,68 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("feeds the pull request template to enterprise change requests too", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
NodeFS.mkdirSync(NodePath.join(repoDir, ".github"));
NodeFS.writeFileSync(
NodePath.join(repoDir, ".github", "pull_request_template.md"),
"## What changed?\n\n## Verification",
);
yield* runGit(repoDir, ["add", ".github/pull_request_template.md"]);
yield* runGit(repoDir, ["commit", "-m", "Add pull request template"]);
yield* runGit(repoDir, ["checkout", "-b", "feature-enterprise-template"]);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
NodeFS.writeFileSync(NodePath.join(repoDir, "changes.txt"), "change\n");
yield* runGit(repoDir, ["add", "changes.txt"]);
yield* runGit(repoDir, ["commit", "-m", "Feature commit"]);
yield* runGit(repoDir, ["push", "-u", "origin", "feature-enterprise-template"]);
yield* runGit(repoDir, [
"config",
"branch.feature-enterprise-template.gh-merge-base",
"main",
]);
let generatedChangeRequestTemplate: string | undefined;

const { manager } = yield* makeManager({
providerKind: "github-enterprise",
textGeneration: {
generatePrContent: (input) => {
generatedChangeRequestTemplate = input.changeRequestTemplate;
return Effect.succeed({
title: "Add stacked git actions",
body: "## What changed?\nAdded stacked git actions.",
});
},
},
ghScenario: {
prListSequence: [
"[]",
// @effect-diagnostics-next-line preferSchemaOverJson:off
JSON.stringify([
{
number: 12,
title: "Add stacked git actions",
url: "https://git.corp.com/owner/repo/pull/12",
baseRefName: "main",
headRefName: "feature-enterprise-template",
},
]),
],
},
});
const result = yield* runStackedAction(manager, {
cwd: repoDir,
action: "commit_push_pr",
});

expect(result.pr.status).toBe("created");
expect(generatedChangeRequestTemplate).toBe("## What changed?\n\n## Verification");
}),
);

it.effect("generates PR content against the remote base when the local base is stale", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2020,7 +2020,8 @@ export const make = Effect.gen(function* () {
const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef);
const policy = yield* resolveStylePolicy(cwd, settings);
const changeRequestTemplate =
settings.style.followChangeRequestTemplates && provider.kind === "github"
settings.style.followChangeRequestTemplates &&
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
JorrinKievit marked this conversation as resolved.
(provider.kind === "github" || provider.kind === "github-enterprise")
? Option.getOrUndefined(yield* detectPrTemplate(cwd, baseRangeRef, gitCore.execute))
: undefined;

Expand Down
30 changes: 30 additions & 0 deletions apps/server/src/pullRequest/GitHubPullRequestCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2532,6 +2532,36 @@ layer("GitHubPullRequestCli.layer", (it) => {
}),
);

it.effect("reads the viewer from the host it was asked about", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValueOnce(Effect.succeed(output("gho_token")));
mockedExecute.mockReturnValueOnce(
// @effect-diagnostics-next-line preferSchemaOverJson:off
Effect.succeed(output(JSON.stringify({ id: 42, login: "octocat" }))),
);
const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;

const login = yield* cli.getViewerLogin({ cwd: "/w", host: "ghe.example.com" });

assert.strictEqual(login, "octocat");
// Neither call reads repository context: without the host they answer for github.com,
// which on an Enterprise install is either a different account or none at all.
assert.deepStrictEqual(mockedExecute.mock.calls[0]![0].args, [
"auth",
"token",
"--hostname",
"ghe.example.com",
]);
assert.deepStrictEqual(mockedExecute.mock.calls[1]![0].args, [
"api",
"user",
"--hostname",
"ghe.example.com",
]);
assert.strictEqual(mockedExecute.mock.calls[1]![0].env?.GH_HOST, "ghe.example.com");
}),
);

it.effect("accounts for the avatar lookup in the GraphQL budget", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValueOnce(
Expand Down
29 changes: 28 additions & 1 deletion apps/server/src/pullRequest/GitHubPullRequestProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import type { PullRequestReaction } from "@t3tools/contracts";

import * as GitHubCli from "../sourceControl/GitHubCli.ts";
import * as GitHubPullRequestCli from "./GitHubPullRequestCli.ts";
import { gitHubViewerPermissions, loginAvatarUrl, make } from "./GitHubPullRequestProvider.ts";
import {
gitHubViewerPermissions,
loginAvatarUrl,
make,
makeProvider,
} from "./GitHubPullRequestProvider.ts";
import type { GitHubReviewThreadComments } from "./gitHubPullRequestJson.ts";

it.effect("maps credential verification failures without relabeling operation failures", () =>
Expand Down Expand Up @@ -803,6 +808,28 @@ describe("getChangeRequest commits", () => {
);
});

describe("makeProvider", () => {
it.effect("registers under the enterprise kind and names it on a failure", () =>
Effect.gen(function* () {
const provider = yield* makeProvider("github-enterprise");
expect(provider.kind).toBe("github-enterprise");

const error = yield* Effect.flip(provider.getViewer({ cwd: "/w", host: "ghe.example.com" }));
expect(error.provider).toBe("github-enterprise");
expect(error.reason).toBe("unauthenticated");
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
getViewerLogin: () =>
Effect.fail(
new GitHubCli.GitHubCliAuthenticationError({ command: "gh", cwd: "/w", cause: null }),
),
}),
),
),
);
});

describe("getChangeRequestActivity dismissed reviews", () => {
const dismissedReview = (body: string) => ({
id: "PRR_1",
Expand Down
Loading
Loading