Skip to content

Commit 831a181

Browse files
authored
Merge pull request #3389 from tanmay-pathak/preview-deploy-rebuild
feat(preview): ✨ add manual rebuild option for previews
2 parents 7921f75 + 6b9bcbc commit 831a181

8 files changed

Lines changed: 277 additions & 4 deletions

File tree

apps/api/src/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
2525
titleLog: z.string().optional(),
2626
descriptionLog: z.string().optional(),
2727
server: z.boolean().optional(),
28-
type: z.enum(["deploy"]),
28+
type: z.enum(["deploy", "redeploy"]),
2929
applicationType: z.literal("application-preview"),
3030
serverId: z.string().min(1),
3131
}),

apps/api/src/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
deployPreviewApplication,
55
rebuildApplication,
66
rebuildCompose,
7+
rebuildPreviewApplication,
78
updateApplicationStatus,
89
updateCompose,
910
updatePreviewDeployment,
@@ -54,7 +55,14 @@ export const deploy = async (job: DeployJob) => {
5455
previewStatus: "running",
5556
});
5657
if (job.server) {
57-
if (job.type === "deploy") {
58+
if (job.type === "redeploy") {
59+
await rebuildPreviewApplication({
60+
applicationId: job.applicationId,
61+
titleLog: job.titleLog || "Rebuild Preview Deployment",
62+
descriptionLog: job.descriptionLog || "",
63+
previewDeploymentId: job.previewDeploymentId,
64+
});
65+
} else if (job.type === "deploy") {
5866
await deployPreviewApplication({
5967
applicationId: job.applicationId,
6068
titleLog: job.titleLog || "Preview Deployment",

apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ExternalLink,
33
FileText,
44
GitPullRequest,
5+
Hammer,
56
Loader2,
67
PenSquare,
78
RocketIcon,
@@ -22,6 +23,13 @@ import {
2223
CardTitle,
2324
} from "@/components/ui/card";
2425
import { Input } from "@/components/ui/input";
26+
import {
27+
Tooltip,
28+
TooltipContent,
29+
TooltipProvider,
30+
TooltipTrigger,
31+
} from "@/components/ui/tooltip";
32+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2533
import { api } from "@/utils/api";
2634
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
2735
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
@@ -38,6 +46,9 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
3846
const { mutateAsync: deletePreviewDeployment, isLoading } =
3947
api.previewDeployment.delete.useMutation();
4048

49+
const { mutateAsync: redeployPreviewDeployment } =
50+
api.previewDeployment.redeploy.useMutation();
51+
4152
const {
4253
data: previewDeployments,
4354
refetch: refetchPreviewDeployments,
@@ -46,6 +57,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
4657
{ applicationId },
4758
{
4859
enabled: !!applicationId,
60+
refetchInterval: (data) =>
61+
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
4962
},
5063
);
5164

@@ -193,6 +206,58 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
193206
</Button>
194207
</ShowDeploymentsModal>
195208

209+
<DialogAction
210+
title="Rebuild Preview Deployment"
211+
description="Are you sure you want to rebuild this preview deployment?"
212+
type="default"
213+
onClick={async () => {
214+
await redeployPreviewDeployment({
215+
previewDeploymentId:
216+
deployment.previewDeploymentId,
217+
})
218+
.then(() => {
219+
toast.success(
220+
"Preview deployment rebuild started",
221+
);
222+
refetchPreviewDeployments();
223+
})
224+
.catch(() => {
225+
toast.error(
226+
"Error rebuilding preview deployment",
227+
);
228+
});
229+
}}
230+
>
231+
<Button
232+
variant="outline"
233+
size="sm"
234+
isLoading={status === "running"}
235+
className="gap-2"
236+
>
237+
<TooltipProvider>
238+
<Tooltip>
239+
<TooltipTrigger asChild>
240+
<div className="flex items-center gap-2">
241+
<Hammer className="size-4" />
242+
Rebuild
243+
</div>
244+
</TooltipTrigger>
245+
<TooltipPrimitive.Portal>
246+
<TooltipContent
247+
sideOffset={5}
248+
className="z-[60]"
249+
>
250+
<p>
251+
Rebuild the preview deployment without
252+
downloading new code
253+
</p>
254+
</TooltipContent>
255+
</TooltipPrimitive.Portal>
256+
</Tooltip>
257+
</TooltipProvider>
258+
</Button>
259+
</DialogAction>
260+
196261
<AddPreviewDomain
197262
previewDeploymentId={`${deployment.previewDeploymentId}`}
198263
domainId={deployment.domain?.domainId}

apps/dokploy/server/api/routers/preview-deployment.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import {
22
findApplicationById,
33
findPreviewDeploymentById,
44
findPreviewDeploymentsByApplicationId,
5+
IS_CLOUD,
56
removePreviewDeployment,
67
} from "@dokploy/server";
78
import { TRPCError } from "@trpc/server";
89
import { z } from "zod";
910
import { apiFindAllByApplication } from "@/server/db/schema";
11+
import type { DeploymentJob } from "@/server/queues/queue-types";
12+
import { myQueue } from "@/server/queues/queueSetup";
13+
import { deploy } from "@/server/utils/deploy";
1014
import { createTRPCRouter, protectedProcedure } from "../trpc";
1115

1216
export const previewDeploymentRouter = createTRPCRouter({
@@ -60,4 +64,55 @@ export const previewDeploymentRouter = createTRPCRouter({
6064
}
6165
return previewDeployment;
6266
}),
67+
redeploy: protectedProcedure
68+
.input(
69+
z.object({
70+
previewDeploymentId: z.string(),
71+
title: z.string().optional(),
72+
description: z.string().optional(),
73+
}),
74+
)
75+
.mutation(async ({ input, ctx }) => {
76+
const previewDeployment = await findPreviewDeploymentById(
77+
input.previewDeploymentId,
78+
);
79+
if (
80+
previewDeployment.application.environment.project.organizationId !==
81+
ctx.session.activeOrganizationId
82+
) {
83+
throw new TRPCError({
84+
code: "UNAUTHORIZED",
85+
message: "You are not authorized to redeploy this preview deployment",
86+
});
87+
}
88+
const application = await findApplicationById(
89+
previewDeployment.applicationId,
90+
);
91+
const jobData: DeploymentJob = {
92+
applicationId: previewDeployment.applicationId,
93+
titleLog: input.title || "Rebuild Preview Deployment",
94+
descriptionLog: input.description || "",
95+
type: "redeploy",
96+
applicationType: "application-preview",
97+
previewDeploymentId: input.previewDeploymentId,
98+
server: !!application.serverId,
99+
};
100+
101+
if (IS_CLOUD && application.serverId) {
102+
jobData.serverId = application.serverId;
103+
deploy(jobData).catch((error) => {
104+
console.error("Background deployment failed:", error);
105+
});
106+
return true;
107+
}
108+
await myQueue.add(
109+
"deployments",
110+
{ ...jobData },
111+
{
112+
removeOnComplete: true,
113+
removeOnFail: true,
114+
},
115+
);
116+
return true;
117+
}),
63118
});

apps/dokploy/server/queues/deployments-queue.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
deployPreviewApplication,
55
rebuildApplication,
66
rebuildCompose,
7+
rebuildPreviewApplication,
78
updateApplicationStatus,
89
updateCompose,
910
updatePreviewDeployment,
@@ -54,7 +55,14 @@ export const deploymentWorker = new Worker(
5455
previewStatus: "running",
5556
});
5657

57-
if (job.data.type === "deploy") {
58+
if (job.data.type === "redeploy") {
59+
await rebuildPreviewApplication({
60+
applicationId: job.data.applicationId,
61+
titleLog: job.data.titleLog,
62+
descriptionLog: job.data.descriptionLog,
63+
previewDeploymentId: job.data.previewDeploymentId,
64+
});
65+
} else if (job.data.type === "deploy") {
5866
await deployPreviewApplication({
5967
applicationId: job.data.applicationId,
6068
titleLog: job.data.titleLog,

apps/dokploy/server/queues/queue-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type DeployJob =
2222
titleLog: string;
2323
descriptionLog: string;
2424
server?: boolean;
25-
type: "deploy";
25+
type: "deploy" | "redeploy";
2626
applicationType: "application-preview";
2727
previewDeploymentId: string;
2828
serverId?: string;

packages/server/src/lib/auth.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ const { handler, api } = betterAuth({
4545
return [
4646
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
4747
...(settings?.host ? [`https://${settings?.host}`] : []),
48+
...(process.env.NODE_ENV === "development"
49+
? [
50+
"http://localhost:3000",
51+
"https://absolutely-handy-falcon.ngrok-free.app",
52+
]
53+
: []),
4854
];
4955
},
5056
}),

packages/server/src/services/application.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,137 @@ export const deployPreviewApplication = async ({
452452
return true;
453453
};
454454

455+
export const rebuildPreviewApplication = async ({
456+
applicationId,
457+
titleLog = "Rebuild Preview Deployment",
458+
descriptionLog = "",
459+
previewDeploymentId,
460+
}: {
461+
applicationId: string;
462+
titleLog: string;
463+
descriptionLog: string;
464+
previewDeploymentId: string;
465+
}) => {
466+
const application = await findApplicationById(applicationId);
467+
const previewDeployment =
468+
await findPreviewDeploymentById(previewDeploymentId);
469+
470+
const deployment = await createDeploymentPreview({
471+
title: titleLog,
472+
description: descriptionLog,
473+
previewDeploymentId: previewDeploymentId,
474+
});
475+
476+
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
477+
const issueParams = {
478+
owner: application?.owner || "",
479+
repository: application?.repository || "",
480+
issue_number: previewDeployment.pullRequestNumber,
481+
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
482+
githubId: application?.githubId || "",
483+
};
484+
485+
try {
486+
const commentExists = await issueCommentExists({
487+
...issueParams,
488+
});
489+
if (!commentExists) {
490+
const result = await createPreviewDeploymentComment({
491+
...issueParams,
492+
previewDomain,
493+
appName: previewDeployment.appName,
494+
githubId: application?.githubId || "",
495+
previewDeploymentId,
496+
});
497+
498+
if (!result) {
499+
throw new TRPCError({
500+
code: "NOT_FOUND",
501+
message: "Pull request comment not found",
502+
});
503+
}
504+
505+
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
506+
}
507+
508+
const buildingComment = getIssueComment(
509+
application.name,
510+
"running",
511+
previewDomain,
512+
);
513+
await updateIssueComment({
514+
...issueParams,
515+
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
516+
});
517+
518+
// Set application properties for preview deployment
519+
application.appName = previewDeployment.appName;
520+
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
521+
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
522+
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
523+
application.rollbackActive = false;
524+
application.buildRegistry = null;
525+
application.rollbackRegistry = null;
526+
application.registry = null;
527+
528+
const serverId = application.serverId;
529+
let command = "set -e;";
530+
// Only rebuild, don't clone repository
531+
command += await getBuildCommand(application);
532+
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
533+
if (serverId) {
534+
await execAsyncRemote(serverId, commandWithLog);
535+
} else {
536+
await execAsync(commandWithLog);
537+
}
538+
await mechanizeDockerContainer(application);
539+
540+
const successComment = getIssueComment(
541+
application.name,
542+
"success",
543+
previewDomain,
544+
);
545+
await updateIssueComment({
546+
...issueParams,
547+
body: `### Dokploy Preview Deployment\n\n${successComment}`,
548+
});
549+
await updateDeploymentStatus(deployment.deploymentId, "done");
550+
await updatePreviewDeployment(previewDeploymentId, {
551+
previewStatus: "done",
552+
});
553+
} catch (error) {
554+
let command = "";
555+
556+
// Only log details for non-ExecError errors
557+
if (!(error instanceof ExecError)) {
558+
const message = error instanceof Error ? error.message : String(error);
559+
const encodedMessage = encodeBase64(message);
560+
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
561+
}
562+
563+
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
564+
const serverId = application.buildServerId || application.serverId;
565+
if (serverId) {
566+
await execAsyncRemote(serverId, command);
567+
} else {
568+
await execAsync(command);
569+
}
570+
571+
const comment = getIssueComment(application.name, "error", previewDomain);
572+
await updateIssueComment({
573+
...issueParams,
574+
body: `### Dokploy Preview Deployment\n\n${comment}`,
575+
});
576+
await updateDeploymentStatus(deployment.deploymentId, "error");
577+
await updatePreviewDeployment(previewDeploymentId, {
578+
previewStatus: "error",
579+
});
580+
throw error;
581+
}
582+
583+
return true;
584+
};
585+
455586
export const getApplicationStats = async (appName: string) => {
456587
if (appName === "dokploy") {
457588
return await getAdvancedStats(appName);

0 commit comments

Comments
 (0)