import { createWriteStream } from "node:fs";import { join } from "node:path";import { paths } from "@dokploy/server/constants";import type { InferResultType } from "@dokploy/server/types/with";import { createAppAuth } from "@octokit/auth-app";import { TRPCError } from "@trpc/server";import { Octokit } from "octokit";import { recreateDirectory } from "../filesystem/directory";import { spawnAsync } from "../process/spawnAsync"; import type { apiFindGithubBranches } from "@dokploy/server/db/schema";import type { Compose } from "@dokploy/server/services/compose";import { type Github, findGithubById } from "@dokploy/server/services/github";import { execAsyncRemote } from "../process/execAsync"; 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;}; 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; logPath: string; type?: "application" | "compose"; enableSubmodules: boolean;}export const cloneGithubRepository = async ({ logPath, type = "application", ...entity}: CloneGithubRepository) => { const isCompose = type === "compose"; const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const writeStream = createWriteStream(logPath, { flags: "a" }); const { appName, repository, owner, branch, githubId, enableSubmodules } = entity; if (!githubId) { throw new TRPCError({ code: "NOT_FOUND", message: "GitHub Provider not found", }); } const requirements = getErrorCloneRequirements(entity); // Check if requirements are met if (requirements.length > 0) { writeStream.write( `\nGitHub Repository configuration failed for application: ${appName}\n`, ); writeStream.write("Reasons:\n"); writeStream.write(requirements.join("\n")); writeStream.end(); throw new TRPCError({ code: "BAD_REQUEST", message: "Error: GitHub repository information is incomplete.", }); } 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`; await recreateDirectory(outputPath); const cloneUrl = `https://oauth2:${token}@${repoclone}`; try { writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`); const cloneArgs = [ "clone", "--branch", branch!, "--depth", "1", ...(enableSubmodules ? ["--recurse-submodules"] : []), cloneUrl, outputPath, "--progress", ]; await spawnAsync("git", cloneArgs, (data) => { if (writeStream.writable) { writeStream.write(data); } }); writeStream.write(`\nCloned ${repoclone}: ✅\n`); } catch (error) { writeStream.write(`ERROR Cloning: ${error}: ❌`); throw error; } finally { writeStream.end(); }}; export const getGithubCloneCommand = async ({ logPath, type = "application", ...entity}: CloneGithubRepository & { serverId: string }) => { const { appName, repository, owner, branch, githubId, serverId, enableSubmodules, } = entity; const isCompose = type === "compose"; if (!serverId) { throw new TRPCError({ code: "NOT_FOUND", message: "Server not found", }); } if (!githubId) { const command = ` echo "Error: ❌ Github Provider not found" >> ${logPath}; exit 1; `; await execAsyncRemote(serverId, command); throw new TRPCError({ code: "NOT_FOUND", message: "GitHub Provider not found", }); } const requirements = getErrorCloneRequirements(entity); // Build log messages let logMessages = ""; if (requirements.length > 0) { logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`; logMessages += "Reasons:\n"; logMessages += requirements.join("\n"); const escapedLogMessages = logMessages .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') .replace(/\n/g, "\\n"); const bashCommand = ` echo "${escapedLogMessages}" >> ${logPath}; exit 1; # Exit with error code `; await execAsyncRemote(serverId, bashCommand); return; } const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); 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`; const cloneUrl = `https://oauth2:${token}@${repoclone}`; const cloneCommand = `rm -rf ${outputPath};mkdir -p ${outputPath};if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath}; exit 1;fiecho "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath}; `; return cloneCommand;}; export const cloneRawGithubRepository = async (entity: Compose) => { const { appName, repository, owner, branch, githubId, enableSubmodules } = entity; if (!githubId) { throw new TRPCError({ code: "NOT_FOUND", message: "GitHub Provider not found", }); } const { COMPOSE_PATH } = paths(); const githubProvider = await findGithubById(githubId); const basePath = COMPOSE_PATH; const outputPath = join(basePath, appName, "code"); const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; await recreateDirectory(outputPath); const cloneUrl = `https://oauth2:${token}@${repoclone}`; try { const cloneArgs = [ "clone", "--branch", branch!, "--depth", "1", ...(enableSubmodules ? ["--recurse-submodules"] : []), cloneUrl, outputPath, "--progress", ]; await spawnAsync("git", cloneArgs); } catch (error) { throw error; }}; export const cloneRawGithubRepositoryRemote = async (compose: Compose) => { const { appName, repository, owner, branch, githubId, serverId, enableSubmodules, } = compose; if (!serverId) { throw new TRPCError({ code: "NOT_FOUND", message: "Server not found", }); } if (!githubId) { throw new TRPCError({ code: "NOT_FOUND", message: "GitHub Provider not found", }); } const { COMPOSE_PATH } = paths(true); const githubProvider = await findGithubById(githubId); const basePath = COMPOSE_PATH; const outputPath = join(basePath, appName, "code"); const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; const cloneUrl = `https://oauth2:${token}@${repoclone}`; try { const command = ` rm -rf ${outputPath}; git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} `; await execAsyncRemote(serverId, command); } catch (error) { throw error; }}; 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 { createWriteStream } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { InferResultType } from "@dokploy/server/types/with";
import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit";
import { recreateDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import { type Github, findGithubById } from "@dokploy/server/services/github";
import { execAsyncRemote } from "../process/execAsync";
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;
};
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;
logPath: string;
type?: "application" | "compose";
enableSubmodules: boolean;
}
export const cloneGithubRepository = async ({
logPath,
type = "application",
...entity
}: CloneGithubRepository) => {
const isCompose = type === "compose";
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "GitHub Provider not found",
});
}
const requirements = getErrorCloneRequirements(entity);
// Check if requirements are met
if (requirements.length > 0) {
writeStream.write(
`\nGitHub Repository configuration failed for application: ${appName}\n`,
);
writeStream.write("Reasons:\n");
writeStream.write(requirements.join("\n"));
writeStream.end();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error: GitHub repository information is incomplete.",
});
}
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`;
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
export const getGithubCloneCommand = async ({
logPath,
type = "application",
...entity
}: CloneGithubRepository & { serverId: string }) => {
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = entity;
const isCompose = type === "compose";
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!githubId) {
const command = `
echo "Error: ❌ Github Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "GitHub Provider not found",
});
}
const requirements = getErrorCloneRequirements(entity);
// Build log messages
let logMessages = "";
if (requirements.length > 0) {
logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`;
logMessages += "Reasons:\n";
logMessages += requirements.join("\n");
const escapedLogMessages = logMessages
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n");
const bashCommand = `
echo "${escapedLogMessages}" >> ${logPath};
exit 1; # Exit with error code
`;
await execAsyncRemote(serverId, bashCommand);
return;
}
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
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`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
};
export const cloneRawGithubRepository = async (entity: Compose) => {
const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "GitHub Provider not found",
});
}
const { COMPOSE_PATH } = paths();
const githubProvider = await findGithubById(githubId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!githubId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "GitHub Provider not found",
});
}
const { COMPOSE_PATH } = paths(true);
const githubProvider = await findGithubById(githubId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}
};
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;
};