Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
titleLog: z.string().optional(),
descriptionLog: z.string().optional(),
server: z.boolean().optional(),
type: z.enum(["deploy"]),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application-preview"),
serverId: z.string().min(1),
}),
Expand Down
10 changes: 9 additions & 1 deletion apps/api/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
deployPreviewApplication,
rebuildApplication,
rebuildCompose,
rebuildPreviewApplication,
updateApplicationStatus,
updateCompose,
updatePreviewDeployment,
Expand Down Expand Up @@ -54,7 +55,14 @@ export const deploy = async (job: DeployJob) => {
previewStatus: "running",
});
if (job.server) {
if (job.type === "deploy") {
if (job.type === "redeploy") {
await rebuildPreviewApplication({
applicationId: job.applicationId,
titleLog: job.titleLog || "Rebuild Preview Deployment",
descriptionLog: job.descriptionLog || "",
previewDeploymentId: job.previewDeploymentId,
});
} else if (job.type === "deploy") {
await deployPreviewApplication({
applicationId: job.applicationId,
titleLog: job.titleLog || "Preview Deployment",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ExternalLink,
FileText,
GitPullRequest,
Hammer,
Loader2,
PenSquare,
RocketIcon,
Expand All @@ -22,6 +23,13 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
Expand All @@ -38,6 +46,9 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { mutateAsync: deletePreviewDeployment, isLoading } =
api.previewDeployment.delete.useMutation();

const { mutateAsync: redeployPreviewDeployment } =
api.previewDeployment.redeploy.useMutation();

const {
data: previewDeployments,
refetch: refetchPreviewDeployments,
Expand All @@ -46,6 +57,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
refetchInterval: (data) =>
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
},
);

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

<DialogAction
title="Rebuild Preview Deployment"
description="Are you sure you want to rebuild this preview deployment?"
type="default"
onClick={async () => {
await redeployPreviewDeployment({
previewDeploymentId:
deployment.previewDeploymentId,
})
.then(() => {
toast.success(
"Preview deployment rebuild started",
);
refetchPreviewDeployments();
})
.catch(() => {
toast.error(
"Error rebuilding preview deployment",
);
});
}}
>
<Button
variant="outline"
size="sm"
isLoading={status === "running"}
className="gap-2"
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Hammer className="size-4" />
Rebuild
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent
sideOffset={5}
className="z-[60]"
>
<p>
Rebuild the preview deployment without
downloading new code
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</TooltipProvider>
</Button>
</DialogAction>

<AddPreviewDomain
previewDeploymentId={`${deployment.previewDeploymentId}`}
domainId={deployment.domain?.domainId}
Expand Down
55 changes: 55 additions & 0 deletions apps/dokploy/server/api/routers/preview-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import {
findApplicationById,
findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId,
IS_CLOUD,
removePreviewDeployment,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { apiFindAllByApplication } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { createTRPCRouter, protectedProcedure } from "../trpc";

export const previewDeploymentRouter = createTRPCRouter({
Expand Down Expand Up @@ -60,4 +64,55 @@ export const previewDeploymentRouter = createTRPCRouter({
}
return previewDeployment;
}),
redeploy: protectedProcedure
.input(
z.object({
previewDeploymentId: z.string(),
title: z.string().optional(),
description: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
input.previewDeploymentId,
);
if (
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this preview deployment",
});
}
const application = await findApplicationById(
previewDeployment.applicationId,
);
const jobData: DeploymentJob = {
applicationId: previewDeployment.applicationId,
titleLog: input.title || "Rebuild Preview Deployment",
descriptionLog: input.description || "",
type: "redeploy",
applicationType: "application-preview",
previewDeploymentId: input.previewDeploymentId,
server: !!application.serverId,
};

if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
deploy(jobData).catch((error) => {
console.error("Background deployment failed:", error);
});
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
return true;
}),
});
10 changes: 9 additions & 1 deletion apps/dokploy/server/queues/deployments-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
deployPreviewApplication,
rebuildApplication,
rebuildCompose,
rebuildPreviewApplication,
updateApplicationStatus,
updateCompose,
updatePreviewDeployment,
Expand Down Expand Up @@ -54,7 +55,14 @@ export const deploymentWorker = new Worker(
previewStatus: "running",
});

if (job.data.type === "deploy") {
if (job.data.type === "redeploy") {
await rebuildPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
});
} else if (job.data.type === "deploy") {
await deployPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
Expand Down
2 changes: 1 addition & 1 deletion apps/dokploy/server/queues/queue-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type DeployJob =
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy";
type: "deploy" | "redeploy";
applicationType: "application-preview";
previewDeploymentId: string;
serverId?: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/server/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ const { handler, api } = betterAuth({
return [
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
...(settings?.host ? [`https://${settings?.host}`] : []),
...(process.env.NODE_ENV === "development"
? [
"http://localhost:3000",
"https://absolutely-handy-falcon.ngrok-free.app",
]
: []),
];
},
}),
Expand Down
131 changes: 131 additions & 0 deletions packages/server/src/services/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,137 @@ export const deployPreviewApplication = async ({
return true;
};

export const rebuildPreviewApplication = async ({
applicationId,
titleLog = "Rebuild Preview Deployment",
descriptionLog = "",
previewDeploymentId,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
}) => {
const application = await findApplicationById(applicationId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);

const deployment = await createDeploymentPreview({
title: titleLog,
description: descriptionLog,
previewDeploymentId: previewDeploymentId,
});

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}`,
});

// Set application properties for preview deployment
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;

const serverId = application.serverId;
let command = "set -e;";
// Only rebuild, don't clone repository
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
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) {
let command = "";

// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}

command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
const serverId = application.buildServerId || application.serverId;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}

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 getApplicationStats = async (appName: string) => {
if (appName === "dokploy") {
return await getAdvancedStats(appName);
Expand Down