mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add clone by gitlab, github and bitbucket
This commit is contained in:
@@ -11,11 +11,13 @@ import {
|
||||
getBitbucketProvider,
|
||||
getGitlabProvider,
|
||||
haveGithubRequirements,
|
||||
haveGitlabRequirements,
|
||||
removeGithubProvider,
|
||||
updateGitlabProvider,
|
||||
} from "../services/git-provider";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
haveGitlabRequirements,
|
||||
refreshGitlabToken,
|
||||
} from "@/server/utils/providers/gitlab";
|
||||
|
||||
export const gitProvider = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async () => {
|
||||
@@ -164,8 +166,7 @@ export const gitProvider = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
console.log(input);
|
||||
if (!input.gitlabProviderId || !input.repo || !input.owner) {
|
||||
if (!input.gitlabProviderId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -329,47 +330,6 @@ export const gitProvider = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
});
|
||||
async function refreshGitlabToken(gitlabProviderId: string) {
|
||||
const gitlabProvider = await getGitlabProvider(gitlabProviderId);
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
const safetyMargin = 60;
|
||||
if (
|
||||
gitlabProvider.expiresAt &&
|
||||
currentTime + safetyMargin < gitlabProvider.expiresAt
|
||||
) {
|
||||
console.log("Token still valid, no need to refresh");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: gitlabProvider.refreshToken as string,
|
||||
client_id: gitlabProvider.applicationId as string,
|
||||
client_secret: gitlabProvider.secret as string,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const expiresAt = Math.floor(Date.now() / 1000) + data.expires_in;
|
||||
|
||||
await updateGitlabProvider(gitlabProviderId, {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
// 1725175543
|
||||
// {
|
||||
// access_token: '11d422887d8fac712191ee9b09dfdb043a705938cd67a4a39f36b4bc65b3106d',
|
||||
|
||||
@@ -21,6 +21,8 @@ import { createDeployment, updateDeploymentStatus } from "./deployment";
|
||||
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { cloneGitlabRepository } from "@/server/utils/providers/gitlab";
|
||||
import { cloneBitbucketRepository } from "@/server/utils/providers/bitbucket";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
@@ -81,6 +83,9 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
security: true,
|
||||
ports: true,
|
||||
registry: true,
|
||||
gitlabProvider: true,
|
||||
githubProvider: true,
|
||||
bitbucketProvider: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
@@ -150,7 +155,13 @@ export const deployApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository(admin, application, deployment.logPath);
|
||||
await cloneGithubRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
@@ -214,6 +225,10 @@ export const rebuildApplication = async ({
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
|
||||
@@ -142,10 +142,6 @@ export const haveGithubRequirements = (githubProvider: GithubProvider) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const haveGitlabRequirements = (gitlabProvider: GitlabProvider) => {
|
||||
return !!(gitlabProvider?.accessToken && gitlabProvider?.refreshToken);
|
||||
};
|
||||
|
||||
export const getGitlabProvider = async (gitlabProviderId: string) => {
|
||||
const gitlabProviderResult = await db.query.gitlabProvider.findFirst({
|
||||
where: eq(gitlabProvider.gitlabProviderId, gitlabProviderId),
|
||||
|
||||
110
apps/dokploy/server/utils/providers/bitbucket.ts
Normal file
110
apps/dokploy/server/utils/providers/bitbucket.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
|
||||
export type ApplicationWithBitbucket = InferResultType<
|
||||
"applications",
|
||||
{ bitbucketProvider: true }
|
||||
>;
|
||||
|
||||
export const cloneBitbucketRepository = async (
|
||||
entity: ApplicationWithBitbucket,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketProviderId,
|
||||
bitbucketProvider,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketProviderId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawBitbucketRepository = async (
|
||||
entity: ApplicationWithBitbucket,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketProviderId,
|
||||
bitbucketProvider,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketProviderId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,19 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { type Admin, findAdmin } from "@/server/api/services/admin";
|
||||
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
|
||||
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 { InferResultType } from "@/server/types/with";
|
||||
import {
|
||||
getGithubProvider,
|
||||
type GithubProvider,
|
||||
} from "@/server/api/services/git-provider";
|
||||
|
||||
export const authGithub = (admin: Admin) => {
|
||||
if (!haveGithubRequirements(admin)) {
|
||||
export const authGithub = (githubProvider: GithubProvider) => {
|
||||
if (!haveGithubRequirements(githubProvider)) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Github Account not configured correctly",
|
||||
@@ -19,9 +23,9 @@ export const authGithub = (admin: Admin) => {
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: admin?.githubAppId || 0,
|
||||
privateKey: admin?.githubPrivateKey || "",
|
||||
installationId: admin?.githubInstallationId,
|
||||
appId: githubProvider?.githubAppId || 0,
|
||||
privateKey: githubProvider?.githubPrivateKey || "",
|
||||
installationId: githubProvider?.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,11 +44,11 @@ export const getGithubToken = async (
|
||||
return installation.token;
|
||||
};
|
||||
|
||||
export const haveGithubRequirements = (admin: Admin) => {
|
||||
export const haveGithubRequirements = (githubProvider: GithubProvider) => {
|
||||
return !!(
|
||||
admin?.githubAppId &&
|
||||
admin?.githubPrivateKey &&
|
||||
admin?.githubInstallationId
|
||||
githubProvider?.githubAppId &&
|
||||
githubProvider?.githubPrivateKey &&
|
||||
githubProvider?.githubInstallationId
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,19 +67,24 @@ const getErrorCloneRequirements = (entity: {
|
||||
return reasons;
|
||||
};
|
||||
|
||||
export type ApplicationWithGithub = InferResultType<
|
||||
"applications",
|
||||
{ githubProvider: true }
|
||||
>;
|
||||
export const cloneGithubRepository = async (
|
||||
admin: Admin,
|
||||
entity: {
|
||||
appName: string;
|
||||
repository?: string | null;
|
||||
owner?: string | null;
|
||||
branch?: string | null;
|
||||
},
|
||||
entity: ApplicationWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch } = entity;
|
||||
const { appName, repository, owner, branch, githubProviderId } = entity;
|
||||
|
||||
if (!githubProviderId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
@@ -92,9 +101,11 @@ export const cloneGithubRepository = async (
|
||||
message: "Error: GitHub repository information is incomplete.",
|
||||
});
|
||||
}
|
||||
|
||||
const githubProvider = await getGithubProvider(githubProviderId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(admin);
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
@@ -129,17 +140,21 @@ export const cloneGithubRepository = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepository = async (entity: {
|
||||
appName: string;
|
||||
repository?: string | null;
|
||||
owner?: string | null;
|
||||
branch?: string | null;
|
||||
}) => {
|
||||
const { appName, repository, owner, branch } = entity;
|
||||
const admin = await findAdmin();
|
||||
export const cloneRawGithubRepository = async (
|
||||
entity: ApplicationWithGithub,
|
||||
) => {
|
||||
const { appName, repository, owner, branch, githubProviderId } = entity;
|
||||
|
||||
if (!githubProviderId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
const githubProvider = await getGithubProvider(githubProviderId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(admin);
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
194
apps/dokploy/server/utils/providers/gitlab.ts
Normal file
194
apps/dokploy/server/utils/providers/gitlab.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { findAdmin } from "@/server/api/services/admin";
|
||||
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import {
|
||||
getGitlabProvider,
|
||||
type GitlabProvider,
|
||||
updateGitlabProvider,
|
||||
} from "@/server/api/services/git-provider";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
|
||||
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
const gitlabProvider = await getGitlabProvider(gitlabProviderId);
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
const safetyMargin = 60;
|
||||
if (
|
||||
gitlabProvider.expiresAt &&
|
||||
currentTime + safetyMargin < gitlabProvider.expiresAt
|
||||
) {
|
||||
console.log("Token still valid, no need to refresh");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: gitlabProvider.refreshToken as string,
|
||||
client_id: gitlabProvider.applicationId as string,
|
||||
client_secret: gitlabProvider.secret as string,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const expiresAt = Math.floor(Date.now() / 1000) + data.expires_in;
|
||||
|
||||
await updateGitlabProvider(gitlabProviderId, {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const haveGitlabRequirements = (gitlabProvider: GitlabProvider) => {
|
||||
return !!(gitlabProvider?.accessToken && gitlabProvider?.refreshToken);
|
||||
};
|
||||
|
||||
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 ApplicationWithGitlab = InferResultType<
|
||||
"applications",
|
||||
{ gitlabProvider: true }
|
||||
>;
|
||||
|
||||
export const cloneGitlabRepository = async (
|
||||
entity: ApplicationWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabBranch,
|
||||
gitlabProviderId,
|
||||
gitlabProvider,
|
||||
} = entity;
|
||||
|
||||
if (!gitlabProviderId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabProviderId);
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Check if requirements are met
|
||||
if (requirements.length > 0) {
|
||||
writeStream.write(
|
||||
`\nGitLab 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: GitLab repository information is incomplete.",
|
||||
});
|
||||
}
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabOwner}/${gitlabRepository}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(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 cloneRawGitlabRepository = async (
|
||||
entity: ApplicationWithGitlab,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabBranch,
|
||||
gitlabProviderId,
|
||||
gitlabProvider,
|
||||
} = entity;
|
||||
|
||||
if (!gitlabProviderId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabProviderId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabOwner}/${gitlabRepository}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user