This commit is contained in:
Niels Klumper 2026-09-11 13:11:53 -04:00 committed by GitHub
commit c8c36bcbcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 825 additions and 109 deletions

View File

@ -0,0 +1,332 @@
import { db } from "@dokploy/server/db";
import {
deployPreviewApplication,
rebuildPreviewApplication,
} from "@dokploy/server/services/application";
import * as deploymentService from "@dokploy/server/services/deployment";
import * as githubService from "@dokploy/server/services/github";
import * as previewService from "@dokploy/server/services/preview-deployment";
import * as builders from "@dokploy/server/utils/builders";
import * as execProcess from "@dokploy/server/utils/process/execAsync";
import * as githubProvider from "@dokploy/server/utils/providers/github";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@dokploy/server/db", () => ({
db: {
query: {
applications: {
findFirst: vi.fn(),
},
},
},
}));
vi.mock("@dokploy/server/services/deployment", () => ({
createDeployment: vi.fn(),
createDeploymentPreview: vi.fn(),
updateDeployment: vi.fn(),
updateDeploymentStatus: vi.fn(),
}));
vi.mock("@dokploy/server/services/preview-deployment", () => ({
findPreviewDeploymentById: vi.fn(),
updatePreviewDeployment: vi.fn(),
}));
vi.mock("@dokploy/server/services/github", () => ({
createPreviewDeploymentComment: vi.fn(),
getIssueComment: vi.fn(),
issueCommentExists: vi.fn(),
updateIssueComment: vi.fn(),
}));
vi.mock("@dokploy/server/utils/builders", () => ({
getBuildCommand: vi.fn(),
mechanizeDockerContainer: vi.fn(),
}));
vi.mock("@dokploy/server/utils/process/execAsync", () => ({
execAsync: vi.fn(),
execAsyncRemote: vi.fn(),
ExecError: class ExecError extends Error {},
}));
vi.mock("@dokploy/server/utils/providers/github", () => ({
cloneGithubRepository: vi.fn(),
}));
const createMockApplication = () => ({
applicationId: "application-id",
name: "Test application",
appName: "test-application",
sourceType: "github" as const,
owner: "Dokploy",
repository: "dokploy",
branch: "canary",
githubId: "github-id",
enableSubmodules: false,
serverId: "deployment-server-id",
buildServerId: "build-server-id",
buildRegistry: {
registryId: "build-registry-id",
},
registry: null,
rollbackRegistry: null,
rollbackActive: true,
previewEnv: "",
previewBuildArgs: "",
previewBuildSecrets: "",
environment: {
env: "",
projectId: "project-id",
project: {
env: "",
name: "Test project",
organizationId: "organization-id",
},
},
domains: [],
mounts: [],
ports: [],
security: [],
redirects: [],
});
const createMockPreview = () => ({
previewDeploymentId: "preview-id",
applicationId: "application-id",
appName: "preview-test-application",
branch: "feature/test-preview",
pullRequestNumber: "123",
pullRequestCommentId: "456",
deployments: [
{
buildServerId: "build-server-id",
status: "done",
},
],
domain: {
host: "preview.example.com",
https: true,
},
});
describe("preview deployment build server and registry", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
createMockApplication() as any,
);
vi.mocked(previewService.findPreviewDeploymentById).mockResolvedValue(
createMockPreview() as any,
);
vi.mocked(deploymentService.createDeploymentPreview).mockResolvedValue({
deploymentId: "deployment-id",
logPath: "/tmp/preview.log",
} as any);
vi.mocked(githubService.issueCommentExists).mockResolvedValue(true);
vi.mocked(githubService.getIssueComment).mockReturnValue("comment");
vi.mocked(githubService.updateIssueComment).mockResolvedValue(
undefined as any,
);
vi.mocked(githubProvider.cloneGithubRepository).mockResolvedValue(
"git clone command;",
);
vi.mocked(builders.getBuildCommand).mockResolvedValue(
"docker build and push command;",
);
vi.mocked(builders.mechanizeDockerContainer).mockResolvedValue(
undefined as any,
);
vi.mocked(execProcess.execAsyncRemote).mockResolvedValue({
stdout: "",
stderr: "",
} as any);
});
it("builds a new preview on the configured build server and preserves its registry", async () => {
await deployPreviewApplication({
applicationId: "application-id",
previewDeploymentId: "preview-id",
titleLog: "Preview deployment",
descriptionLog: "",
});
expect(githubProvider.cloneGithubRepository).toHaveBeenCalledWith(
expect.objectContaining({
appName: "preview-test-application",
branch: "feature/test-preview",
serverId: "build-server-id",
}),
);
expect(builders.getBuildCommand).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "build-server-id",
buildServerId: "build-server-id",
buildRegistry: expect.objectContaining({
registryId: "build-registry-id",
}),
}),
);
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
"build-server-id",
expect.stringContaining("docker build and push command"),
);
expect(builders.mechanizeDockerContainer).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "deployment-server-id",
buildRegistry: expect.objectContaining({
registryId: "build-registry-id",
}),
}),
);
});
it("rebuilds a preview on the configured build server and preserves its registry", async () => {
await rebuildPreviewApplication({
applicationId: "application-id",
previewDeploymentId: "preview-id",
titleLog: "Rebuild preview deployment",
descriptionLog: "",
});
expect(githubProvider.cloneGithubRepository).not.toHaveBeenCalled();
expect(builders.getBuildCommand).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "build-server-id",
buildServerId: "build-server-id",
buildRegistry: expect.objectContaining({
registryId: "build-registry-id",
}),
}),
);
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
"build-server-id",
expect.stringContaining("docker build and push command"),
);
expect(builders.mechanizeDockerContainer).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "deployment-server-id",
}),
);
});
it("reclones a preview on the current build server when its build server changed", async () => {
vi.mocked(previewService.findPreviewDeploymentById).mockResolvedValue({
...createMockPreview(),
deployments: [
{
buildServerId: "previous-build-server-id",
status: "done",
},
],
} as any);
await rebuildPreviewApplication({
applicationId: "application-id",
previewDeploymentId: "preview-id",
titleLog: "Rebuild preview deployment",
descriptionLog: "",
});
expect(githubProvider.cloneGithubRepository).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "build-server-id",
appName: "preview-test-application",
branch: "feature/test-preview",
}),
);
expect(builders.getBuildCommand).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "build-server-id",
buildRegistry: expect.objectContaining({
registryId: "build-registry-id",
}),
}),
);
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
"build-server-id",
expect.stringContaining("git clone command;"),
);
});
it("reclones a legacy preview on the configured build server", async () => {
vi.mocked(previewService.findPreviewDeploymentById).mockResolvedValue({
...createMockPreview(),
deployments: [
{
buildServerId: null,
status: "done",
},
],
} as any);
await rebuildPreviewApplication({
applicationId: "application-id",
previewDeploymentId: "preview-id",
titleLog: "Rebuild preview deployment",
descriptionLog: "",
});
expect(githubProvider.cloneGithubRepository).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "build-server-id",
appName: "preview-test-application",
branch: "feature/test-preview",
}),
);
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
"build-server-id",
expect.stringContaining("git clone command;"),
);
});
it("reclones after a failed deployment on the same build server", async () => {
vi.mocked(previewService.findPreviewDeploymentById).mockResolvedValue({
...createMockPreview(),
deployments: [
{
buildServerId: "build-server-id",
status: "error",
},
],
} as any);
await rebuildPreviewApplication({
applicationId: "application-id",
previewDeploymentId: "preview-id",
titleLog: "Rebuild preview deployment",
descriptionLog: "",
});
expect(githubProvider.cloneGithubRepository).toHaveBeenCalledWith(
expect.objectContaining({
serverId: "build-server-id",
appName: "preview-test-application",
branch: "feature/test-preview",
}),
);
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
"build-server-id",
expect.stringContaining("git clone command;"),
);
});
});

View File

@ -0,0 +1,150 @@
import { db } from "@dokploy/server/db";
import * as applicationService from "@dokploy/server/services/application";
import { removePreviewDeployment } from "@dokploy/server/services/preview-deployment";
import * as dockerUtils from "@dokploy/server/utils/docker/utils";
import * as directoryUtils from "@dokploy/server/utils/filesystem/directory";
import * as execProcess from "@dokploy/server/utils/process/execAsync";
import * as traefikApplication from "@dokploy/server/utils/traefik/application";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@dokploy/server/db", () => ({
db: {
query: {
previewDeployments: {
findFirst: vi.fn(),
},
},
delete: vi.fn(() => ({
where: vi.fn(() => ({
returning: vi.fn().mockResolvedValue([]),
})),
})),
},
}));
vi.mock("@dokploy/server/services/application", () => ({
findApplicationById: vi.fn(),
updateApplicationStatus: vi.fn(),
}));
vi.mock("@dokploy/server/utils/docker/utils", () => ({
encodeBase64: vi.fn((value: string) => value),
removeService: vi.fn(),
}));
vi.mock("@dokploy/server/utils/filesystem/directory", () => ({
removeDirectoryCode: vi.fn(),
removeDirectoryIfExistsContent: vi.fn(),
}));
vi.mock("@dokploy/server/utils/process/execAsync", () => ({
execAsync: vi.fn(),
execAsyncRemote: vi.fn(),
ExecError: class ExecError extends Error {},
}));
vi.mock("@dokploy/server/utils/traefik/application", () => ({
removeTraefikConfig: vi.fn(),
}));
describe("preview deployment cleanup", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(db.query.previewDeployments.findFirst).mockResolvedValue({
previewDeploymentId: "preview-id",
applicationId: "application-id",
appName: "preview-test-application",
deployments: [
{
buildServerId: "previous-build-server-id",
},
{
buildServerId: "build-server-id",
},
{
buildServerId: null,
},
],
application: {
applicationId: "application-id",
serverId: "deployment-server-id",
buildServerId: "build-server-id",
},
domain: null,
} as any);
vi.mocked(applicationService.findApplicationById).mockResolvedValue({
applicationId: "application-id",
appName: "application-name",
serverId: "deployment-server-id",
buildServerId: "build-server-id",
} as any);
});
it("removes preview artifacts from every current and historical build host", async () => {
await removePreviewDeployment("preview-id");
expect(execProcess.execAsyncRemote).toHaveBeenCalledTimes(3);
for (const serverId of [
"deployment-server-id",
"build-server-id",
"previous-build-server-id",
]) {
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
serverId,
expect.stringContaining("preview-test-application"),
);
}
expect(directoryUtils.removeDirectoryIfExistsContent).toHaveBeenCalledWith(
expect.stringContaining("preview-test-application"),
);
expect(directoryUtils.removeDirectoryCode).toHaveBeenCalledTimes(4);
for (const serverId of [
null,
"deployment-server-id",
"build-server-id",
"previous-build-server-id",
]) {
expect(directoryUtils.removeDirectoryCode).toHaveBeenCalledWith(
"preview-test-application",
serverId,
);
}
expect(dockerUtils.removeService).toHaveBeenCalledWith(
"preview-test-application",
"deployment-server-id",
);
expect(traefikApplication.removeTraefikConfig).toHaveBeenCalledWith(
"preview-test-application",
"deployment-server-id",
);
});
it("keeps preview records when cleanup on a historical build server fails", async () => {
vi.mocked(directoryUtils.removeDirectoryCode).mockImplementation(
async (_appName, serverId) => {
if (serverId === "previous-build-server-id") {
throw new Error("Historical build server is unavailable");
}
},
);
await expect(removePreviewDeployment("preview-id")).rejects.toThrow(
"Historical build server is unavailable",
);
expect(directoryUtils.removeDirectoryCode).toHaveBeenCalledWith(
"preview-test-application",
"previous-build-server-id",
);
expect(db.delete).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,165 @@
import { db } from "@dokploy/server/db";
import { createDeploymentPreview } from "@dokploy/server/services/deployment";
import * as previewService from "@dokploy/server/services/preview-deployment";
import * as serverService from "@dokploy/server/services/server";
import * as directoryUtils from "@dokploy/server/utils/filesystem/directory";
import * as execProcess from "@dokploy/server/utils/process/execAsync";
import { beforeEach, describe, expect, it, vi } from "vitest";
const dbMocks = vi.hoisted(() => ({
deleteReturning: vi.fn(),
insertReturning: vi.fn(),
}));
vi.mock("@dokploy/server/db", () => ({
db: {
query: {
deployments: {
findMany: vi.fn(),
findFirst: vi.fn(),
},
},
delete: vi.fn(() => ({
where: vi.fn(() => ({
returning: dbMocks.deleteReturning,
})),
})),
insert: vi.fn(() => ({
values: vi.fn(() => ({
returning: dbMocks.insertReturning,
})),
})),
},
}));
vi.mock("@dokploy/server/services/application", () => ({
findApplicationById: vi.fn(),
updateApplicationStatus: vi.fn(),
}));
vi.mock("@dokploy/server/services/preview-deployment", () => ({
findPreviewDeploymentById: vi.fn(),
updatePreviewDeployment: vi.fn(),
}));
vi.mock("@dokploy/server/services/server", () => ({
findServerById: vi.fn(),
}));
vi.mock("@dokploy/server/utils/filesystem/directory", () => ({
removeDirectoryCode: vi.fn(),
removeDirectoryIfExistsContent: vi.fn(),
}));
vi.mock("@dokploy/server/utils/process/execAsync", () => ({
execAsync: vi.fn(),
execAsyncRemote: vi.fn(),
ExecError: class ExecError extends Error {},
}));
describe("preview deployment pruning", () => {
beforeEach(() => {
vi.clearAllMocks();
const retainedDeployments = Array.from({ length: 10 }, (_, index) => ({
deploymentId: `retained-deployment-${index}`,
buildServerId: "build-server-id",
serverId: null,
logPath: `/tmp/retained-deployment-${index}.log`,
rollbackId: null,
}));
const prunedDeployment = {
deploymentId: "pruned-deployment-id",
buildServerId: "previous-build-server-id",
serverId: null,
logPath: "/tmp/pruned-deployment.log",
rollbackId: null,
};
vi.mocked(db.query.deployments.findFirst).mockResolvedValue(
prunedDeployment as any,
);
vi.mocked(db.query.deployments.findMany).mockResolvedValue([
...retainedDeployments,
prunedDeployment,
] as any);
vi.mocked(previewService.findPreviewDeploymentById).mockResolvedValue({
previewDeploymentId: "preview-id",
appName: "preview-test-application",
application: {
applicationId: "application-id",
serverId: "deployment-server-id",
buildServerId: "build-server-id",
},
} as any);
vi.mocked(serverService.findServerById).mockResolvedValue({
serverId: "build-server-id",
} as any);
dbMocks.deleteReturning.mockResolvedValue([prunedDeployment]);
dbMocks.insertReturning.mockResolvedValue([
{
deploymentId: "new-deployment-id",
logPath: "/tmp/new-deployment.log",
},
]);
vi.mocked(execProcess.execAsyncRemote).mockResolvedValue({
stdout: "",
stderr: "",
} as any);
});
it("removes logs and source when the last deployment for a historical build server is pruned", async () => {
await createDeploymentPreview({
title: "Preview deployment",
description: "",
previewDeploymentId: "preview-id",
});
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
"previous-build-server-id",
"rm -f /tmp/pruned-deployment.log;",
);
expect(directoryUtils.removeDirectoryCode).toHaveBeenCalledWith(
"preview-test-application",
"previous-build-server-id",
);
expect(directoryUtils.removeDirectoryCode).not.toHaveBeenCalledWith(
"preview-test-application",
"build-server-id",
);
expect(execProcess.execAsyncRemote).toHaveBeenCalledWith(
"build-server-id",
expect.stringContaining("Initializing deployment"),
);
});
it("keeps historical deployment records when source cleanup fails", async () => {
vi.mocked(directoryUtils.removeDirectoryCode).mockRejectedValueOnce(
new Error("Historical build server is unavailable"),
);
await createDeploymentPreview({
title: "Preview deployment",
description: "",
previewDeploymentId: "preview-id",
});
expect(directoryUtils.removeDirectoryCode).toHaveBeenCalledWith(
"preview-test-application",
"previous-build-server-id",
);
expect(db.query.deployments.findFirst).not.toHaveBeenCalled();
expect(db.delete).not.toHaveBeenCalled();
});
});

View File

@ -429,22 +429,27 @@ export const deployPreviewApplication = async ({
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;
const buildServerId = application.buildServerId || application.serverId;
const buildApplication = {
...application,
serverId: buildServerId,
};
let command = "set -e;";
if (application.sourceType === "github") {
command += await cloneGithubRepository({
...application,
...buildApplication,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
});
command += await getBuildCommand(application);
command += await getBuildCommand(buildApplication);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, commandWithLog);
if (buildServerId) {
await execAsyncRemote(buildServerId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
@ -494,6 +499,19 @@ export const rebuildPreviewApplication = async ({
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
const previousDeployment = previewDeployment.deployments[0];
const previousBuildServerId = previousDeployment
? previousDeployment.buildServerId || application.serverId
: null;
const buildServerId = application.buildServerId || application.serverId;
const shouldCloneRepository =
!previousDeployment ||
previousDeployment.status !== "done" ||
previousBuildServerId !== buildServerId;
const deployment = await createDeploymentPreview({
title: titleLog,
description: descriptionLog,
@ -548,17 +566,27 @@ export const rebuildPreviewApplication = async ({
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;
const serverId = application.serverId;
const buildApplication = {
...application,
serverId: buildServerId,
};
let command = "set -e;";
// Only rebuild, don't clone repository
command += await getBuildCommand(application);
if (shouldCloneRepository && application.sourceType === "github") {
command += await cloneGithubRepository({
...buildApplication,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
});
}
command += await getBuildCommand(buildApplication);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
if (buildServerId) {
await execAsyncRemote(buildServerId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
@ -588,9 +616,8 @@ export const rebuildPreviewApplication = async ({
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
const serverId = application.buildServerId || application.serverId;
if (serverId) {
await execAsyncRemote(serverId, command);
if (buildServerId) {
await execAsyncRemote(buildServerId, command);
} else {
await execAsync(command);
}

View File

@ -16,7 +16,10 @@ import {
environments,
projects,
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import {
removeDirectoryCode,
removeDirectoryIfExistsContent,
} from "@dokploy/server/utils/filesystem/directory";
import {
execAsync,
execAsyncRemote,
@ -210,26 +213,30 @@ export const createDeploymentPreview = async (
const previewDeployment = await findPreviewDeploymentById(
deployment.previewDeploymentId,
);
const buildServerId = previewDeployment.application?.buildServerId;
const serverId = buildServerId || previewDeployment.application?.serverId;
await removeLastTenDeployments(
deployment.previewDeploymentId,
"previewDeployment",
previewDeployment?.application?.serverId,
previewDeployment.application?.serverId,
previewDeployment.appName,
);
try {
const appName = `${previewDeployment.appName}`;
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
const { LOGS_PATH } = paths(!!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,
);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${LOGS_PATH}/${appName};
echo "Initializing deployment" >> ${logFilePath};
echo "Initializing deployment" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
@ -249,6 +256,9 @@ export const createDeploymentPreview = async (
description: deployment.description || "",
previewDeploymentId: deployment.previewDeploymentId,
startedAt: new Date().toISOString(),
...(serverId && {
buildServerId: serverId,
}),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
@ -614,13 +624,14 @@ export const createDeploymentVolumeBackup = async (
}
};
export const removeDeployment = async (deploymentId: string) => {
export const removeDeployment = async (
deploymentId: string,
fallbackServerId?: string | null,
) => {
try {
const deployment = await db
.delete(deployments)
.where(eq(deployments.deploymentId, deploymentId))
.returning()
.then((result) => result[0]);
const deployment = await db.query.deployments.findFirst({
where: eq(deployments.deploymentId, deploymentId),
});
if (!deployment) {
return null;
@ -629,13 +640,21 @@ export const removeDeployment = async (deploymentId: string) => {
const logPath = path.join(deployment.logPath);
if (logPath && logPath !== ".") {
const command = `rm -f ${logPath};`;
if (deployment.serverId) {
await execAsyncRemote(deployment.serverId, command);
const deploymentServerId =
deployment.buildServerId || deployment.serverId || fallbackServerId;
if (deploymentServerId) {
await execAsyncRemote(deploymentServerId, command);
} else {
await execAsync(command);
}
}
await db
.delete(deployments)
.where(eq(deployments.deploymentId, deploymentId))
.returning();
return deployment;
} catch (error) {
const message =
@ -699,73 +718,86 @@ const removeLastTenDeployments = async (
| "previewDeployment"
| "backup"
| "volumeBackup",
serverId?: string | null,
fallbackServerId?: string | null,
previewAppName?: string,
) => {
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 (deploymentList.length <= 10) {
return;
}
const retainedDeployments = deploymentList.slice(0, 10);
const deploymentsToDelete = deploymentList.slice(10);
const getDeploymentServerId = (deployment: (typeof deploymentList)[number]) =>
deployment.buildServerId || deployment.serverId || fallbackServerId || null;
const retainedServerIds = new Set(
retainedDeployments.map(getDeploymentServerId),
);
const removedServerIds = new Set(
deploymentsToDelete.map(getDeploymentServerId),
);
const failedServerIds = new Set<string | null>();
if (type === "previewDeployment" && previewAppName) {
for (const serverId of removedServerIds) {
if (retainedServerIds.has(serverId)) {
continue;
}
if (command) {
await execAsyncRemote(serverId, command);
try {
await removeDirectoryCode(previewAppName, serverId);
} catch (error) {
failedServerIds.add(serverId);
console.error(
`Failed to remove preview source from ${serverId || "the local server"} during deployment cleanup:`,
error,
);
}
} 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,
);
}
}
}
for (const oldDeployment of deploymentsToDelete) {
const deploymentServerId = getDeploymentServerId(oldDeployment);
if (failedServerIds.has(deploymentServerId)) {
continue;
}
try {
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
await removeDeployment(oldDeployment.deploymentId, fallbackServerId);
} catch (error) {
console.error(
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
error,
);
}
}
};
export const removeDeploymentsByPreviewDeploymentId = async (
previewDeployment: PreviewDeployment,
serverId: string | null,
serverIds: Array<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);
const uniqueServerIds = [...new Set(serverIds)];
for (const serverId of uniqueServerIds) {
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

View File

@ -28,11 +28,19 @@ export const findPreviewDeploymentById = async (
const application = await db.query.previewDeployments.findFirst({
where: eq(previewDeployments.previewDeploymentId, previewDeploymentId),
with: {
deployments: {
columns: {
buildServerId: true,
status: true,
},
orderBy: desc(deployments.createdAt),
},
domain: true,
application: {
columns: {
applicationId: true,
serverId: true,
buildServerId: true,
},
},
},
@ -55,33 +63,35 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => {
);
application.appName = previewDeployment.appName;
const cleanupOperations = [
async () =>
await removeService(application?.appName, application?.serverId),
async () =>
await removeDeploymentsByPreviewDeploymentId(
previewDeployment,
application?.serverId,
),
async () =>
await removeDirectoryCode(application?.appName, application?.serverId),
async () =>
await removeTraefikConfig(application?.appName, application?.serverId),
async () =>
await db
.delete(previewDeployments)
.where(
eq(previewDeployments.previewDeploymentId, previewDeploymentId),
)
.returning(),
const cleanupServerIds = [
null,
application.serverId,
application.buildServerId,
...previewDeployment.deployments.map(
(deployment) => deployment.buildServerId,
),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {
console.error(error);
}
const uniqueCleanupServerIds = [...new Set(cleanupServerIds)];
await removeService(application.appName, application.serverId);
for (const serverId of uniqueCleanupServerIds) {
await removeDirectoryCode(application.appName, serverId);
}
await removeTraefikConfig(application.appName, application.serverId);
await removeDeploymentsByPreviewDeploymentId(
previewDeployment,
uniqueCleanupServerIds,
);
await db
.delete(previewDeployments)
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
.returning();
return previewDeployment;
} catch (error) {
const message =