mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-12 19:51:00 +05:00
Merge 08880ac1b6 into 853ca33659
This commit is contained in:
commit
c8c36bcbcb
332
apps/dokploy/__test__/deploy/preview-build-server.test.ts
Normal file
332
apps/dokploy/__test__/deploy/preview-build-server.test.ts
Normal 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;"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 =
|
||||
|
||||
Loading…
Reference in New Issue
Block a user