mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-14 16:05:18 +00: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(),
|
titleLog: z.string().optional(),
|
||||||
descriptionLog: z.string().optional(),
|
descriptionLog: z.string().optional(),
|
||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("application-preview"),
|
applicationType: z.literal("application-preview"),
|
||||||
serverId: z.string().min(1),
|
serverId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
deployPreviewApplication,
|
deployPreviewApplication,
|
||||||
rebuildApplication,
|
rebuildApplication,
|
||||||
rebuildCompose,
|
rebuildCompose,
|
||||||
|
rebuildPreviewApplication,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
@@ -54,7 +55,14 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
previewStatus: "running",
|
previewStatus: "running",
|
||||||
});
|
});
|
||||||
if (job.server) {
|
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({
|
await deployPreviewApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog || "Preview Deployment",
|
titleLog: job.titleLog || "Preview Deployment",
|
||||||
|
|||||||
+65
@@ -2,6 +2,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
|
Hammer,
|
||||||
Loader2,
|
Loader2,
|
||||||
PenSquare,
|
PenSquare,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
@@ -22,6 +23,13 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { api } from "@/utils/api";
|
||||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||||
@@ -38,6 +46,9 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
api.previewDeployment.delete.useMutation();
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: redeployPreviewDeployment } =
|
||||||
|
api.previewDeployment.redeploy.useMutation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: previewDeployments,
|
data: previewDeployments,
|
||||||
refetch: refetchPreviewDeployments,
|
refetch: refetchPreviewDeployments,
|
||||||
@@ -46,6 +57,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
{ applicationId },
|
{ applicationId },
|
||||||
{
|
{
|
||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
|
refetchInterval: (data) =>
|
||||||
|
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -193,6 +206,58 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ShowDeploymentsModal>
|
</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
|
<AddPreviewDomain
|
||||||
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||||
domainId={deployment.domain?.domainId}
|
domainId={deployment.domain?.domainId}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import {
|
|||||||
findApplicationById,
|
findApplicationById,
|
||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
findPreviewDeploymentsByApplicationId,
|
findPreviewDeploymentsByApplicationId,
|
||||||
|
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 { apiFindAllByApplication } from "@/server/db/schema";
|
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";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
export const previewDeploymentRouter = createTRPCRouter({
|
export const previewDeploymentRouter = createTRPCRouter({
|
||||||
@@ -60,4 +64,55 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return previewDeployment;
|
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,
|
deployPreviewApplication,
|
||||||
rebuildApplication,
|
rebuildApplication,
|
||||||
rebuildCompose,
|
rebuildCompose,
|
||||||
|
rebuildPreviewApplication,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
@@ -54,7 +55,14 @@ export const deploymentWorker = new Worker(
|
|||||||
previewStatus: "running",
|
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({
|
await deployPreviewApplication({
|
||||||
applicationId: job.data.applicationId,
|
applicationId: job.data.applicationId,
|
||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type DeployJob =
|
|||||||
titleLog: string;
|
titleLog: string;
|
||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
server?: boolean;
|
server?: boolean;
|
||||||
type: "deploy";
|
type: "deploy" | "redeploy";
|
||||||
applicationType: "application-preview";
|
applicationType: "application-preview";
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ const { handler, api } = betterAuth({
|
|||||||
return [
|
return [
|
||||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
...(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;
|
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) => {
|
export const getApplicationStats = async (appName: string) => {
|
||||||
if (appName === "dokploy") {
|
if (appName === "dokploy") {
|
||||||
return await getAdvancedStats(appName);
|
return await getAdvancedStats(appName);
|
||||||
|
|||||||
Reference in New Issue
Block a user