diff --git a/apps/api/src/schema.ts b/apps/api/src/schema.ts index 5a4355956..e2f37cd1c 100644 --- a/apps/api/src/schema.ts +++ b/apps/api/src/schema.ts @@ -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), }), diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts index 0d0b574fc..13708006a 100644 --- a/apps/api/src/utils.ts +++ b/apps/api/src/utils.ts @@ -4,6 +4,7 @@ import { deployPreviewApplication, rebuildApplication, rebuildCompose, + rebuildPreviewApplication, updateApplicationStatus, updateCompose, updatePreviewDeployment, @@ -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", diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 9c2e48931..23127383a 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -2,6 +2,7 @@ import { ExternalLink, FileText, GitPullRequest, + Hammer, Loader2, PenSquare, RocketIcon, @@ -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"; @@ -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, @@ -46,6 +57,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { { applicationId }, { enabled: !!applicationId, + refetchInterval: (data) => + data?.some((d) => d.previewStatus === "running") ? 2000 : false, }, ); @@ -193,6 +206,58 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { + { + await redeployPreviewDeployment({ + previewDeploymentId: + deployment.previewDeploymentId, + }) + .then(() => { + toast.success( + "Preview deployment rebuild started", + ); + refetchPreviewDeployments(); + }) + .catch(() => { + toast.error( + "Error rebuilding preview deployment", + ); + }); + }} + > + + + + + + + Rebuild + + + + + + Rebuild the preview deployment without + downloading new code + + + + + + + + { + 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; + }), }); diff --git a/apps/dokploy/server/queues/deployments-queue.ts b/apps/dokploy/server/queues/deployments-queue.ts index 4c117e7e3..0474b63e2 100644 --- a/apps/dokploy/server/queues/deployments-queue.ts +++ b/apps/dokploy/server/queues/deployments-queue.ts @@ -4,6 +4,7 @@ import { deployPreviewApplication, rebuildApplication, rebuildCompose, + rebuildPreviewApplication, updateApplicationStatus, updateCompose, updatePreviewDeployment, @@ -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, diff --git a/apps/dokploy/server/queues/queue-types.ts b/apps/dokploy/server/queues/queue-types.ts index ef8df6943..1000725ad 100644 --- a/apps/dokploy/server/queues/queue-types.ts +++ b/apps/dokploy/server/queues/queue-types.ts @@ -22,7 +22,7 @@ type DeployJob = titleLog: string; descriptionLog: string; server?: boolean; - type: "deploy"; + type: "deploy" | "redeploy"; applicationType: "application-preview"; previewDeploymentId: string; serverId?: string; diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index d4379e4be..d952e1f6a 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -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", + ] + : []), ]; }, }), diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 61a77ae5a..335ffbf77 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -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);
+ Rebuild the preview deployment without + downloading new code +