feat: add baseUrl to github provider to support github enterprise url

This commit is contained in:
Narciso
2026-07-31 11:42:12 -04:00
parent bb73c6e6dd
commit 1cf81e3802
18 changed files with 9576 additions and 13 deletions
+94 -2
View File
@@ -9,6 +9,94 @@ import { Octokit } from "octokit";
import { quote } from "shell-quote";
import type { z } from "zod";
export const DEFAULT_GITHUB_URL = "https://github.com";
export const DEFAULT_GITHUB_API_URL = "https://api.github.com";
const IPV4_HOSTNAME = /^\d{1,3}(\.\d{1,3}){3}$/;
/**
* Validate a user-supplied base URL: defaults the scheme to https, drops
* anything past the host, and returns the normalized URL or the reason it was
* rejected. Never falls back to github.com, so a typo cannot pass silently.
* An absent value does mean github.com.
*/
export const parseGithubBaseUrl = (
githubUrl?: string | null,
): { url: string } | { error: string } => {
const raw = githubUrl?.trim();
if (!raw) {
return { url: DEFAULT_GITHUB_URL };
}
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)
? raw
: `https://${raw}`;
let parsed: URL;
try {
parsed = new URL(withScheme);
} catch {
return { error: `"${raw}" is not a valid URL` };
}
if (parsed.protocol !== "https:") {
return { error: "Only https is supported for GitHub instances" };
}
// "acme.ghe.com." resolves the same as "acme.ghe.com", but the trailing dot
// would slip past the checks below.
if (parsed.hostname.endsWith(".")) {
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
}
const { hostname } = parsed;
// Rejects hosts that cannot serve Git, not hosts that are internal: a
// self-hosted Enterprise Server usually resolves to a private address.
if (hostname.startsWith("[") || IPV4_HOSTNAME.test(hostname)) {
return { error: "IP addresses are not supported, use a hostname" };
}
if (!hostname.includes(".")) {
return { error: `"${hostname}" is not a fully qualified hostname` };
}
return { url: `${parsed.protocol}//${parsed.host}` };
};
/**
* Lenient read-path variant: stored values already passed parseGithubBaseUrl,
* and a deploy is a worse place to throw than to fall back.
*/
export const normalizeGithubUrl = (githubUrl?: string | null): string => {
const result = parseGithubBaseUrl(githubUrl);
return "url" in result ? result.url : DEFAULT_GITHUB_URL;
};
/**
* Derive the REST API endpoint for a GitHub base URL. The three products do not
* agree on this:
* - github.com -> https://api.github.com
* - Enterprise Cloud w/ data residency (`<tenant>.ghe.com`)
* -> https://api.<tenant>.ghe.com
* - Enterprise Server (self-hosted) -> https://<host>/api/v3
*/
export const deriveGithubApiUrl = (githubUrl?: string | null): string => {
const normalized = normalizeGithubUrl(githubUrl);
const { protocol, host } = new URL(normalized);
if (host === "github.com" || host === "www.github.com") {
return DEFAULT_GITHUB_API_URL;
}
// Data residency tenants keep the api. prefix; GHES does not have one.
if (host === "ghe.com" || host.endsWith(".ghe.com")) {
return `${protocol}//api.${host}`;
}
return `${protocol}//${host}/api/v3`;
};
export const authGithub = (githubProvider: Github): Octokit => {
if (!haveGithubRequirements(githubProvider)) {
throw new TRPCError({
@@ -24,6 +112,7 @@ export const authGithub = (githubProvider: Github): Octokit => {
privateKey: githubProvider?.githubPrivateKey || "",
installationId: githubProvider?.githubInstallationId,
},
baseUrl: deriveGithubApiUrl(githubProvider?.githubUrl),
});
return octokit;
@@ -162,10 +251,11 @@ export const cloneGithubRepository = async ({
const outputPath = outputPathOverride ?? join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
const cloneBase = new URL(normalizeGithubUrl(githubProvider.githubUrl));
const repoclone = `${cloneBase.host}/${owner}/${repository}.git`;
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
const cloneUrl = `${cloneBase.protocol}//oauth2:${token}@${repoclone}`;
command += `echo ${quote([`Cloning Repo ${repoclone} to ${outputPath}: ✅`])};`;
command += `git clone --branch ${quote([String(branch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
@@ -187,6 +277,7 @@ export const getGithubRepositories = async (githubId?: string) => {
privateKey: githubProvider.githubPrivateKey,
installationId: githubProvider.githubInstallationId,
},
baseUrl: deriveGithubApiUrl(githubProvider.githubUrl),
});
const repositories = (await octokit.paginate(
@@ -213,6 +304,7 @@ export const getGithubBranches = async (
privateKey: githubProvider.githubPrivateKey,
installationId: githubProvider.githubInstallationId,
},
baseUrl: deriveGithubApiUrl(githubProvider.githubUrl),
});
const branches = (await octokit.paginate(octokit.rest.repos.listBranches, {