import { join } from "node:path";import { paths } from "@dokploy/server/constants";import type { apiFindGithubBranches } from "@dokploy/server/db/schema";import { findGithubById, type Github } from "@dokploy/server/services/github";import type { InferResultType } from "@dokploy/server/types/with";import { createAppAuth } from "@octokit/auth-app";import { TRPCError } from "@trpc/server";import { Octokit } from "octokit"; export const authGithub = (githubProvider: Github): Octokit => { if (!haveGithubRequirements(githubProvider)) { throw new TRPCError({ code: "NOT_FOUND", message: "Github Account not configured correctly", }); } const octokit: Octokit = new Octokit({ authStrategy: createAppAuth, auth: { appId: githubProvider?.githubAppId || 0, privateKey: githubProvider?.githubPrivateKey || "", installationId: githubProvider?.githubInstallationId, }, }); return octokit;}; export const getGithubToken = async ( octokit: ReturnType<typeof authGithub>,) => { const installation = (await octokit.auth({ type: "installation", })) as { token: string; }; return installation.token;}; /** * Check if a GitHub user has write/admin permissions on a repository * This is used to validate PR authors before allowing preview deployments */export const checkUserRepositoryPermissions = async ( githubProvider: Github, owner: string, repo: string, username: string,): Promise<{ hasWriteAccess: boolean; permission: string | null }> => { try { const octokit = authGithub(githubProvider); // Check if user is a collaborator with write permissions const { data: permission } = await octokit.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username, }); // Allow only users with 'write', 'admin', or 'maintain' permissions // Currently exists Read, Triage, Write, Maintain, Admin const allowedPermissions = ["write", "admin", "maintain"]; const hasWriteAccess = allowedPermissions.includes(permission.permission); return { hasWriteAccess, permission: permission.permission, }; } catch (error) { // If user is not a collaborator, GitHub API returns 404 console.warn( `User ${username} is not a collaborator of ${owner}/${repo}:`, error, ); return { hasWriteAccess: false, permission: null, }; }}; export const haveGithubRequirements = (githubProvider: Github) => { return !!( githubProvider?.githubAppId && githubProvider?.githubPrivateKey && githubProvider?.githubInstallationId );}; const getErrorCloneRequirements = (entity: { repository?: string | null; owner?: string | null; branch?: string | null;}) => { const reasons: string[] = []; const { repository, owner, branch } = entity; if (!repository) reasons.push("1. Repository not assigned."); if (!owner) reasons.push("2. Owner not specified."); if (!branch) reasons.push("3. Branch not defined."); return reasons;}; export type ApplicationWithGithub = InferResultType< "applications", { github: true }>; export type ComposeWithGithub = InferResultType<"compose", { github: true }>; interface CloneGithubRepository { appName: string; owner: string | null; branch: string | null; githubId: string | null; repository: string | null; type?: "application" | "compose"; enableSubmodules: boolean; serverId: string | null;}export const cloneGithubRepository = async ({ type = "application", ...entity}: CloneGithubRepository) => { let command = "set -e;"; const isCompose = type === "compose"; const { appName, repository, owner, branch, githubId, enableSubmodules, serverId, } = entity; const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); if (!githubId) { command += `echo "Error: ❌ Github Provider not found"; exit 1;`; return command; } const requirements = getErrorCloneRequirements(entity); // Check if requirements are met if (requirements.length > 0) { command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`; return command; } const githubProvider = await findGithubById(githubId); const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; command += `rm -rf ${outputPath};`; command += `mkdir -p ${outputPath};`; const cloneUrl = `https://oauth2:${token}@${repoclone}`; command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`; command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; return command;}; export const getGithubRepositories = async (githubId?: string) => { if (!githubId) { return []; } const githubProvider = await findGithubById(githubId); const octokit = new Octokit({ authStrategy: createAppAuth, auth: { appId: githubProvider.githubAppId, privateKey: githubProvider.githubPrivateKey, installationId: githubProvider.githubInstallationId, }, }); const repositories = (await octokit.paginate( octokit.rest.apps.listReposAccessibleToInstallation, )) as unknown as Awaited< ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation> >["data"]["repositories"]; return repositories;}; export const getGithubBranches = async ( input: typeof apiFindGithubBranches._type,) => { if (!input.githubId) { return []; } const githubProvider = await findGithubById(input.githubId); const octokit = new Octokit({ authStrategy: createAppAuth, auth: { appId: githubProvider.githubAppId, privateKey: githubProvider.githubPrivateKey, installationId: githubProvider.githubInstallationId, }, }); const branches = (await octokit.paginate(octokit.rest.repos.listBranches, { owner: input.owner, repo: input.repo, })) as unknown as Awaited< ReturnType<typeof octokit.rest.repos.listBranches> >["data"]; return branches;};
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
import { findGithubById, type Github } from "@dokploy/server/services/github";
import type { InferResultType } from "@dokploy/server/types/with";
import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit";
export const authGithub = (githubProvider: Github): Octokit => {
if (!haveGithubRequirements(githubProvider)) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Github Account not configured correctly",
});
}
const octokit: Octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: githubProvider?.githubAppId || 0,
privateKey: githubProvider?.githubPrivateKey || "",
installationId: githubProvider?.githubInstallationId,
},
});
return octokit;
};
export const getGithubToken = async (
octokit: ReturnType<typeof authGithub>,
) => {
const installation = (await octokit.auth({
type: "installation",
})) as {
token: string;
};
return installation.token;
};
/**
* Check if a GitHub user has write/admin permissions on a repository
* This is used to validate PR authors before allowing preview deployments
*/
export const checkUserRepositoryPermissions = async (
githubProvider: Github,
owner: string,
repo: string,
username: string,
): Promise<{ hasWriteAccess: boolean; permission: string | null }> => {
try {
const octokit = authGithub(githubProvider);
// Check if user is a collaborator with write permissions
const { data: permission } =
await octokit.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
// Allow only users with 'write', 'admin', or 'maintain' permissions
// Currently exists Read, Triage, Write, Maintain, Admin
const allowedPermissions = ["write", "admin", "maintain"];
const hasWriteAccess = allowedPermissions.includes(permission.permission);
return {
hasWriteAccess,
permission: permission.permission,
};
} catch (error) {
// If user is not a collaborator, GitHub API returns 404
console.warn(
`User ${username} is not a collaborator of ${owner}/${repo}:`,
error,
);
return {
hasWriteAccess: false,
permission: null,
};
}
};
export const haveGithubRequirements = (githubProvider: Github) => {
return !!(
githubProvider?.githubAppId &&
githubProvider?.githubPrivateKey &&
githubProvider?.githubInstallationId
);
};
const getErrorCloneRequirements = (entity: {
repository?: string | null;
owner?: string | null;
branch?: string | null;
}) => {
const reasons: string[] = [];
const { repository, owner, branch } = entity;
if (!repository) reasons.push("1. Repository not assigned.");
if (!owner) reasons.push("2. Owner not specified.");
if (!branch) reasons.push("3. Branch not defined.");
return reasons;
};
export type ApplicationWithGithub = InferResultType<
"applications",
{ github: true }
>;
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
interface CloneGithubRepository {
appName: string;
owner: string | null;
branch: string | null;
githubId: string | null;
repository: string | null;
type?: "application" | "compose";
enableSubmodules: boolean;
serverId: string | null;
}
export const cloneGithubRepository = async ({
type = "application",
...entity
}: CloneGithubRepository) => {
let command = "set -e;";
const isCompose = type === "compose";
const {
appName,
repository,
owner,
branch,
githubId,
enableSubmodules,
serverId,
} = entity;
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
if (!githubId) {
command += `echo "Error: ❌ Github Provider not found"; exit 1;`;
return command;
}
const requirements = getErrorCloneRequirements(entity);
// Check if requirements are met
if (requirements.length > 0) {
command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;
return command;
}
const githubProvider = await findGithubById(githubId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
return command;
};
export const getGithubRepositories = async (githubId?: string) => {
if (!githubId) {
return [];
}
const githubProvider = await findGithubById(githubId);
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: githubProvider.githubAppId,
privateKey: githubProvider.githubPrivateKey,
installationId: githubProvider.githubInstallationId,
},
});
const repositories = (await octokit.paginate(
octokit.rest.apps.listReposAccessibleToInstallation,
)) as unknown as Awaited<
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
>["data"]["repositories"];
return repositories;
};
export const getGithubBranches = async (
input: typeof apiFindGithubBranches._type,
) => {
if (!input.githubId) {
return [];
}
const githubProvider = await findGithubById(input.githubId);
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: githubProvider.githubAppId,
privateKey: githubProvider.githubPrivateKey,
installationId: githubProvider.githubInstallationId,
},
});
const branches = (await octokit.paginate(octokit.rest.repos.listBranches, {
owner: input.owner,
repo: input.repo,
})) as unknown as Awaited<
ReturnType<typeof octokit.rest.repos.listBranches>
>["data"];
return branches;
};