mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
226 lines
6.1 KiB
TypeScript
226 lines
6.1 KiB
TypeScript
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 type { z } from "zod";
|
|
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;
|
|
outputPathOverride?: string;
|
|
}
|
|
export const cloneGithubRepository = async ({
|
|
type = "application",
|
|
...entity
|
|
}: CloneGithubRepository) => {
|
|
let command = "set -e;";
|
|
const isCompose = type === "compose";
|
|
const {
|
|
appName,
|
|
repository,
|
|
owner,
|
|
branch,
|
|
githubId,
|
|
enableSubmodules,
|
|
serverId,
|
|
outputPathOverride,
|
|
} = 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 = outputPathOverride ?? 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: z.infer<typeof apiFindGithubBranches>,
|
|
) => {
|
|
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;
|
|
};
|