mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-18 11:12:56 +00:00
feat: add baseUrl to github provider to support github enterprise url
This commit is contained in:
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user