mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge pull request #3389 from tanmay-pathak/preview-deploy-rebuild
feat(preview): ✨ add manual rebuild option for previews
This commit is contained in:
@@ -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),
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
</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}
|
||||
|
||||
@@ -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({
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -22,7 +22,7 @@ type DeployJob =
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy";
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application-preview";
|
||||
previewDeploymentId: string;
|
||||
serverId?: string;
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user