Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 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
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
8 changes: 7 additions & 1 deletion apps/mobile/src/components/SourceControlIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Svg, { Defs, LinearGradient, Path, Stop } from "react-native-svg";

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

export function SourceControlIcon(props: {
readonly kind: SourceControlIconKind;
Expand All @@ -11,6 +16,7 @@ export function SourceControlIcon(props: {

switch (props.kind) {
case "github":
case "github-enterprise":
return (
<Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
<Path
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
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 === "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,15 +1,18 @@
import {
addProjectRemoteSourceLabel,
addProjectRemoteSourcePathHint,
addProjectRemoteSourceProvider,
addProjectRemoteTargetLabel,
addProjectRemoteTargetReadiness,
buildAddProjectRemoteSourceReadiness,
buildAddProjectRemoteTargets,
buildProjectCreateCommand,
canCreateProjectInEnvironment,
findExistingAddProject,
getAddProjectInitialQuery,
resolveAddProjectPath,
sortAddProjectProviderSources,
type AddProjectRemoteSource,
type AddProjectRemoteTarget,
} from "@t3tools/client-runtime/operations/projects";
import {
connectionStatusText,
Expand Down Expand Up @@ -97,6 +100,7 @@ function sourceFromParam(value: string | string[] | undefined): AddProjectRemote
if (
source === "url" ||
source === "github" ||
source === "github-enterprise" ||
source === "gitlab" ||
source === "bitbucket" ||
source === "azure-devops"
Expand Down Expand Up @@ -368,25 +372,23 @@ 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 iconColor = useThemeColor("--color-icon");
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} tintColor={iconColor} type="monochrome" />
) : (
<SourceControlIcon kind={props.source} size={18} color={String(iconColor)} />
<SourceControlIcon kind={props.target.source} size={18} color={String(iconColor)} />
);

if (!props.ready) {
Expand All @@ -406,7 +408,8 @@ function SourceControlRow(props: {
screen: "AddProjectRepository",
params: {
environmentId: props.selectedEnvironmentId,
source: props.source,
source: props.target.source,
...(props.target.host ? { host: props.target.host } : {}),
},
})
}
Expand All @@ -432,6 +435,10 @@ export function AddProjectSourceScreen() {
() => buildAddProjectRemoteSourceReadiness(discoveryState.data),
[discoveryState.data],
);
const targets = useMemo(
() => buildAddProjectRemoteTargets(discoveryState.data),
[discoveryState.data],
);

return (
<AddProjectShell>
Expand Down Expand Up @@ -506,22 +513,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 color={accentColor} /> : null}
</>
Expand Down Expand Up @@ -594,13 +605,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 @@ -630,6 +643,7 @@ export function AddProjectRepositoryScreen(props: {
input: {
provider,
repository: repositoryInput.trim(),
...(host ? { host } : {}),
},
});
if (AsyncResult.isFailure(result)) {
Expand All @@ -647,7 +661,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 @@ -567,6 +567,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 @@ -617,6 +625,7 @@ function makeManager(input?: {
textGeneration?: Partial<FakeGitTextGeneration>;
serverSettings?: Parameters<typeof ServerSettings.layerTest>[0];
setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"];
providerKind?: "github" | "github-enterprise";
}) {
const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);
const textGeneration = createTextGeneration(input?.textGeneration);
Expand All @@ -633,7 +642,7 @@ function makeManager(input?: {
);
const sourceControlRegistryLayer = Layer.effect(
SourceControlProviderRegistry.SourceControlProviderRegistry,
GitHubSourceControlProvider.make.pipe(
GitHubSourceControlProvider.makeProvider(input?.providerKind ?? "github").pipe(
Effect.map((provider) =>
SourceControlProviderRegistry.SourceControlProviderRegistry.of({
get: () => Effect.succeed(provider),
Expand Down Expand Up @@ -2712,6 +2721,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 @@ -1597,7 +1597,8 @@ export const make = Effect.gen(function* () {
const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef);
const policy = yield* resolveStylePolicy(cwd, settings.style);
const changeRequestTemplate =
settings.style.followChangeRequestTemplates && provider.kind === "github"
settings.style.followChangeRequestTemplates &&
Comment thread
cursor[bot] marked this conversation as resolved.
(provider.kind === "github" || provider.kind === "github-enterprise")
? Option.getOrUndefined(yield* detectPrTemplate(cwd, baseRangeRef, gitCore.execute))
: undefined;

Expand Down
Loading
Loading