mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 11:06:15 +05:00
feat: visual deployment queue — show queued state in deployment list
- Add "queued" status to deploymentStatus, applicationStatus enums - Create deployment records at enqueue time for immediate UI visibility - Show blue dot for queued deployments, loading state on deploy buttons - Cancel queued deployments cleanly with proper DB + queue cleanup - Extract shared helpers: resolveQueuedDeployment, enqueueDeployment - Use millisecond-precision timestamps in log filenames to prevent collisions - Fix preview deployment grouping in in-memory queue - Add per-app try/catch in GitHub webhook handler
This commit is contained in:
parent
853ca33659
commit
7fc7718bc0
@ -61,6 +61,7 @@ vi.mock("@dokploy/server/services/admin", () => ({
|
||||
|
||||
vi.mock("@dokploy/server/services/deployment", () => ({
|
||||
createDeployment: vi.fn(),
|
||||
resolveQueuedDeployment: vi.fn(),
|
||||
updateDeploymentStatus: vi.fn(),
|
||||
updateDeployment: vi.fn(),
|
||||
}));
|
||||
@ -152,7 +153,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
||||
vi.mocked(adminService.getDokployUrl).mockResolvedValue(
|
||||
"http://localhost:3000",
|
||||
);
|
||||
vi.mocked(deploymentService.createDeployment).mockResolvedValue(
|
||||
vi.mocked(deploymentService.resolveQueuedDeployment).mockResolvedValue(
|
||||
createMockDeployment() as any,
|
||||
);
|
||||
vi.mocked(execProcess.execAsync).mockResolvedValue({
|
||||
|
||||
@ -62,6 +62,7 @@ vi.mock("@dokploy/server/services/admin", () => ({
|
||||
|
||||
vi.mock("@dokploy/server/services/deployment", () => ({
|
||||
createDeployment: vi.fn(),
|
||||
resolveQueuedDeployment: vi.fn(),
|
||||
updateDeploymentStatus: vi.fn(),
|
||||
updateDeployment: vi.fn(),
|
||||
}));
|
||||
@ -195,7 +196,7 @@ describe(
|
||||
vi.mocked(adminService.getDokployUrl).mockResolvedValue(
|
||||
"http://localhost:3000",
|
||||
);
|
||||
vi.mocked(deploymentService.createDeployment).mockResolvedValue(
|
||||
vi.mocked(deploymentService.resolveQueuedDeployment).mockResolvedValue(
|
||||
currentDeployment as any,
|
||||
);
|
||||
vi.mocked(deploymentService.updateDeploymentStatus).mockResolvedValue(
|
||||
|
||||
@ -10,6 +10,9 @@ const mocks = vi.hoisted(() => ({
|
||||
applicationsFindMany: vi.fn(),
|
||||
composeFindMany: vi.fn(),
|
||||
queueAdd: vi.fn(),
|
||||
enqueueApplicationDeployment: vi.fn(),
|
||||
enqueueComposeDeployment: vi.fn(),
|
||||
enqueuePreviewDeployment: vi.fn(),
|
||||
verify: vi.fn(),
|
||||
shouldDeploy: vi.fn(),
|
||||
createPreviewDeployment: vi.fn(),
|
||||
@ -88,6 +91,9 @@ vi.mock("@/server/queues/queueSetup", () => ({
|
||||
myQueue: {
|
||||
add: mocks.queueAdd,
|
||||
},
|
||||
enqueueApplicationDeployment: mocks.enqueueApplicationDeployment,
|
||||
enqueueComposeDeployment: mocks.enqueueComposeDeployment,
|
||||
enqueuePreviewDeployment: mocks.enqueuePreviewDeployment,
|
||||
}));
|
||||
|
||||
vi.mock("@/server/utils/deploy", () => ({
|
||||
@ -171,7 +177,9 @@ describe("GitHub app webhook auto-deploy", () => {
|
||||
mocks.verify.mockResolvedValue(true);
|
||||
mocks.shouldDeploy.mockReturnValue(true);
|
||||
mocks.composeFindMany.mockResolvedValue([]);
|
||||
mocks.queueAdd.mockResolvedValue({ id: "job-id" });
|
||||
mocks.enqueueApplicationDeployment.mockResolvedValue({ id: "job-id" });
|
||||
mocks.enqueueComposeDeployment.mockResolvedValue({ id: "job-id" });
|
||||
mocks.enqueuePreviewDeployment.mockResolvedValue({ id: "job-id" });
|
||||
|
||||
mocks.applicationsFindMany.mockImplementation(({ where }) => {
|
||||
const matches =
|
||||
@ -209,17 +217,12 @@ describe("GitHub app webhook auto-deploy", () => {
|
||||
res,
|
||||
);
|
||||
|
||||
expect(mocks.queueAdd).toHaveBeenCalledWith(
|
||||
"deployments",
|
||||
expect(mocks.enqueueApplicationDeployment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
applicationId: "application-id",
|
||||
applicationType: "application",
|
||||
type: "deploy",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}),
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({ message: "Deployed 1 apps" });
|
||||
@ -253,17 +256,12 @@ describe("GitHub app webhook auto-deploy", () => {
|
||||
|
||||
await handler(createPushRequest("main"), res);
|
||||
|
||||
expect(mocks.queueAdd).toHaveBeenCalledWith(
|
||||
"deployments",
|
||||
expect(mocks.enqueueComposeDeployment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
applicationType: "compose",
|
||||
composeId: "compose-id",
|
||||
type: "deploy",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}),
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({ message: "Deployed 1 apps" });
|
||||
@ -295,18 +293,13 @@ describe("GitHub app webhook auto-deploy", () => {
|
||||
|
||||
await handler(createTagRequest("v1.0.0"), res);
|
||||
|
||||
expect(mocks.queueAdd).toHaveBeenCalledWith(
|
||||
"deployments",
|
||||
expect(mocks.enqueueApplicationDeployment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
applicationId: "application-id",
|
||||
applicationType: "application",
|
||||
titleLog: "Tag created: v1.0.0",
|
||||
type: "deploy",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}),
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
@ -319,7 +312,9 @@ describe("GitHub app webhook auto-deploy", () => {
|
||||
|
||||
await handler(createPushRequest("feature"), res);
|
||||
|
||||
expect(mocks.queueAdd).not.toHaveBeenCalled();
|
||||
expect(mocks.enqueueApplicationDeployment).not.toHaveBeenCalled();
|
||||
expect(mocks.enqueueComposeDeployment).not.toHaveBeenCalled();
|
||||
expect(mocks.enqueuePreviewDeployment).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({ message: "No apps to deploy" });
|
||||
});
|
||||
@ -389,7 +384,9 @@ describe("GitHub app webhook preview deployments", () => {
|
||||
githubWebhookSecret: "webhook-secret",
|
||||
});
|
||||
mocks.verify.mockResolvedValue(true);
|
||||
mocks.queueAdd.mockResolvedValue({ id: "job-id" });
|
||||
mocks.enqueueApplicationDeployment.mockResolvedValue({ id: "job-id" });
|
||||
mocks.enqueueComposeDeployment.mockResolvedValue({ id: "job-id" });
|
||||
mocks.enqueuePreviewDeployment.mockResolvedValue({ id: "job-id" });
|
||||
mocks.createPreviewDeployment.mockResolvedValue({
|
||||
previewDeploymentId: "new-preview-id",
|
||||
});
|
||||
@ -411,18 +408,13 @@ describe("GitHub app webhook preview deployments", () => {
|
||||
await handler(createPullRequestRequest("synchronize"), res);
|
||||
|
||||
expect(mocks.createPreviewDeployment).not.toHaveBeenCalled();
|
||||
expect(mocks.queueAdd).toHaveBeenCalledWith(
|
||||
"deployments",
|
||||
expect(mocks.enqueuePreviewDeployment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
applicationId: "application-id",
|
||||
applicationType: "application-preview",
|
||||
previewDeploymentId: "existing-preview-0",
|
||||
type: "deploy",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}),
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
@ -439,7 +431,9 @@ describe("GitHub app webhook preview deployments", () => {
|
||||
await handler(createPullRequestRequest("opened"), res);
|
||||
|
||||
expect(mocks.createPreviewDeployment).not.toHaveBeenCalled();
|
||||
expect(mocks.queueAdd).not.toHaveBeenCalled();
|
||||
expect(mocks.enqueueApplicationDeployment).not.toHaveBeenCalled();
|
||||
expect(mocks.enqueueComposeDeployment).not.toHaveBeenCalled();
|
||||
expect(mocks.enqueuePreviewDeployment).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
@ -462,18 +456,13 @@ describe("GitHub app webhook preview deployments", () => {
|
||||
pullRequestNumber: 42,
|
||||
}),
|
||||
);
|
||||
expect(mocks.queueAdd).toHaveBeenCalledWith(
|
||||
"deployments",
|
||||
expect(mocks.enqueuePreviewDeployment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
applicationId: "application-id",
|
||||
applicationType: "application-preview",
|
||||
previewDeploymentId: "new-preview-id",
|
||||
type: "deploy",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}),
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { QUEUED_LOG_MESSAGE } from "@dokploy/server";
|
||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
|
||||
@ -21,6 +22,7 @@ interface Props {
|
||||
onClose: () => void;
|
||||
serverId?: string;
|
||||
errorMessage?: string;
|
||||
status?: string;
|
||||
}
|
||||
export const ShowDeployment = ({
|
||||
logPath,
|
||||
@ -28,6 +30,7 @@ export const ShowDeployment = ({
|
||||
onClose,
|
||||
serverId,
|
||||
errorMessage,
|
||||
status,
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||
@ -99,8 +102,31 @@ export const ShowDeployment = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (status === "cancelled" && filteredLogsResult.length > 0) {
|
||||
const isOnlyQueuedLog =
|
||||
filteredLogsResult.some((log) =>
|
||||
log.message.includes(QUEUED_LOG_MESSAGE),
|
||||
) &&
|
||||
!filteredLogsResult.some((log) => log.message.includes("Building on"));
|
||||
if (isOnlyQueuedLog) {
|
||||
filteredLogsResult = [
|
||||
{
|
||||
message: "Deployment was cancelled while queued.",
|
||||
rawTimestamp: null,
|
||||
timestamp: null,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
filteredLogsResult.push({
|
||||
message: "Deployment cancelled.",
|
||||
rawTimestamp: null,
|
||||
timestamp: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setFilteredLogs(filteredLogsResult);
|
||||
}, [data, showExtraLogs]);
|
||||
}, [data, showExtraLogs, status]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
|
||||
@ -133,8 +133,7 @@ export const ShowDeployments = ({
|
||||
const mostRecentDeployment = deployments[0];
|
||||
|
||||
if (
|
||||
!mostRecentDeployment ||
|
||||
mostRecentDeployment.status !== "running" ||
|
||||
mostRecentDeployment?.status !== "running" ||
|
||||
!mostRecentDeployment.startedAt
|
||||
) {
|
||||
return null;
|
||||
@ -165,9 +164,10 @@ export const ShowDeployments = ({
|
||||
{(type === "application" || type === "compose") && (
|
||||
<KillBuild id={id} type={type} />
|
||||
)}
|
||||
{(type === "application" || type === "compose") && (
|
||||
<CancelQueues id={id} type={type} />
|
||||
)}
|
||||
{(type === "application" || type === "compose") &&
|
||||
deployments?.some((d) => d.status === "queued") && (
|
||||
<CancelQueues id={id} type={type} />
|
||||
)}
|
||||
{type === "application" && (
|
||||
<ShowRollbackSettings applicationId={id}>
|
||||
<Button variant="outline">
|
||||
@ -286,7 +286,9 @@ export const ShowDeployments = ({
|
||||
deployment.deploymentId,
|
||||
);
|
||||
const canDelete =
|
||||
deployment.status === "done" || deployment.status === "error";
|
||||
deployment.status === "done" ||
|
||||
deployment.status === "error" ||
|
||||
deployment.status === "cancelled";
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -506,6 +508,7 @@ export const ShowDeployments = ({
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog?.logPath || ""}
|
||||
errorMessage={activeLog?.errorMessage || ""}
|
||||
status={activeLog?.status ?? undefined}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -83,7 +83,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
isLoading={
|
||||
data?.applicationStatus === "running" ||
|
||||
data?.applicationStatus === "queued"
|
||||
}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
@ -165,7 +168,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
isLoading={
|
||||
data?.applicationStatus === "running" ||
|
||||
data?.applicationStatus === "queued"
|
||||
}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
|
||||
@ -61,7 +61,10 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
isLoading={
|
||||
data?.composeStatus === "running" ||
|
||||
data?.composeStatus === "queued"
|
||||
}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
|
||||
@ -39,7 +39,7 @@ export type Services = {
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status?: "idle" | "running" | "done" | "error";
|
||||
status?: "idle" | "queued" | "running" | "done" | "error";
|
||||
};
|
||||
|
||||
interface DuplicateProjectProps {
|
||||
|
||||
@ -52,7 +52,7 @@ export type Services = {
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status?: "idle" | "running" | "done" | "error";
|
||||
status?: "idle" | "queued" | "running" | "done" | "error";
|
||||
};
|
||||
|
||||
export const extractServices = (data: Environment | undefined) => {
|
||||
|
||||
@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
status:
|
||||
| "queued"
|
||||
| "running"
|
||||
| "error"
|
||||
| "done"
|
||||
@ -54,6 +55,11 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
|
||||
/>
|
||||
)}
|
||||
{status === "queued" && (
|
||||
<div
|
||||
className={cn("size-3.5 rounded-full bg-blue-500", className)}
|
||||
/>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="center">
|
||||
<span>
|
||||
@ -62,6 +68,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
{status === "done" && "Done"}
|
||||
{status === "running" && "Running"}
|
||||
{status === "cancelled" && "Cancelled"}
|
||||
{status === "queued" && "Queued"}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
2
apps/dokploy/drizzle/0197_strong_thunderbolt_ross.sql
Normal file
2
apps/dokploy/drizzle/0197_strong_thunderbolt_ross.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TYPE "public"."deploymentStatus" ADD VALUE IF NOT EXISTS 'queued' BEFORE 'running';--> statement-breakpoint
|
||||
ALTER TYPE "public"."applicationStatus" ADD VALUE IF NOT EXISTS 'queued' BEFORE 'running';
|
||||
9172
apps/dokploy/drizzle/meta/0197_snapshot.json
Normal file
9172
apps/dokploy/drizzle/meta/0197_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1380,6 +1380,13 @@
|
||||
"when": 1788995453103,
|
||||
"tag": "0196_worthless_ravenous",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 197,
|
||||
"version": "7",
|
||||
"when": 1789065932792,
|
||||
"tag": "0197_strong_thunderbolt_ross",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -9,7 +9,7 @@ import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { applications } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { enqueueApplicationDeployment } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
|
||||
/**
|
||||
@ -275,14 +275,7 @@ export default async function handler(
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
} else {
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueueApplicationDeployment(jobData);
|
||||
}
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Application:", error);
|
||||
|
||||
@ -4,7 +4,7 @@ import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { compose } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { enqueueComposeDeployment } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import {
|
||||
extractBranchName,
|
||||
@ -198,14 +198,7 @@ export default async function handler(
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
} else {
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueueComposeDeployment(jobData);
|
||||
}
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Compose:", error);
|
||||
|
||||
@ -15,7 +15,11 @@ import { and, eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { applications, compose, github } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import {
|
||||
enqueueApplicationDeployment,
|
||||
enqueueComposeDeployment,
|
||||
enqueuePreviewDeployment,
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import {
|
||||
extractCommitMessage,
|
||||
@ -129,30 +133,30 @@ export default async function handler(
|
||||
});
|
||||
|
||||
for (const app of apps) {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!app.serverId,
|
||||
};
|
||||
try {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!app.serverId,
|
||||
};
|
||||
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await enqueueApplicationDeployment(jobData);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to queue tag deployment for application ${app.applicationId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Find compose apps configured to deploy on tag
|
||||
@ -168,31 +172,30 @@ export default async function handler(
|
||||
});
|
||||
|
||||
for (const composeApp of composeApps) {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeApp.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
server: !!composeApp.serverId,
|
||||
};
|
||||
try {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeApp.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
server: !!composeApp.serverId,
|
||||
};
|
||||
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await enqueueComposeDeployment(jobData);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to queue tag deployment for compose ${composeApp.composeId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const totalApps = apps.length + composeApps.length;
|
||||
@ -242,39 +245,39 @@ export default async function handler(
|
||||
});
|
||||
|
||||
for (const app of apps) {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!app.serverId,
|
||||
};
|
||||
try {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!app.serverId,
|
||||
};
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
app.watchPaths,
|
||||
normalizedCommits,
|
||||
);
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
app.watchPaths,
|
||||
normalizedCommits,
|
||||
);
|
||||
|
||||
if (!shouldDeployPaths) {
|
||||
continue;
|
||||
if (!shouldDeployPaths) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await enqueueApplicationDeployment(jobData);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to queue push deployment for application ${app.applicationId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const composeApps = await db.query.compose.findMany({
|
||||
@ -290,39 +293,38 @@ export default async function handler(
|
||||
});
|
||||
|
||||
for (const composeApp of composeApps) {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeApp.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
server: !!composeApp.serverId,
|
||||
};
|
||||
try {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeApp.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
server: !!composeApp.serverId,
|
||||
};
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
composeApp.watchPaths,
|
||||
normalizedCommits,
|
||||
);
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
composeApp.watchPaths,
|
||||
normalizedCommits,
|
||||
);
|
||||
|
||||
if (!shouldDeployPaths) {
|
||||
continue;
|
||||
if (!shouldDeployPaths) {
|
||||
continue;
|
||||
}
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await enqueueComposeDeployment(jobData);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to queue push deployment for compose ${composeApp.composeId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const totalApps = apps.length + composeApps.length;
|
||||
@ -471,71 +473,68 @@ export default async function handler(
|
||||
}
|
||||
|
||||
for (const app of secureApps) {
|
||||
// check for labels
|
||||
if (app?.previewLabels && app?.previewLabels?.length > 0) {
|
||||
let hasLabel = false;
|
||||
const labels = githubBody?.pull_request?.labels;
|
||||
for (const label of labels) {
|
||||
if (app?.previewLabels?.includes(label.name)) {
|
||||
hasLabel = true;
|
||||
break;
|
||||
try {
|
||||
if (app?.previewLabels && app?.previewLabels?.length > 0) {
|
||||
let hasLabel = false;
|
||||
const labels = githubBody?.pull_request?.labels;
|
||||
for (const label of labels) {
|
||||
if (app?.previewLabels?.includes(label.name)) {
|
||||
hasLabel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasLabel) continue;
|
||||
}
|
||||
if (!hasLabel) continue;
|
||||
}
|
||||
|
||||
const previewDeploymentResult =
|
||||
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
|
||||
const previewDeploymentResult =
|
||||
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
|
||||
|
||||
let previewDeploymentId =
|
||||
previewDeploymentResult?.previewDeploymentId || "";
|
||||
let previewDeploymentId =
|
||||
previewDeploymentResult?.previewDeploymentId || "";
|
||||
|
||||
if (!previewDeploymentResult && shouldCreateDeployment) {
|
||||
// The limit only applies to new previews, existing ones must
|
||||
// still be redeployed when the pull request is updated.
|
||||
const previewLimit = app?.previewLimit ?? 3;
|
||||
if ((app?.previewDeployments?.length ?? 0) >= previewLimit) {
|
||||
console.warn(
|
||||
`⚠️ Preview deployment limit (${previewLimit}) reached for ${app.name}, skipping preview for pull request #${prNumber}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const previewDeployment = await createPreviewDeployment({
|
||||
applicationId: app.applicationId as string,
|
||||
branch: prBranch,
|
||||
pullRequestId: prId,
|
||||
pullRequestNumber: prNumber,
|
||||
pullRequestTitle: prTitle,
|
||||
pullRequestURL: prURL,
|
||||
});
|
||||
previewDeploymentId = previewDeployment.previewDeploymentId;
|
||||
}
|
||||
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: "Preview Deployment",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application-preview",
|
||||
server: !!app.serverId,
|
||||
previewDeploymentId,
|
||||
};
|
||||
|
||||
if (previewDeploymentId) {
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
if (!previewDeploymentResult && shouldCreateDeployment) {
|
||||
const previewLimit = app?.previewLimit ?? 3;
|
||||
if ((app?.previewDeployments?.length ?? 0) >= previewLimit) {
|
||||
console.warn(
|
||||
`⚠️ Preview deployment limit (${previewLimit}) reached for ${app.name}, skipping preview for pull request #${prNumber}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const previewDeployment = await createPreviewDeployment({
|
||||
applicationId: app.applicationId as string,
|
||||
branch: prBranch,
|
||||
pullRequestId: prId,
|
||||
pullRequestNumber: prNumber,
|
||||
pullRequestTitle: prTitle,
|
||||
pullRequestURL: prURL,
|
||||
});
|
||||
continue;
|
||||
previewDeploymentId = previewDeployment.previewDeploymentId;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: "Preview Deployment",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application-preview",
|
||||
server: !!app.serverId,
|
||||
previewDeploymentId,
|
||||
};
|
||||
|
||||
if (previewDeploymentId) {
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await enqueuePreviewDeployment(jobData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to queue preview deployment for application ${app.applicationId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ export type Services = {
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status?: "idle" | "running" | "done" | "error";
|
||||
status?: "idle" | "running" | "done" | "error" | "queued";
|
||||
lastDeployDate?: Date | null;
|
||||
icon?: string | null;
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
cancelAllQueuedDeploymentsByApplicationId,
|
||||
clearOldDeployments,
|
||||
createApplication,
|
||||
createDomain,
|
||||
@ -69,14 +70,15 @@ import {
|
||||
apiSaveGitProvider,
|
||||
apiUpdateApplication,
|
||||
applications,
|
||||
deployments,
|
||||
environments,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import {
|
||||
cleanQueuesByApplication,
|
||||
enqueueApplicationDeployment,
|
||||
killDockerBuild,
|
||||
myQueue,
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
|
||||
@ -232,11 +234,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
} else {
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{ removeOnComplete: true, removeOnFail: true },
|
||||
);
|
||||
await enqueueApplicationDeployment(jobData);
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
@ -472,14 +470,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueueApplicationDeployment(jobData);
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "application",
|
||||
@ -839,14 +830,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueueApplicationDeployment(jobData);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "application",
|
||||
@ -861,7 +845,17 @@ export const applicationRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
await cancelAllQueuedDeploymentsByApplicationId(input.applicationId);
|
||||
await cleanQueuesByApplication(input.applicationId);
|
||||
const hasRunning = await db.query.deployments.findFirst({
|
||||
where: and(
|
||||
eq(deployments.applicationId, input.applicationId),
|
||||
eq(deployments.status, "running"),
|
||||
),
|
||||
});
|
||||
if (!hasRunning) {
|
||||
await updateApplicationStatus(input.applicationId, "idle");
|
||||
}
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
@ -953,14 +947,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}
|
||||
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueueApplicationDeployment(jobData);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "application",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
addDomainToCompose,
|
||||
cancelAllQueuedDeploymentsByComposeId,
|
||||
clearOldDeployments,
|
||||
cloneCompose,
|
||||
createCommand,
|
||||
@ -68,14 +69,15 @@ import {
|
||||
apiSaveEnvironmentVariablesCompose,
|
||||
apiUpdateCompose,
|
||||
compose as composeTable,
|
||||
deployments,
|
||||
environments,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import {
|
||||
cleanQueuesByCompose,
|
||||
enqueueComposeDeployment,
|
||||
killDockerBuild,
|
||||
myQueue,
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
@ -283,7 +285,17 @@ export const composeRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await cancelAllQueuedDeploymentsByComposeId(input.composeId);
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
const hasRunning = await db.query.deployments.findFirst({
|
||||
where: and(
|
||||
eq(deployments.composeId, input.composeId),
|
||||
eq(deployments.status, "running"),
|
||||
),
|
||||
});
|
||||
if (!hasRunning) {
|
||||
await updateCompose(input.composeId, { composeStatus: "idle" });
|
||||
}
|
||||
return { success: true, message: "Queues cleaned successfully" };
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
@ -443,14 +455,7 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueueComposeDeployment(jobData);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
@ -492,14 +497,7 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueueComposeDeployment(jobData);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
|
||||
@ -10,7 +10,7 @@ import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { enqueuePreviewDeployment } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
@ -100,14 +100,7 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await enqueuePreviewDeployment(jobData);
|
||||
await audit(ctx, {
|
||||
action: "redeploy",
|
||||
resourceType: "previewDeployment",
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
rebuildPreviewApplication,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updateDeploymentStatus,
|
||||
updatePreviewDeployment,
|
||||
} from "@dokploy/server";
|
||||
import type { InMemoryJob } from "./in-memory-queue";
|
||||
@ -18,31 +19,38 @@ import type { InMemoryJob } from "./in-memory-queue";
|
||||
export const processDeploymentJob = async (job: InMemoryJob) => {
|
||||
try {
|
||||
if (job.data.applicationType === "application") {
|
||||
await updateApplicationStatus(job.data.applicationId, "running");
|
||||
if (!job.data.deploymentId) {
|
||||
await updateApplicationStatus(job.data.applicationId, "running");
|
||||
}
|
||||
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
deploymentId: job.data.deploymentId,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
deploymentId: job.data.deploymentId,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "compose") {
|
||||
await updateCompose(job.data.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
if (!job.data.deploymentId) {
|
||||
await updateCompose(job.data.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
}
|
||||
if (job.data.type === "deploy") {
|
||||
await deployCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
freshVolumes: job.data.freshVolumes,
|
||||
deploymentId: job.data.deploymentId,
|
||||
});
|
||||
} else if (job.data.type === "redeploy") {
|
||||
await rebuildCompose({
|
||||
@ -50,12 +58,15 @@ export const processDeploymentJob = async (job: InMemoryJob) => {
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
freshVolumes: job.data.freshVolumes,
|
||||
deploymentId: job.data.deploymentId,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "application-preview") {
|
||||
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
||||
previewStatus: "running",
|
||||
});
|
||||
if (!job.data.deploymentId) {
|
||||
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
||||
previewStatus: "running",
|
||||
});
|
||||
}
|
||||
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildPreviewApplication({
|
||||
@ -63,6 +74,7 @@ export const processDeploymentJob = async (job: InMemoryJob) => {
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
deploymentId: job.data.deploymentId,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployPreviewApplication({
|
||||
@ -70,10 +82,27 @@ export const processDeploymentJob = async (job: InMemoryJob) => {
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
deploymentId: job.data.deploymentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
console.error("Deployment job failed", error);
|
||||
try {
|
||||
if (job.data.deploymentId) {
|
||||
await updateDeploymentStatus(job.data.deploymentId, "error");
|
||||
}
|
||||
if (job.data.applicationType === "application") {
|
||||
await updateApplicationStatus(job.data.applicationId, "error");
|
||||
} else if (job.data.applicationType === "compose") {
|
||||
await updateCompose(job.data.composeId, { composeStatus: "error" });
|
||||
} else if (job.data.applicationType === "application-preview") {
|
||||
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error("Failed to update status after job failure", cleanupError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -49,6 +49,9 @@ export const getGroup = (data: DeploymentJob): string => {
|
||||
if (data.applicationType === "compose") {
|
||||
return `compose:${data.composeId}`;
|
||||
}
|
||||
if (data.applicationType === "application-preview") {
|
||||
return `preview:${data.previewDeploymentId}`;
|
||||
}
|
||||
return `application:${data.applicationId}`;
|
||||
};
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ type DeployJob =
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application";
|
||||
serverId?: string;
|
||||
deploymentId?: string;
|
||||
}
|
||||
| {
|
||||
composeId: string;
|
||||
@ -17,6 +18,7 @@ type DeployJob =
|
||||
applicationType: "compose";
|
||||
serverId?: string;
|
||||
freshVolumes?: boolean;
|
||||
deploymentId?: string;
|
||||
}
|
||||
| {
|
||||
applicationId: string;
|
||||
@ -27,6 +29,7 @@ type DeployJob =
|
||||
applicationType: "application-preview";
|
||||
previewDeploymentId: string;
|
||||
serverId?: string;
|
||||
deploymentId?: string;
|
||||
};
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
import {
|
||||
createDeployment,
|
||||
createDeploymentCompose,
|
||||
createDeploymentPreview,
|
||||
IS_CLOUD,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updateDeploymentStatus,
|
||||
updatePreviewDeployment,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
@ -79,13 +88,19 @@ export const startDeploymentWorker = () => myQueue.run();
|
||||
export const getJobsByApplicationId = async (applicationId: string) => {
|
||||
const jobs = await myQueue.getJobs();
|
||||
return jobs.filter(
|
||||
(job) => (job.data as any)?.applicationId === applicationId,
|
||||
(job) =>
|
||||
job.data.applicationType === "application" &&
|
||||
job.data.applicationId === applicationId,
|
||||
);
|
||||
};
|
||||
|
||||
export const getJobsByComposeId = async (composeId: string) => {
|
||||
const jobs = await myQueue.getJobs();
|
||||
return jobs.filter((job) => (job.data as any)?.composeId === composeId);
|
||||
return jobs.filter(
|
||||
(job) =>
|
||||
job.data.applicationType === "compose" &&
|
||||
job.data.composeId === composeId,
|
||||
);
|
||||
};
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
@ -97,7 +112,9 @@ if (!IS_CLOUD) {
|
||||
|
||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||
const removed = myQueue.removeWaiting(
|
||||
(data) => (data as any)?.applicationId === applicationId,
|
||||
(data) =>
|
||||
data.applicationType === "application" &&
|
||||
data.applicationId === applicationId,
|
||||
);
|
||||
if (removed > 0) {
|
||||
console.log(
|
||||
@ -108,7 +125,8 @@ export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||
|
||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||
const removed = myQueue.removeWaiting(
|
||||
(data) => (data as any)?.composeId === composeId,
|
||||
(data) =>
|
||||
data.applicationType === "compose" && data.composeId === composeId,
|
||||
);
|
||||
if (removed > 0) {
|
||||
console.log(`Removed ${removed} waiting job(s) for compose ${composeId}`);
|
||||
@ -147,4 +165,81 @@ export const killDockerBuild = async (
|
||||
}
|
||||
};
|
||||
|
||||
const enqueueDeployment = async (
|
||||
jobData: DeploymentJob,
|
||||
createRecord: () => Promise<{ deploymentId: string }>,
|
||||
resetStatus: () => Promise<unknown>,
|
||||
) => {
|
||||
try {
|
||||
const deployment = await createRecord();
|
||||
jobData.deploymentId = deployment.deploymentId;
|
||||
return myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{ removeOnComplete: true, removeOnFail: true },
|
||||
);
|
||||
} catch (error) {
|
||||
if (jobData.deploymentId) {
|
||||
await updateDeploymentStatus(jobData.deploymentId, "cancelled").catch(
|
||||
console.error,
|
||||
);
|
||||
}
|
||||
await resetStatus().catch(console.error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const enqueueApplicationDeployment = async (jobData: DeploymentJob) => {
|
||||
if (jobData.applicationType !== "application") return;
|
||||
await updateApplicationStatus(jobData.applicationId, "queued");
|
||||
return enqueueDeployment(
|
||||
jobData,
|
||||
() =>
|
||||
createDeployment({
|
||||
applicationId: jobData.applicationId,
|
||||
title: jobData.titleLog,
|
||||
description: jobData.descriptionLog,
|
||||
status: "queued",
|
||||
}),
|
||||
() => updateApplicationStatus(jobData.applicationId, "idle"),
|
||||
);
|
||||
};
|
||||
|
||||
export const enqueueComposeDeployment = async (jobData: DeploymentJob) => {
|
||||
if (jobData.applicationType !== "compose") return;
|
||||
await updateCompose(jobData.composeId, { composeStatus: "queued" });
|
||||
return enqueueDeployment(
|
||||
jobData,
|
||||
() =>
|
||||
createDeploymentCompose({
|
||||
composeId: jobData.composeId,
|
||||
title: jobData.titleLog,
|
||||
description: jobData.descriptionLog,
|
||||
status: "queued",
|
||||
}),
|
||||
() => updateCompose(jobData.composeId, { composeStatus: "idle" }),
|
||||
);
|
||||
};
|
||||
|
||||
export const enqueuePreviewDeployment = async (jobData: DeploymentJob) => {
|
||||
if (jobData.applicationType !== "application-preview") return;
|
||||
await updatePreviewDeployment(jobData.previewDeploymentId, {
|
||||
previewStatus: "queued",
|
||||
});
|
||||
return enqueueDeployment(
|
||||
jobData,
|
||||
() =>
|
||||
createDeploymentPreview({
|
||||
previewDeploymentId: jobData.previewDeploymentId,
|
||||
title: jobData.titleLog,
|
||||
description: jobData.descriptionLog,
|
||||
status: "queued",
|
||||
}),
|
||||
() =>
|
||||
updatePreviewDeployment(jobData.previewDeploymentId, {
|
||||
previewStatus: "idle",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export { myQueue };
|
||||
|
||||
@ -343,7 +343,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
||||
.optional(),
|
||||
triggerType: z.enum(["push", "tag"]).optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
applicationStatus: z.enum(["idle", "queued", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
"heroku_buildpacks",
|
||||
|
||||
@ -193,7 +193,9 @@ const createSchema = createInsertSchema(compose, {
|
||||
.enum(["git", "github", "gitlab", "bitbucket", "gitea", "raw"])
|
||||
.optional(),
|
||||
triggerType: z.enum(["push", "tag"]).optional(),
|
||||
composeStatus: z.enum(["idle", "running", "done", "error"]).optional(),
|
||||
composeStatus: z
|
||||
.enum(["idle", "queued", "running", "done", "error"])
|
||||
.optional(),
|
||||
icon: z
|
||||
.string()
|
||||
.max(2 * 1024 * 1024, "Icon must be less than 2MB")
|
||||
|
||||
@ -18,6 +18,7 @@ import { schedules } from "./schedule";
|
||||
import { server } from "./server";
|
||||
import { volumeBackups } from "./volume-backups";
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"queued",
|
||||
"running",
|
||||
"done",
|
||||
"error",
|
||||
@ -118,7 +119,7 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
|
||||
const schema = createInsertSchema(deployments, {
|
||||
title: z.string().min(1),
|
||||
status: z.string().default("running"),
|
||||
status: z.enum(deploymentStatus.enumValues).default("running"),
|
||||
logPath: z.string().min(1),
|
||||
applicationId: z.string(),
|
||||
composeId: z.string(),
|
||||
|
||||
@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
|
||||
export const applicationStatus = pgEnum("applicationStatus", [
|
||||
"idle",
|
||||
"queued",
|
||||
"running",
|
||||
"done",
|
||||
"error",
|
||||
|
||||
@ -35,6 +35,7 @@ import { getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeployment,
|
||||
createDeploymentPreview,
|
||||
resolveQueuedDeployment,
|
||||
updateDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
@ -179,10 +180,12 @@ export const deployApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
deploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
deploymentId?: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
@ -192,11 +195,18 @@ export const deployApplication = async ({
|
||||
};
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
const deployment = await resolveQueuedDeployment(
|
||||
deploymentId,
|
||||
() =>
|
||||
createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
}),
|
||||
() => updateApplicationStatus(applicationId, "running"),
|
||||
() => updateApplicationStatus(applicationId, "idle"),
|
||||
);
|
||||
if (!deployment) return;
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
@ -297,20 +307,29 @@ export const rebuildApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
deploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
deploymentId?: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
const deployment = await resolveQueuedDeployment(
|
||||
deploymentId,
|
||||
() =>
|
||||
createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
}),
|
||||
() => updateApplicationStatus(applicationId, "running"),
|
||||
() => updateApplicationStatus(applicationId, "idle"),
|
||||
);
|
||||
if (!deployment) return;
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
@ -364,19 +383,34 @@ export const deployPreviewApplication = async ({
|
||||
titleLog = "Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
deploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
deploymentId?: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
const deployment = await resolveQueuedDeployment(
|
||||
deploymentId,
|
||||
() =>
|
||||
createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
}),
|
||||
() =>
|
||||
updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "running",
|
||||
}),
|
||||
() =>
|
||||
updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "idle",
|
||||
}),
|
||||
);
|
||||
if (!deployment) return;
|
||||
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
@ -484,21 +518,36 @@ export const rebuildPreviewApplication = async ({
|
||||
titleLog = "Rebuild Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
deploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
deploymentId?: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
const deployment = await resolveQueuedDeployment(
|
||||
deploymentId,
|
||||
() =>
|
||||
createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
}),
|
||||
() =>
|
||||
updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "running",
|
||||
}),
|
||||
() =>
|
||||
updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "idle",
|
||||
}),
|
||||
);
|
||||
if (!deployment) return;
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
|
||||
@ -39,6 +39,7 @@ import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeploymentCompose,
|
||||
resolveQueuedDeployment,
|
||||
updateDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
@ -229,11 +230,13 @@ export const deployCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
deploymentId,
|
||||
freshVolumes = false,
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
deploymentId?: string;
|
||||
freshVolumes?: boolean;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
@ -241,11 +244,18 @@ export const deployCompose = async ({
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.environment.projectId
|
||||
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
const deployment = await resolveQueuedDeployment(
|
||||
deploymentId,
|
||||
() =>
|
||||
createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
}),
|
||||
() => updateCompose(composeId, { composeStatus: "running" }),
|
||||
() => updateCompose(composeId, { composeStatus: "idle" }),
|
||||
);
|
||||
if (!deployment) return;
|
||||
|
||||
try {
|
||||
const entity = {
|
||||
@ -371,20 +381,29 @@ export const rebuildCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
deploymentId,
|
||||
freshVolumes = false,
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
deploymentId?: string;
|
||||
freshVolumes?: boolean;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
const deployment = await resolveQueuedDeployment(
|
||||
deploymentId,
|
||||
() =>
|
||||
createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
}),
|
||||
() => updateCompose(composeId, { composeStatus: "running" }),
|
||||
() => updateCompose(composeId, { composeStatus: "idle" }),
|
||||
);
|
||||
if (!deployment) return;
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
|
||||
@ -120,10 +120,36 @@ export const findDeploymentByApplicationId = async (applicationId: string) => {
|
||||
return deployment;
|
||||
};
|
||||
|
||||
export const QUEUED_LOG_MESSAGE = "Waiting for worker to pick job...";
|
||||
|
||||
export const resolveQueuedDeployment = async <
|
||||
T extends { deploymentId: string },
|
||||
>(
|
||||
deploymentId: string | undefined,
|
||||
createNew: () => Promise<T>,
|
||||
setRunning: () => Promise<unknown>,
|
||||
resetStatus: () => Promise<unknown>,
|
||||
): Promise<T | null> => {
|
||||
if (!deploymentId) {
|
||||
return createNew();
|
||||
}
|
||||
try {
|
||||
const deployment = await findDeploymentById(deploymentId);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "running");
|
||||
await setRunning();
|
||||
return deployment as unknown as T;
|
||||
} catch (error) {
|
||||
console.error(`Deployment ${deploymentId} lookup failed, skipping`, error);
|
||||
await updateDeploymentStatus(deploymentId, "error").catch(console.error);
|
||||
await resetStatus().catch(console.error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeployment = async (
|
||||
deployment: Omit<
|
||||
z.infer<typeof apiCreateDeployment>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
"deploymentId" | "createdAt" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
@ -136,16 +162,21 @@ export const createDeployment = async (
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss.SSS");
|
||||
const fileName = `${application.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
|
||||
|
||||
const initialLog =
|
||||
deployment.status === "queued"
|
||||
? QUEUED_LOG_MESSAGE
|
||||
: "Initializing deployment";
|
||||
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${application.appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
echo '${initialLog}' >> ${logFilePath};
|
||||
echo "Building on ${serverId ? "Build Server" : "Dokploy Server"}" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
@ -154,7 +185,7 @@ export const createDeployment = async (
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||
await fsPromises.writeFile(logFilePath, `${initialLog}\n`);
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
@ -162,10 +193,11 @@ export const createDeployment = async (
|
||||
.values({
|
||||
applicationId: deployment.applicationId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
status: deployment.status || "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
startedAt:
|
||||
deployment.status === "queued" ? undefined : new Date().toISOString(),
|
||||
...(application.buildServerId && {
|
||||
buildServerId: application.buildServerId,
|
||||
}),
|
||||
@ -204,7 +236,7 @@ export const createDeployment = async (
|
||||
export const createDeploymentPreview = async (
|
||||
deployment: Omit<
|
||||
z.infer<typeof apiCreateDeploymentPreview>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
"deploymentId" | "createdAt" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
@ -218,10 +250,15 @@ export const createDeploymentPreview = async (
|
||||
try {
|
||||
const appName = `${previewDeployment.appName}`;
|
||||
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss.SSS");
|
||||
const fileName = `${appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, appName, fileName);
|
||||
|
||||
const initialLog =
|
||||
deployment.status === "queued"
|
||||
? QUEUED_LOG_MESSAGE
|
||||
: "Initializing deployment";
|
||||
|
||||
if (previewDeployment?.application?.serverId) {
|
||||
const server = await findServerById(
|
||||
previewDeployment?.application?.serverId,
|
||||
@ -229,7 +266,7 @@ export const createDeploymentPreview = async (
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
echo '${initialLog}' >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
@ -237,18 +274,19 @@ export const createDeploymentPreview = async (
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
await fsPromises.writeFile(logFilePath, `${initialLog}\n`);
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
status: deployment.status || "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
previewDeploymentId: deployment.previewDeploymentId,
|
||||
startedAt: new Date().toISOString(),
|
||||
startedAt:
|
||||
deployment.status === "queued" ? undefined : new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
@ -286,7 +324,7 @@ export const createDeploymentPreview = async (
|
||||
export const createDeploymentCompose = async (
|
||||
deployment: Omit<
|
||||
z.infer<typeof apiCreateDeploymentCompose>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
"deploymentId" | "createdAt" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const compose = await findComposeById(deployment.composeId);
|
||||
@ -297,16 +335,21 @@ export const createDeploymentCompose = async (
|
||||
);
|
||||
try {
|
||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss.SSS");
|
||||
const fileName = `${compose.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, compose.appName, fileName);
|
||||
|
||||
const initialLog =
|
||||
deployment.status === "queued"
|
||||
? QUEUED_LOG_MESSAGE
|
||||
: "Initializing deployment";
|
||||
|
||||
if (compose.serverId) {
|
||||
const server = await findServerById(compose.serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${compose.appName};
|
||||
echo "Initializing deployment\n" >> ${logFilePath};
|
||||
echo '${initialLog}' >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
@ -314,7 +357,7 @@ echo "Initializing deployment\n" >> ${logFilePath};
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||
await fsPromises.writeFile(logFilePath, `${initialLog}\n`);
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
@ -323,9 +366,10 @@ echo "Initializing deployment\n" >> ${logFilePath};
|
||||
composeId: deployment.composeId,
|
||||
title: deployment.title || "Deployment",
|
||||
description: deployment.description || "",
|
||||
status: "running",
|
||||
status: deployment.status || "running",
|
||||
logPath: logFilePath,
|
||||
startedAt: new Date().toISOString(),
|
||||
startedAt:
|
||||
deployment.status === "queued" ? undefined : new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
@ -381,7 +425,7 @@ export const createDeploymentBackup = async (
|
||||
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
|
||||
try {
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss.SSS");
|
||||
const fileName = `${backup.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, backup.appName, fileName);
|
||||
|
||||
@ -468,7 +512,7 @@ export const createDeploymentSchedule = async (
|
||||
);
|
||||
try {
|
||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss.SSS");
|
||||
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(SCHEDULES_PATH, schedule.appName, fileName);
|
||||
|
||||
@ -546,7 +590,7 @@ export const createDeploymentVolumeBackup = async (
|
||||
);
|
||||
try {
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss.SSS");
|
||||
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(
|
||||
VOLUME_BACKUPS_PATH,
|
||||
@ -965,8 +1009,13 @@ export const updateDeploymentStatus = async (
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: deploymentStatus,
|
||||
...(deploymentStatus === "running" && {
|
||||
startedAt: new Date().toISOString(),
|
||||
}),
|
||||
finishedAt:
|
||||
deploymentStatus === "done" || deploymentStatus === "error"
|
||||
deploymentStatus === "done" ||
|
||||
deploymentStatus === "error" ||
|
||||
deploymentStatus === "cancelled"
|
||||
? new Date().toISOString()
|
||||
: null,
|
||||
})
|
||||
@ -976,6 +1025,42 @@ export const updateDeploymentStatus = async (
|
||||
return application;
|
||||
};
|
||||
|
||||
export const cancelAllQueuedDeploymentsByApplicationId = async (
|
||||
applicationId: string,
|
||||
) => {
|
||||
return db
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: "cancelled",
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(deployments.applicationId, applicationId),
|
||||
eq(deployments.status, "queued"),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
};
|
||||
|
||||
export const cancelAllQueuedDeploymentsByComposeId = async (
|
||||
composeId: string,
|
||||
) => {
|
||||
return db
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: "cancelled",
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(deployments.composeId, composeId),
|
||||
eq(deployments.status, "queued"),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
};
|
||||
|
||||
export const createServerDeployment = async (
|
||||
deployment: Omit<
|
||||
z.infer<typeof apiCreateDeploymentServer>,
|
||||
@ -987,7 +1072,7 @@ export const createServerDeployment = async (
|
||||
|
||||
const server = await findServerById(deployment.serverId);
|
||||
await removeLastFiveDeployments(deployment.serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss.SSS");
|
||||
const fileName = `${server.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, server.appName, fileName);
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, server.appName), {
|
||||
|
||||
@ -2,9 +2,10 @@ import {
|
||||
applications,
|
||||
compose,
|
||||
deployments,
|
||||
previewDeployments,
|
||||
schedules,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { eq, inArray, or } from "drizzle-orm";
|
||||
import { db } from "../../db/index";
|
||||
|
||||
export const initCancelDeployments = async () => {
|
||||
@ -14,12 +15,18 @@ export const initCancelDeployments = async () => {
|
||||
const runningDeployments = await db
|
||||
.select({
|
||||
deploymentId: deployments.deploymentId,
|
||||
previewDeploymentId: deployments.previewDeploymentId,
|
||||
scheduleId: deployments.scheduleId,
|
||||
scheduleType: schedules.scheduleType,
|
||||
})
|
||||
.from(deployments)
|
||||
.leftJoin(schedules, eq(deployments.scheduleId, schedules.scheduleId))
|
||||
.where(eq(deployments.status, "running"));
|
||||
.where(
|
||||
or(
|
||||
eq(deployments.status, "running"),
|
||||
eq(deployments.status, "queued"),
|
||||
),
|
||||
);
|
||||
|
||||
const deploymentIdsToCancel = runningDeployments
|
||||
.filter(
|
||||
@ -38,6 +45,7 @@ export const initCancelDeployments = async () => {
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: "cancelled",
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(inArray(deployments.deploymentId, deploymentIdsToCancel))
|
||||
.returning();
|
||||
@ -71,6 +79,26 @@ export const initCancelDeployments = async () => {
|
||||
.where(inArray(compose.composeId, composeIds));
|
||||
}
|
||||
|
||||
const previewDeploymentIds = [
|
||||
...new Set(
|
||||
result
|
||||
.map((deployment) => deployment.previewDeploymentId)
|
||||
.filter((id): id is string => !!id),
|
||||
),
|
||||
];
|
||||
|
||||
if (previewDeploymentIds.length > 0) {
|
||||
await db
|
||||
.update(previewDeployments)
|
||||
.set({ previewStatus: "idle" })
|
||||
.where(
|
||||
inArray(
|
||||
previewDeployments.previewDeploymentId,
|
||||
previewDeploymentIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Cancelled ${result.length} deployments`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user