mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
370 lines
9.7 KiB
TypeScript
370 lines
9.7 KiB
TypeScript
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";
|
|
recurseSubmodules?: boolean;
|
|
}
|
|
export const cloneGithubRepository = async ({
|
|
logPath,
|
|
type = "application",
|
|
recurseSubmodules = true,
|
|
...entity
|
|
}: CloneGithubRepository) => {
|
|
const isCompose = type === "compose";
|
|
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
|
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
const { appName, repository, owner, branch, githubId } = 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",
|
|
cloneUrl,
|
|
outputPath,
|
|
"--progress",
|
|
];
|
|
if (recurseSubmodules) {
|
|
cloneArgs.splice(4, 0, "--recurse-submodules");
|
|
}
|
|
await spawnAsync("git", cloneArgs, (data) => {
|
|
if (writeStream.writable) {
|
|
writeStream.write(data);
|
|
}
|
|
});
|
|
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
|
} catch (error) {
|
|
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
|
throw error;
|
|
} finally {
|
|
writeStream.end();
|
|
}
|
|
};
|
|
|
|
export const getGithubCloneCommand = async ({
|
|
logPath,
|
|
type = "application",
|
|
recurseSubmodules = true,
|
|
...entity
|
|
}: CloneGithubRepository & { serverId: string }) => {
|
|
const { appName, repository, owner, branch, githubId, serverId } = 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 ${recurseSubmodules ? "--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,
|
|
recurseSubmodules = true,
|
|
} = 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",
|
|
cloneUrl,
|
|
outputPath,
|
|
"--progress",
|
|
];
|
|
if (recurseSubmodules) {
|
|
cloneArgs.splice(4, 0, "--recurse-submodules");
|
|
}
|
|
await spawnAsync("git", cloneArgs);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
|
const {
|
|
appName,
|
|
repository,
|
|
owner,
|
|
branch,
|
|
githubId,
|
|
serverId,
|
|
recurseSubmodules = true,
|
|
} = 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 ${recurseSubmodules ? "--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;
|
|
};
|