Merge branch 'canary' into feat/stack-env-support

This commit is contained in:
Mauricio Siu
2024-12-08 18:35:40 -06:00
322 changed files with 39371 additions and 1168 deletions

View File

@@ -28,6 +28,7 @@ import {
getCustomGitCloneCommand,
} from "@dokploy/server/utils/providers/git";
import {
authGithub,
cloneGithubRepository,
getGithubCloneCommand,
} from "@dokploy/server/utils/providers/github";
@@ -40,8 +41,23 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin";
import { createDeployment, updateDeploymentStatus } from "./deployment";
import {
createDeployment,
createDeploymentPreview,
updateDeploymentStatus,
} from "./deployment";
import { validUniqueServerAppName } from "./project";
import {
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";
import {
createPreviewDeploymentComment,
getIssueComment,
issueCommentExists,
updateIssueComment,
} from "./github";
import { type Domain, getDomainHost } from "./domain";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -100,6 +116,7 @@ export const findApplicationById = async (applicationId: string) => {
github: true,
bitbucket: true,
server: true,
previewDeployments: true,
},
});
if (!application) {
@@ -168,7 +185,10 @@ export const deployApplication = async ({
try {
if (application.sourceType === "github") {
await cloneGithubRepository(application, deployment.logPath);
await cloneGithubRepository({
...application,
logPath: deployment.logPath,
});
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") {
await cloneGitlabRepository(application, deployment.logPath);
@@ -193,6 +213,7 @@ export const deployApplication = async ({
applicationName: application.name,
applicationType: "application",
buildLink,
adminId: application.project.adminId,
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -204,16 +225,9 @@ export const deployApplication = async ({
// @ts-ignore
errorMessage: error?.message || "Error to build",
buildLink,
adminId: application.project.adminId,
});
console.log(
"Error on ",
application.buildType,
"/",
application.sourceType,
error,
);
throw error;
}
@@ -282,7 +296,11 @@ export const deployRemoteApplication = async ({
if (application.serverId) {
let command = "set -e;";
if (application.sourceType === "github") {
command += await getGithubCloneCommand(application, deployment.logPath);
command += await getGithubCloneCommand({
...application,
serverId: application.serverId,
logPath: deployment.logPath,
});
} else if (application.sourceType === "gitlab") {
command += await getGitlabCloneCommand(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") {
@@ -314,6 +332,7 @@ export const deployRemoteApplication = async ({
applicationName: application.name,
applicationType: "application",
buildLink,
adminId: application.project.adminId,
});
} catch (error) {
// @ts-ignore
@@ -336,6 +355,7 @@ export const deployRemoteApplication = async ({
// @ts-ignore
errorMessage: error?.message || "Error to build",
buildLink,
adminId: application.project.adminId,
});
console.log(
@@ -352,6 +372,225 @@ export const deployRemoteApplication = async ({
return true;
};
export const deployPreviewApplication = async ({
applicationId,
titleLog = "Preview Deployment",
descriptionLog = "",
previewDeploymentId,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
}) => {
const application = await findApplicationById(applicationId);
const deployment = await createDeploymentPreview({
title: titleLog,
description: descriptionLog,
previewDeploymentId: previewDeploymentId,
});
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
await updatePreviewDeployment(previewDeploymentId, {
createdAt: new Date().toISOString(),
});
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
const issueParams = {
owner: application?.owner || "",
repository: application?.repository || "",
issue_number: previewDeployment.pullRequestNumber,
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = application.previewEnv;
application.buildArgs = application.previewBuildArgs;
if (application.sourceType === "github") {
await cloneGithubRepository({
...application,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
logPath: deployment.logPath,
});
await buildApplication(application, deployment.logPath);
}
// 4eef09efc46009187d668cf1c25f768d0bde4f91
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
});
throw error;
}
return true;
};
export const deployRemotePreviewApplication = async ({
applicationId,
titleLog = "Preview Deployment",
descriptionLog = "",
previewDeploymentId,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
}) => {
const application = await findApplicationById(applicationId);
const deployment = await createDeploymentPreview({
title: titleLog,
description: descriptionLog,
previewDeploymentId: previewDeploymentId,
});
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
await updatePreviewDeployment(previewDeploymentId, {
createdAt: new Date().toISOString(),
});
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
const issueParams = {
owner: application?.owner || "",
repository: application?.repository || "",
issue_number: previewDeployment.pullRequestNumber,
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = application.previewEnv;
application.buildArgs = application.previewBuildArgs;
if (application.serverId) {
let command = "set -e;";
if (application.sourceType === "github") {
command += await getGithubCloneCommand({
...application,
serverId: application.serverId,
logPath: deployment.logPath,
});
}
command += getBuildCommand(application, deployment.logPath);
await execAsyncRemote(application.serverId, command);
await mechanizeDockerContainer(application);
}
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
});
throw error;
}
return true;
};
export const rebuildRemoteApplication = async ({
applicationId,
titleLog = "Rebuild deployment",

View File

@@ -214,7 +214,11 @@ export const deployCompose = async ({
try {
if (compose.sourceType === "github") {
await cloneGithubRepository(compose, deployment.logPath, true);
await cloneGithubRepository({
...compose,
logPath: deployment.logPath,
type: "compose",
});
} else if (compose.sourceType === "gitlab") {
await cloneGitlabRepository(compose, deployment.logPath, true);
} else if (compose.sourceType === "bitbucket") {
@@ -235,6 +239,7 @@ export const deployCompose = async ({
applicationName: compose.name,
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -248,6 +253,7 @@ export const deployCompose = async ({
// @ts-ignore
errorMessage: error?.message || "Error to build",
buildLink,
adminId: compose.project.adminId,
});
throw error;
}
@@ -312,11 +318,12 @@ export const deployRemoteCompose = async ({
let command = "set -e;";
if (compose.sourceType === "github") {
command += await getGithubCloneCommand(
compose,
deployment.logPath,
true,
);
command += await getGithubCloneCommand({
...compose,
logPath: deployment.logPath,
type: "compose",
serverId: compose.serverId,
});
} else if (compose.sourceType === "gitlab") {
command += await getGitlabCloneCommand(
compose,
@@ -353,6 +360,7 @@ export const deployRemoteCompose = async ({
applicationName: compose.name,
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
});
} catch (error) {
// @ts-ignore
@@ -376,6 +384,7 @@ export const deployRemoteCompose = async ({
// @ts-ignore
errorMessage: error?.message || "Error to build",
buildLink,
adminId: compose.project.adminId,
});
throw error;
}
@@ -459,6 +468,36 @@ export const removeCompose = async (compose: Compose) => {
return true;
};
export const startCompose = async (composeId: string) => {
const compose = await findComposeById(composeId);
try {
const { COMPOSE_PATH } = paths(!!compose.serverId);
if (compose.composeType === "docker-compose") {
if (compose.serverId) {
await execAsyncRemote(
compose.serverId,
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
);
} else {
await execAsync(`docker compose -p ${compose.appName} up -d`, {
cwd: join(COMPOSE_PATH, compose.appName, "code"),
});
}
}
await updateCompose(composeId, {
composeStatus: "done",
});
} catch (error) {
await updateCompose(composeId, {
composeStatus: "idle",
});
throw error;
}
return true;
};
export const stopCompose = async (composeId: string) => {
const compose = await findComposeById(composeId);
try {

View File

@@ -5,13 +5,14 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateDeployment,
type apiCreateDeploymentCompose,
type apiCreateDeploymentPreview,
type apiCreateDeploymentServer,
deployments,
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { desc, eq } from "drizzle-orm";
import { and, desc, eq, isNull } from "drizzle-orm";
import {
type Application,
findApplicationById,
@@ -21,6 +22,11 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
findPreviewDeploymentById,
type PreviewDeployment,
updatePreviewDeployment,
} from "./preview-deployment";
export type Deployment = typeof deployments.$inferSelect;
@@ -101,6 +107,74 @@ export const createDeployment = async (
}
};
export const createDeploymentPreview = async (
deployment: Omit<
typeof apiCreateDeploymentPreview._type,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const previewDeployment = await findPreviewDeploymentById(
deployment.previewDeploymentId,
);
try {
await removeLastTenPreviewDeploymenById(
deployment.previewDeploymentId,
previewDeployment?.application?.serverId,
);
const appName = `${previewDeployment.appName}`;
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, appName, fileName);
if (previewDeployment?.application?.serverId) {
const server = await findServerById(
previewDeployment?.application?.serverId,
);
const command = `
mkdir -p ${LOGS_PATH}/${appName};
echo "Initializing deployment" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(path.join(LOGS_PATH, appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment");
}
const deploymentCreate = await db
.insert(deployments)
.values({
title: deployment.title || "Deployment",
status: "running",
logPath: logFilePath,
description: deployment.description || "",
previewDeploymentId: deployment.previewDeploymentId,
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
await updatePreviewDeployment(deployment.previewDeploymentId, {
previewStatus: "error",
});
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the deployment",
});
}
};
export const createDeploymentCompose = async (
deployment: Omit<
typeof apiCreateDeploymentCompose._type,
@@ -257,6 +331,41 @@ const removeLastTenComposeDeployments = async (
}
};
export const removeLastTenPreviewDeploymenById = async (
previewDeploymentId: string,
serverId: string | null,
) => {
const deploymentList = await db.query.deployments.findMany({
where: eq(deployments.previewDeploymentId, previewDeploymentId),
orderBy: desc(deployments.createdAt),
});
if (deploymentList.length > 10) {
const deploymentsToDelete = deploymentList.slice(10);
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
command += `
rm -rf ${logPath};
`;
await removeDeployment(oldDeployment.deploymentId);
}
await execAsyncRemote(serverId, command);
} else {
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
}
}
}
};
export const removeDeployments = async (application: Application) => {
const { appName, applicationId } = application;
const { LOGS_PATH } = paths(!!application.serverId);
@@ -269,6 +378,30 @@ export const removeDeployments = async (application: Application) => {
await removeDeploymentsByApplicationId(applicationId);
};
export const removeDeploymentsByPreviewDeploymentId = async (
previewDeployment: PreviewDeployment,
serverId: string | null,
) => {
const { appName } = previewDeployment;
const { LOGS_PATH } = paths(!!serverId);
const logsPath = path.join(LOGS_PATH, appName);
if (serverId) {
await execAsyncRemote(serverId, `rm -rf ${logsPath}`);
} else {
await removeDirectoryIfExistsContent(logsPath);
}
await db
.delete(deployments)
.where(
eq(
deployments.previewDeploymentId,
previewDeployment.previewDeploymentId,
),
)
.returning();
};
export const removeDeploymentsByComposeId = async (compose: Compose) => {
const { appName } = compose;
const { LOGS_PATH } = paths(!!compose.serverId);

View File

@@ -110,7 +110,7 @@ export const getContainersByAppNameMatch = async (
const command =
appType === "docker-compose"
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
: `${cmd} | grep ${appName}`;
: `${cmd} | grep '^.*Name: ${appName}'`;
if (serverId) {
const { stdout, stderr } = await execAsyncRemote(serverId, command);

View File

@@ -134,3 +134,7 @@ export const removeDomainById = async (domainId: string) => {
return result[0];
};
export const getDomainHost = (domain: Domain) => {
return `${domain.https ? "https" : "http"}://${domain.host}`;
};

View File

@@ -6,6 +6,8 @@ import {
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { authGithub } from "../utils/providers/github";
import { updatePreviewDeployment } from "./preview-deployment";
export type Github = typeof github.$inferSelect;
export const createGithub = async (
@@ -72,3 +74,119 @@ export const updateGithub = async (
.returning()
.then((response) => response[0]);
};
export const getIssueComment = (
appName: string,
status: "success" | "error" | "running" | "initializing",
previewDomain: string,
) => {
let statusMessage = "";
if (status === "success") {
statusMessage = "✅ Done";
} else if (status === "error") {
statusMessage = "❌ Failed";
} else if (status === "initializing") {
statusMessage = "🔄 Building";
} else {
statusMessage = "🔄 Building";
}
const finished = `
| Name | Status | Preview | Updated (UTC) |
|------------|--------------|-------------------------------------|-----------------------|
| ${appName} | ${statusMessage} | [Preview URL](${previewDomain}) | ${new Date().toISOString()} |
`;
return finished;
};
interface CommentExists {
owner: string;
repository: string;
comment_id: number;
githubId: string;
}
export const issueCommentExists = async ({
owner,
repository,
comment_id,
githubId,
}: CommentExists) => {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
try {
await octokit.rest.issues.getComment({
owner: owner || "",
repo: repository || "",
comment_id: comment_id,
});
return true;
} catch (error) {
return false;
}
};
interface Comment {
owner: string;
repository: string;
issue_number: string;
body: string;
comment_id: number;
githubId: string;
}
export const updateIssueComment = async ({
owner,
repository,
issue_number,
body,
comment_id,
githubId,
}: Comment) => {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
await octokit.rest.issues.updateComment({
owner: owner || "",
repo: repository || "",
issue_number: issue_number,
body,
comment_id: comment_id,
});
};
interface CommentCreate {
appName: string;
owner: string;
repository: string;
issue_number: string;
previewDomain: string;
githubId: string;
previewDeploymentId: string;
}
export const createPreviewDeploymentComment = async ({
owner,
repository,
issue_number,
previewDomain,
appName,
githubId,
previewDeploymentId,
}: CommentCreate) => {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
const runningComment = getIssueComment(
appName,
"initializing",
previewDomain,
);
const issue = await octokit.rest.issues.createComment({
owner: owner || "",
repo: repository || "",
issue_number: Number.parseInt(issue_number),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
return await updatePreviewDeployment(previewDeploymentId, {
pullRequestCommentId: `${issue.data.id}`,
}).then((response) => response[0]);
};

View File

@@ -14,7 +14,7 @@ export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
`${input.appName}-${generatePassword(6)}` || generateAppName("mongo");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
@@ -72,12 +72,12 @@ export const findMongoById = async (mongoId: string) => {
export const updateMongoById = async (
mongoId: string,
postgresData: Partial<Mongo>,
mongoData: Partial<Mongo>,
) => {
const result = await db
.update(mongo)
.set({
...postgresData,
...mongoData,
})
.where(eq(mongo.mongoId, mongoId))
.returning();

View File

@@ -0,0 +1,283 @@
import { db } from "@dokploy/server/db";
import {
type apiCreatePreviewDeployment,
deployments,
previewDeployments,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, desc, eq } from "drizzle-orm";
import { slugify } from "../setup/server-setup";
import { findApplicationById } from "./application";
import { createDomain } from "./domain";
import { generatePassword, generateRandomDomain } from "../templates/utils";
import { manageDomain } from "../utils/traefik/domain";
import {
removeDeployments,
removeDeploymentsByPreviewDeploymentId,
} from "./deployment";
import { removeDirectoryCode } from "../utils/filesystem/directory";
import { removeTraefikConfig } from "../utils/traefik/application";
import { removeService } from "../utils/docker/utils";
import { authGithub } from "../utils/providers/github";
import { getIssueComment, type Github } from "./github";
import { findAdminById } from "./admin";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
export const findPreviewDeploymentById = async (
previewDeploymentId: string,
) => {
const application = await db.query.previewDeployments.findFirst({
where: eq(previewDeployments.previewDeploymentId, previewDeploymentId),
with: {
domain: true,
application: {
with: {
server: true,
project: true,
},
},
},
});
if (!application) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Preview Deployment not found",
});
}
return application;
};
export const findApplicationByPreview = async (applicationId: string) => {
const application = await db.query.applications.findFirst({
with: {
previewDeployments: {
where: eq(previewDeployments.applicationId, applicationId),
},
project: true,
domains: true,
deployments: true,
mounts: true,
redirects: true,
security: true,
ports: true,
registry: true,
gitlab: true,
github: true,
bitbucket: true,
server: true,
},
});
if (!application) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Applicationnot found",
});
}
return application;
};
export const removePreviewDeployment = async (previewDeploymentId: string) => {
try {
const application = await findApplicationByPreview(previewDeploymentId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
const deployment = await db
.delete(previewDeployments)
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
.returning();
application.appName = previewDeployment.appName;
const cleanupOperations = [
async () =>
await removeDeploymentsByPreviewDeploymentId(
previewDeployment,
application.serverId,
),
async () =>
await removeDirectoryCode(application.appName, application.serverId),
async () =>
await removeTraefikConfig(application.appName, application.serverId),
async () =>
await removeService(application?.appName, application.serverId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return deployment[0];
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this preview deployment",
});
}
};
// testing-tesoitnmg-ddq0ul-preview-ihl44o
export const updatePreviewDeployment = async (
previewDeploymentId: string,
previewDeploymentData: Partial<PreviewDeployment>,
) => {
const application = await db
.update(previewDeployments)
.set({
...previewDeploymentData,
})
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
.returning();
return application;
};
export const findPreviewDeploymentsByApplicationId = async (
applicationId: string,
) => {
const deploymentsList = await db.query.previewDeployments.findMany({
where: eq(previewDeployments.applicationId, applicationId),
orderBy: desc(previewDeployments.createdAt),
with: {
deployments: {
orderBy: desc(deployments.createdAt),
},
domain: true,
},
});
return deploymentsList;
};
export const createPreviewDeployment = async (
schema: typeof apiCreatePreviewDeployment._type,
) => {
const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`;
const generateDomain = await generateWildcardDomain(
application.previewWildcard || "*.traefik.me",
appName,
application.server?.ipAddress || "",
application.project.adminId,
);
const octokit = authGithub(application?.github as Github);
const runningComment = getIssueComment(
application.name,
"initializing",
generateDomain,
);
const issue = await octokit.rest.issues.createComment({
owner: application?.owner || "",
repo: application?.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
const previewDeployment = await db
.insert(previewDeployments)
.values({
...schema,
appName: appName,
pullRequestCommentId: `${issue.data.id}`,
})
.returning()
.then((value) => value[0]);
if (!previewDeployment) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the preview deployment",
});
}
const newDomain = await createDomain({
host: generateDomain,
path: application.previewPath,
port: application.previewPort,
https: application.previewHttps,
certificateType: application.previewCertificateType,
domainType: "preview",
previewDeploymentId: previewDeployment.previewDeploymentId,
});
application.appName = appName;
await manageDomain(application, newDomain);
await db
.update(previewDeployments)
.set({
domainId: newDomain.domainId,
})
.where(
eq(
previewDeployments.previewDeploymentId,
previewDeployment.previewDeploymentId,
),
);
return previewDeployment;
};
export const findPreviewDeploymentsByPullRequestId = async (
pullRequestId: string,
) => {
const previewDeploymentResult = await db.query.previewDeployments.findMany({
where: eq(previewDeployments.pullRequestId, pullRequestId),
});
return previewDeploymentResult;
};
export const findPreviewDeploymentByApplicationId = async (
applicationId: string,
pullRequestId: string,
) => {
const previewDeploymentResult = await db.query.previewDeployments.findFirst({
where: and(
eq(previewDeployments.applicationId, applicationId),
eq(previewDeployments.pullRequestId, pullRequestId),
),
});
return previewDeploymentResult;
};
const generateWildcardDomain = async (
baseDomain: string,
appName: string,
serverIp: string,
adminId: string,
): Promise<string> => {
if (!baseDomain.startsWith("*.")) {
throw new Error('The base domain must start with "*."');
}
const hash = `${appName}`;
if (baseDomain.includes("traefik.me")) {
let ip = "";
if (process.env.NODE_ENV === "development") {
ip = "127.0.0.1";
}
if (serverIp) {
ip = serverIp;
}
if (!ip) {
const admin = await findAdminById(adminId);
ip = admin?.serverIp || "";
}
const slugIp = ip.replaceAll(".", "-");
return baseDomain.replace(
"*",
`${hash}${slugIp === "" ? "" : `-${slugIp}`}`,
);
}
return baseDomain.replace("*", hash);
};