Compare commits

...

1 Commits

Author SHA1 Message Date
Mauricio Siu
da0e726326 feat(preview-deployment): enhance external deployment support
- Add support for external preview deployments with optional GitHub comment handling
- Modify deployment services to conditionally update GitHub issue comments
- Update queue types and deployment worker to handle external deployment flag
- Refactor preview deployment creation to support external deployments
- Improve preview deployment router with more flexible deployment creation logic
2025-03-08 17:07:07 -06:00
6 changed files with 282 additions and 112 deletions

View File

@@ -1,13 +1,23 @@
import { apiFindAllByApplication } from "@/server/db/schema"; import { db } from "@/server/db";
import { apiFindAllByApplication, applications } from "@/server/db/schema";
import { import {
createPreviewDeployment,
findApplicationById, findApplicationById,
findPreviewDeploymentByApplicationId,
findPreviewDeploymentById, findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId, findPreviewDeploymentsByApplicationId,
findPreviewDeploymentsByPullRequestId,
IS_CLOUD,
removePreviewDeployment, removePreviewDeployment,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { eq } from "drizzle-orm";
import { and } from "drizzle-orm";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import type { DeploymentJob } from "@/server/queues/queue-types";
export const previewDeploymentRouter = createTRPCRouter({ export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
} }
return previewDeployment; return previewDeployment;
}), }),
create: protectedProcedure
.input(
z.object({
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
pullRequestId: z.string(),
repository: z.string(),
owner: z.string(),
branch: z.string(),
deploymentHash: z.string(),
prBranch: z.string(),
prNumber: z.any(),
prTitle: z.string(),
prURL: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const organizationId = ctx.session.activeOrganizationId;
const action = input.action;
const prId = input.pullRequestId;
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
(previewDeployment) =>
previewDeployment.application.project.organizationId ===
organizationId,
);
if (filteredPreviewDeploymentResult.length > 0) {
for (const previewDeployment of filteredPreviewDeploymentResult) {
try {
await removePreviewDeployment(
previewDeployment.previewDeploymentId,
);
} catch (error) {
console.log(error);
}
}
}
return {
message: "Preview Deployments Closed",
};
}
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const deploymentHash = input.deploymentHash;
const prBranch = input.prBranch;
const prNumber = input.prNumber;
const prTitle = input.prTitle;
const prURL = input.prURL;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, input.repository),
eq(applications.branch, input.branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, input.owner),
),
with: {
previewDeployments: true,
project: true,
},
});
const filteredApps = apps.filter(
(app) => app.project.organizationId === organizationId,
);
console.log(filteredApps);
for (const app of filteredApps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
try {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
console.log(previewDeployment);
previewDeploymentId = previewDeployment.previewDeploymentId;
} catch (error) {
console.log(error);
}
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
isExternal: true,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
}
return {
message: "Preview Deployments Created",
};
}),
}); });

View File

@@ -596,25 +596,17 @@ export const settingsRouter = createTRPCRouter({
}), }),
readStats: adminProcedure readStats: adminProcedure
.input( .input(
z z.object({
.object({ start: z.string().optional(),
dateRange: z end: z.string().optional(),
.object({ }),
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
) )
.query(({ input }) => { .query(({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return []; return [];
} }
const rawConfig = readMonitoringConfig( const rawConfig = readMonitoringConfig(!!input?.start || !!input?.end);
!!input?.dateRange?.start || !!input?.dateRange?.end, const processedLogs = processLogs(rawConfig as string, input);
);
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
return processedLogs || []; return processedLogs || [];
}), }),
haveActivateRequests: adminProcedure.query(async () => { haveActivateRequests: adminProcedure.query(async () => {

View File

@@ -98,6 +98,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId, previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
}); });
} }
} else { } else {
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId, previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
}); });
} }
} }

View File

@@ -26,6 +26,7 @@ type DeployJob =
applicationType: "application-preview"; applicationType: "application-preview";
previewDeploymentId: string; previewDeploymentId: string;
serverId?: string; serverId?: string;
isExternal?: boolean;
}; };
export type DeploymentJob = DeployJob; export type DeploymentJob = DeployJob;

View File

@@ -387,11 +387,13 @@ export const deployPreviewApplication = async ({
titleLog = "Preview Deployment", titleLog = "Preview Deployment",
descriptionLog = "", descriptionLog = "",
previewDeploymentId, previewDeploymentId,
isExternal = false,
}: { }: {
applicationId: string; applicationId: string;
titleLog: string; titleLog: string;
descriptionLog: string; descriptionLog: string;
previewDeploymentId: string; previewDeploymentId: string;
isExternal?: boolean;
}) => { }) => {
const application = await findApplicationById(applicationId); const application = await findApplicationById(applicationId);
@@ -417,46 +419,43 @@ export const deployPreviewApplication = async ({
githubId: application?.githubId || "", githubId: application?.githubId || "",
}; };
try { try {
const commentExists = await issueCommentExists({ if (!isExternal) {
...issueParams, const commentExists = await issueCommentExists({
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams, ...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
if (!commentExists) {
if (!result) { const result = await createPreviewDeploymentComment({
throw new TRPCError({ ...issueParams,
code: "NOT_FOUND", previewDomain,
message: "Pull request comment not found", appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); 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}`,
});
} }
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName; application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
...application, ...application,
@@ -466,25 +465,31 @@ export const deployPreviewApplication = async ({
}); });
await buildApplication(application, deployment.logPath); await buildApplication(application, deployment.logPath);
} }
const successComment = getIssueComment(
application.name, if (!isExternal) {
"success", const successComment = getIssueComment(
previewDomain, application.name,
); "success",
await updateIssueComment({ previewDomain,
...issueParams, );
body: `### Dokploy Preview Deployment\n\n${successComment}`, await updateIssueComment({
}); ...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done", previewStatus: "done",
}); });
} catch (error) { } catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain); if (!isExternal) {
await updateIssueComment({ const comment = getIssueComment(application.name, "error", previewDomain);
...issueParams, await updateIssueComment({
body: `### Dokploy Preview Deployment\n\n${comment}`, ...issueParams,
}); body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error", previewStatus: "error",
@@ -500,11 +505,13 @@ export const deployRemotePreviewApplication = async ({
titleLog = "Preview Deployment", titleLog = "Preview Deployment",
descriptionLog = "", descriptionLog = "",
previewDeploymentId, previewDeploymentId,
isExternal = false,
}: { }: {
applicationId: string; applicationId: string;
titleLog: string; titleLog: string;
descriptionLog: string; descriptionLog: string;
previewDeploymentId: string; previewDeploymentId: string;
isExternal?: boolean;
}) => { }) => {
const application = await findApplicationById(applicationId); const application = await findApplicationById(applicationId);
@@ -530,36 +537,39 @@ export const deployRemotePreviewApplication = async ({
githubId: application?.githubId || "", githubId: application?.githubId || "",
}; };
try { try {
const commentExists = await issueCommentExists({ if (!isExternal) {
...issueParams, const commentExists = await issueCommentExists({
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams, ...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
if (!commentExists) {
if (!result) { const result = await createPreviewDeploymentComment({
throw new TRPCError({ ...issueParams,
code: "NOT_FOUND", previewDomain,
message: "Pull request comment not found", appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); 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}`,
});
} }
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName; application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
@@ -586,25 +596,29 @@ export const deployRemotePreviewApplication = async ({
await mechanizeDockerContainer(application); await mechanizeDockerContainer(application);
} }
const successComment = getIssueComment( if (!isExternal) {
application.name, const successComment = getIssueComment(
"success", application.name,
previewDomain, "success",
); previewDomain,
await updateIssueComment({ );
...issueParams, await updateIssueComment({
body: `### Dokploy Preview Deployment\n\n${successComment}`, ...issueParams,
}); body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done", previewStatus: "done",
}); });
} catch (error) { } catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain); if (!isExternal) {
await updateIssueComment({ const comment = getIssueComment(application.name, "error", previewDomain);
...issueParams, await updateIssueComment({
body: `### Dokploy Preview Deployment\n\n${comment}`, ...issueParams,
}); body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error", previewStatus: "error",

View File

@@ -151,6 +151,7 @@ export const findPreviewDeploymentsByApplicationId = async (
export const createPreviewDeployment = async ( export const createPreviewDeployment = async (
schema: typeof apiCreatePreviewDeployment._type, schema: typeof apiCreatePreviewDeployment._type,
isExternal = false,
) => { ) => {
const application = await findApplicationById(schema.applicationId); const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`; const appName = `preview-${application.appName}-${generatePassword(6)}`;
@@ -165,27 +166,32 @@ export const createPreviewDeployment = async (
org?.ownerId || "", org?.ownerId || "",
); );
const octokit = authGithub(application?.github as Github); let issueId = "";
if (!isExternal) {
const octokit = authGithub(application?.github as Github);
const runningComment = getIssueComment( const runningComment = getIssueComment(
application.name, application.name,
"initializing", "initializing",
generateDomain, generateDomain,
); );
const issue = await octokit.rest.issues.createComment({ const issue = await octokit.rest.issues.createComment({
owner: application?.owner || "", owner: application?.owner || "",
repo: application?.repository || "", repo: application?.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber), issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`, body: `### Dokploy Preview Deployment\n\n${runningComment}`,
}); });
issueId = `${issue.data.id}`;
}
const previewDeployment = await db const previewDeployment = await db
.insert(previewDeployments) .insert(previewDeployments)
.values({ .values({
...schema, ...schema,
appName: appName, appName: appName,
pullRequestCommentId: `${issue.data.id}`, pullRequestCommentId: issueId,
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -231,6 +237,13 @@ export const findPreviewDeploymentsByPullRequestId = async (
) => { ) => {
const previewDeploymentResult = await db.query.previewDeployments.findMany({ const previewDeploymentResult = await db.query.previewDeployments.findMany({
where: eq(previewDeployments.pullRequestId, pullRequestId), where: eq(previewDeployments.pullRequestId, pullRequestId),
with: {
application: {
with: {
project: true,
},
},
},
}); });
return previewDeploymentResult; return previewDeploymentResult;