Files
dokploy/packages/server/src/services/deployment.ts
Elijah b4e2d274b1 fix: resolve server from parent entity in deployment.readLogs (#4689)
The readLogs endpoint only checked deployment.serverId and
deployment.schedule?.serverId to determine where to read log files.
For application and compose deployments on remote servers, serverId
is not stored on the deployment record — the server is resolved from
the parent entity (application.serverId, compose.serverId).

This caused readLogs to fall through to local execAsync, which would
silently fail (2>/dev/null) because the log file lives on the remote
server, returning empty content.

Fix by loading the compose relation in findDeploymentById and adding
fallback resolution through application?.serverId and compose?.serverId.

Fixes #4687

Co-authored-by: Roo <roo@agent.com>
2026-06-29 11:10:19 -06:00

1053 lines
28 KiB
TypeScript

import { existsSync, promises as fsPromises } from "node:fs";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateDeployment,
type apiCreateDeploymentBackup,
type apiCreateDeploymentCompose,
type apiCreateDeploymentPreview,
type apiCreateDeploymentSchedule,
type apiCreateDeploymentServer,
type apiCreateDeploymentVolumeBackup,
applications,
compose,
deployments,
environments,
projects,
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { and, desc, eq, inArray, or, sql } from "drizzle-orm";
import type { z } from "zod";
import {
type Application,
findApplicationById,
updateApplicationStatus,
} from "./application";
import { findBackupById } from "./backup";
import { type Compose, findComposeById, updateCompose } from "./compose";
import {
findPreviewDeploymentById,
type PreviewDeployment,
updatePreviewDeployment,
} from "./preview-deployment";
import { removeRollbackById } from "./rollbacks";
import { findScheduleById } from "./schedule";
import { findServerById, type Server } from "./server";
import { findVolumeBackupById } from "./volume-backups";
export type ServicePath = { href: string | null; label: string };
export async function resolveServicePath(
orgId: string,
data: Record<string, unknown>,
): Promise<ServicePath> {
try {
const applicationId = data?.applicationId as string | undefined;
const composeId = data?.composeId as string | undefined;
if (applicationId) {
const app = await findApplicationById(applicationId);
if (app.environment.project.organizationId !== orgId) {
return { href: null, label: "Application" };
}
return {
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
label: "Application",
};
}
if (composeId) {
const comp = await findComposeById(composeId);
if (comp.environment.project.organizationId !== orgId) {
return { href: null, label: "Compose" };
}
return {
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
label: "Compose",
};
}
} catch {
// not found or unauthorized
}
return { href: null, label: "—" };
}
export type Deployment = typeof deployments.$inferSelect;
export const findDeploymentById = async (deploymentId: string) => {
const deployment = await db.query.deployments.findFirst({
where: eq(deployments.deploymentId, deploymentId),
with: {
application: true,
compose: true,
schedule: true,
},
});
if (!deployment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Deployment not found",
});
}
return deployment;
};
export const findDeploymentByApplicationId = async (applicationId: string) => {
const deployment = await db.query.deployments.findFirst({
where: eq(deployments.applicationId, applicationId),
});
if (!deployment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Deployment not found",
});
}
return deployment;
};
export const createDeployment = async (
deployment: Omit<
z.infer<typeof apiCreateDeployment>,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const application = await findApplicationById(deployment.applicationId);
await removeLastTenDeployments(
deployment.applicationId,
"application",
application.serverId,
);
try {
const serverId = application.buildServerId || application.serverId;
const { LOGS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${application.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${LOGS_PATH}/${application.appName};
echo "Initializing deployment" >> ${logFilePath};
echo "Building on ${serverId ? "Build Server" : "Dokploy Server"}" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
}
const deploymentCreate = await db
.insert(deployments)
.values({
applicationId: deployment.applicationId,
title: deployment.title || "Deployment",
status: "running",
logPath: logFilePath,
description: deployment.description || "",
startedAt: new Date().toISOString(),
...(application.buildServerId && {
buildServerId: application.buildServerId,
}),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
await db
.insert(deployments)
.values({
applicationId: deployment.applicationId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
await updateApplicationStatus(application.applicationId, "error");
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
};
export const createDeploymentPreview = async (
deployment: Omit<
z.infer<typeof apiCreateDeploymentPreview>,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const previewDeployment = await findPreviewDeploymentById(
deployment.previewDeploymentId,
);
await removeLastTenDeployments(
deployment.previewDeploymentId,
"previewDeployment",
previewDeployment?.application?.serverId,
);
try {
const appName = `${previewDeployment.appName}`;
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, appName, fileName);
if (previewDeployment?.application?.serverId) {
const server = await findServerById(
previewDeployment?.application?.serverId,
);
const command = `
mkdir -p ${LOGS_PATH}/${appName};
echo "Initializing deployment" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(path.join(LOGS_PATH, appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment");
}
const deploymentCreate = await db
.insert(deployments)
.values({
title: deployment.title || "Deployment",
status: "running",
logPath: logFilePath,
description: deployment.description || "",
previewDeploymentId: deployment.previewDeploymentId,
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
await db
.insert(deployments)
.values({
previewDeploymentId: deployment.previewDeploymentId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
await updatePreviewDeployment(deployment.previewDeploymentId, {
previewStatus: "error",
});
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
};
export const createDeploymentCompose = async (
deployment: Omit<
z.infer<typeof apiCreateDeploymentCompose>,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const compose = await findComposeById(deployment.composeId);
await removeLastTenDeployments(
deployment.composeId,
"compose",
compose.serverId,
);
try {
const { LOGS_PATH } = paths(!!compose.serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${compose.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, compose.appName, fileName);
if (compose.serverId) {
const server = await findServerById(compose.serverId);
const command = `
mkdir -p ${LOGS_PATH}/${compose.appName};
echo "Initializing deployment\n" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
}
const deploymentCreate = await db
.insert(deployments)
.values({
composeId: deployment.composeId,
title: deployment.title || "Deployment",
description: deployment.description || "",
status: "running",
logPath: logFilePath,
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
await db
.insert(deployments)
.values({
composeId: deployment.composeId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
await updateCompose(compose.composeId, {
composeStatus: "error",
});
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
};
export const createDeploymentBackup = async (
deployment: Omit<
z.infer<typeof apiCreateDeploymentBackup>,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const backup = await findBackupById(deployment.backupId);
let serverId: string | null | undefined;
if (backup.backupType === "database") {
serverId =
backup.postgres?.serverId ||
backup.mariadb?.serverId ||
backup.mysql?.serverId ||
backup.mongo?.serverId;
} else if (backup.backupType === "compose") {
serverId = backup.compose?.serverId;
}
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
try {
const { LOGS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${backup.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, backup.appName, fileName);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${LOGS_PATH}/${backup.appName};
echo "Initializing backup\n" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(path.join(LOGS_PATH, backup.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing backup\n");
}
const deploymentCreate = await db
.insert(deployments)
.values({
backupId: deployment.backupId,
title: deployment.title || "Backup",
description: deployment.description || "",
status: "running",
logPath: logFilePath,
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the backup",
});
}
return deploymentCreate[0];
} catch (error) {
await db
.insert(deployments)
.values({
backupId: deployment.backupId,
title: deployment.title || "Backup",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the backup",
});
}
};
export const createDeploymentSchedule = async (
deployment: Omit<
z.infer<typeof apiCreateDeploymentSchedule>,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const schedule = await findScheduleById(deployment.scheduleId);
const serverId =
schedule.application?.serverId ||
schedule.compose?.serverId ||
schedule.server?.serverId;
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
try {
const { SCHEDULES_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(SCHEDULES_PATH, schedule.appName, fileName);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${SCHEDULES_PATH}/${schedule.appName};
echo "Initializing schedule" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(path.join(SCHEDULES_PATH, schedule.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing schedule\n");
}
const deploymentCreate = await db
.insert(deployments)
.values({
scheduleId: deployment.scheduleId,
title: deployment.title || "Deployment",
status: "running",
logPath: logFilePath,
description: deployment.description || "",
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
console.log(error);
await db
.insert(deployments)
.values({
scheduleId: deployment.scheduleId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
};
export const createDeploymentVolumeBackup = async (
deployment: Omit<
z.infer<typeof apiCreateDeploymentVolumeBackup>,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId);
const serverId =
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
await removeLastTenDeployments(
deployment.volumeBackupId,
"volumeBackup",
serverId,
);
try {
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(
VOLUME_BACKUPS_PATH,
volumeBackup.appName,
fileName,
);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${VOLUME_BACKUPS_PATH}/${volumeBackup.appName};
echo "Initializing volume backup" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(
path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName),
{
recursive: true,
},
);
await fsPromises.writeFile(logFilePath, "Initializing volume backup\n");
}
const deploymentCreate = await db
.insert(deployments)
.values({
volumeBackupId: deployment.volumeBackupId,
title: deployment.title || "Deployment",
status: "running",
logPath: logFilePath,
description: deployment.description || "",
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
console.log(error);
await db
.insert(deployments)
.values({
volumeBackupId: deployment.volumeBackupId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
};
export const removeDeployment = async (deploymentId: string) => {
try {
const deployment = await db
.delete(deployments)
.where(eq(deployments.deploymentId, deploymentId))
.returning()
.then((result) => result[0]);
if (!deployment) {
return null;
}
const logPath = path.join(deployment.logPath);
if (logPath && logPath !== ".") {
const command = `rm -f ${logPath};`;
if (deployment.serverId) {
await execAsyncRemote(deployment.serverId, command);
} else {
await execAsync(command);
}
}
return deployment;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error removing the deployment";
throw new TRPCError({
code: "BAD_REQUEST",
message,
});
}
};
export const removeDeploymentsByApplicationId = async (
applicationId: string,
) => {
await db
.delete(deployments)
.where(eq(deployments.applicationId, applicationId))
.returning();
};
const getDeploymentsByType = async (
id: string,
type:
| "application"
| "compose"
| "server"
| "schedule"
| "previewDeployment"
| "backup"
| "volumeBackup",
) => {
const deploymentList = await db.query.deployments.findMany({
where: eq(deployments[`${type}Id`], id),
orderBy: desc(deployments.createdAt),
with: {
rollback: true,
},
});
return deploymentList;
};
export const removeDeployments = async (application: Application) => {
const { appName, applicationId } = application;
const { LOGS_PATH } = paths(!!application.serverId);
const logsPath = path.join(LOGS_PATH, appName);
if (application.serverId) {
await execAsyncRemote(application.serverId, `rm -rf ${logsPath}`);
} else {
await removeDirectoryIfExistsContent(logsPath);
}
await removeDeploymentsByApplicationId(applicationId);
};
const removeLastTenDeployments = async (
id: string,
type:
| "application"
| "compose"
| "server"
| "schedule"
| "previewDeployment"
| "backup"
| "volumeBackup",
serverId?: string | null,
) => {
const deploymentList = await getDeploymentsByType(id, type);
if (deploymentList.length > 10) {
const deploymentsToDelete = deploymentList.slice(10);
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
try {
const logPath = path.join(oldDeployment.logPath);
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
if (logPath && logPath !== ".") {
command += `rm -rf ${logPath};`;
}
await removeDeployment(oldDeployment.deploymentId);
} catch (err) {
console.error(
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
err,
);
}
}
if (command) {
await execAsyncRemote(serverId, command);
}
} else {
for (const oldDeployment of deploymentsToDelete) {
try {
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
const logPath = path.join(oldDeployment.logPath);
if (
logPath &&
logPath !== "." &&
existsSync(logPath) &&
!oldDeployment.errorMessage
) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
} catch (err) {
console.error(
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
err,
);
}
}
}
}
};
export const removeDeploymentsByPreviewDeploymentId = async (
previewDeployment: PreviewDeployment,
serverId: string | null,
) => {
const { appName } = previewDeployment;
const { LOGS_PATH } = paths(!!serverId);
const logsPath = path.join(LOGS_PATH, appName);
if (serverId) {
await execAsyncRemote(serverId, `rm -rf ${logsPath}`);
} else {
await removeDirectoryIfExistsContent(logsPath);
}
await db
.delete(deployments)
.where(
eq(
deployments.previewDeploymentId,
previewDeployment.previewDeploymentId,
),
)
.returning();
};
export const removeDeploymentsByComposeId = async (compose: Compose) => {
const { appName } = compose;
const { LOGS_PATH } = paths(!!compose.serverId);
const logsPath = path.join(LOGS_PATH, appName);
if (compose.serverId) {
await execAsyncRemote(compose.serverId, `rm -rf ${logsPath}`);
} else {
await removeDirectoryIfExistsContent(logsPath);
}
await db
.delete(deployments)
.where(eq(deployments.composeId, compose.composeId))
.returning();
};
export const findAllDeploymentsByApplicationId = async (
applicationId: string,
) => {
const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments.applicationId, applicationId),
orderBy: desc(deployments.createdAt),
});
return deploymentsList;
};
export const findAllDeploymentsByComposeId = async (composeId: string) => {
const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments.composeId, composeId),
orderBy: desc(deployments.createdAt),
});
return deploymentsList;
};
const centralizedDeploymentsWith = {
application: {
columns: { applicationId: true, name: true, appName: true },
with: {
environment: {
columns: { environmentId: true, name: true },
with: {
project: {
columns: { projectId: true, name: true },
},
},
},
server: {
columns: { serverId: true, name: true, serverType: true },
},
buildServer: {
columns: { serverId: true, name: true, serverType: true },
},
},
},
compose: {
columns: { composeId: true, name: true, appName: true },
with: {
environment: {
columns: { environmentId: true, name: true },
with: {
project: {
columns: { projectId: true, name: true },
},
},
},
server: {
columns: { serverId: true, name: true, serverType: true },
},
},
},
server: {
columns: { serverId: true, name: true, serverType: true },
},
buildServer: {
columns: { serverId: true, name: true, serverType: true },
},
} as const;
async function getApplicationIdsInOrg(
orgId: string,
accessedServices: string[] | null,
): Promise<string[]> {
const rows = await db
.select({ applicationId: applications.applicationId })
.from(applications)
.innerJoin(
environments,
eq(applications.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(
accessedServices !== null
? and(
eq(projects.organizationId, orgId),
inArray(applications.applicationId, accessedServices),
)
: eq(projects.organizationId, orgId),
);
return rows.map((r) => r.applicationId);
}
async function getComposeIdsInOrg(
orgId: string,
accessedServices: string[] | null,
): Promise<string[]> {
const rows = await db
.select({ composeId: compose.composeId })
.from(compose)
.innerJoin(
environments,
eq(compose.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(
accessedServices !== null
? and(
eq(projects.organizationId, orgId),
inArray(compose.composeId, accessedServices),
)
: eq(projects.organizationId, orgId),
);
return rows.map((r) => r.composeId);
}
/**
* All deployments for applications and compose in the org.
* Pass accessedServices for members (only those services), null for owner/admin.
*/
export const findAllDeploymentsCentralized = async (
orgId: string,
accessedServices: string[] | null,
) => {
if (accessedServices !== null && accessedServices.length === 0) {
return [];
}
const [appIds, compIds] = await Promise.all([
getApplicationIdsInOrg(orgId, accessedServices),
getComposeIdsInOrg(orgId, accessedServices),
]);
if (appIds.length === 0 && compIds.length === 0) {
return [];
}
const conditions = [
...(appIds.length > 0 ? [inArray(deployments.applicationId, appIds)] : []),
...(compIds.length > 0 ? [inArray(deployments.composeId, compIds)] : []),
];
const whereClause =
conditions.length === 0
? sql`1 = 0`
: conditions.length === 1
? conditions[0]
: or(...conditions);
return db.query.deployments.findMany({
where: whereClause,
orderBy: desc(deployments.createdAt),
with: centralizedDeploymentsWith,
});
};
export const updateDeployment = async (
deploymentId: string,
deploymentData: Partial<Deployment>,
) => {
const application = await db
.update(deployments)
.set({
...deploymentData,
})
.where(eq(deployments.deploymentId, deploymentId))
.returning();
return application;
};
export const updateDeploymentStatus = async (
deploymentId: string,
deploymentStatus: Deployment["status"],
) => {
const application = await db
.update(deployments)
.set({
status: deploymentStatus,
finishedAt:
deploymentStatus === "done" || deploymentStatus === "error"
? new Date().toISOString()
: null,
})
.where(eq(deployments.deploymentId, deploymentId))
.returning();
return application;
};
export const createServerDeployment = async (
deployment: Omit<
z.infer<typeof apiCreateDeploymentServer>,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
try {
const { LOGS_PATH } = paths();
const server = await findServerById(deployment.serverId);
await removeLastFiveDeployments(deployment.serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${server.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, server.appName, fileName);
await fsPromises.mkdir(path.join(LOGS_PATH, server.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing Setup Server");
const deploymentCreate = await db
.insert(deployments)
.values({
serverId: server.serverId,
title: deployment.title || "Deployment",
description: deployment.description || "",
status: "running",
logPath: logFilePath,
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
const message =
error instanceof Error ? error.message : "Error creating the deployment";
throw new TRPCError({
code: "BAD_REQUEST",
message,
});
}
};
export const removeLastFiveDeployments = async (serverId: string) => {
const deploymentList = await db.query.deployments.findMany({
where: eq(deployments.serverId, serverId),
orderBy: desc(deployments.createdAt),
});
if (deploymentList.length >= 5) {
const deploymentsToDelete = deploymentList.slice(4);
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
}
}
};
export const removeDeploymentsByServerId = async (server: Server) => {
const { LOGS_PATH } = paths();
const { appName } = server;
const logsPath = path.join(LOGS_PATH, appName);
await removeDirectoryIfExistsContent(logsPath);
await db
.delete(deployments)
.where(eq(deployments.serverId, server.serverId))
.returning();
};
export const findAllDeploymentsByServerId = async (serverId: string) => {
const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments.serverId, serverId),
orderBy: desc(deployments.createdAt),
});
return deploymentsList;
};
export const clearOldDeployments = async (
appName: string,
serverId: string | null,
) => {
const { LOGS_PATH } = paths(!!serverId);
const folder = path.join(LOGS_PATH, appName);
const command = `
rm -rf ${folder};
`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
};