From 30d2f382590658c958c5fb438f210786bd4be1ee Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 29 Nov 2025 00:44:44 -0600 Subject: [PATCH 1/5] feat: enhance CI workflow with Nixpacks and Railpack installation - Added steps to install Nixpacks and Railpack in the CI workflow for testing jobs. - Updated the PATH to include build tools for better accessibility during the build process. - Improved Vitest configuration to ensure proper TypeScript path resolution. --- .github/workflows/pull-request.yml | 19 + .../deploy/application.command.test.ts | 277 ++++++++++ .../__test__/deploy/application.real.test.ts | 479 ++++++++++++++++++ apps/dokploy/__test__/vitest.config.ts | 6 +- 4 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/__test__/deploy/application.command.test.ts create mode 100644 apps/dokploy/__test__/deploy/application.real.test.ts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6c74dbc02..7ea03fe0f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -20,6 +20,25 @@ jobs: with: node-version: 20.16.0 cache: "pnpm" + + - name: Install Nixpacks + if: matrix.job == 'test' + run: | + export NIXPACKS_VERSION=1.39.0 + curl -sSL https://nixpacks.com/install.sh | bash + echo "Nixpacks installed $NIXPACKS_VERSION" + + - name: Install Railpack + if: matrix.job == 'test' + run: | + export RAILPACK_VERSION=0.15.0 + curl -sSL https://railpack.com/install.sh | bash + echo "Railpack installed $RAILPACK_VERSION" + + - name: Add build tools to PATH + if: matrix.job == 'test' + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + - run: pnpm install --frozen-lockfile - run: pnpm server:build - run: pnpm ${{ matrix.job }} diff --git a/apps/dokploy/__test__/deploy/application.command.test.ts b/apps/dokploy/__test__/deploy/application.command.test.ts new file mode 100644 index 000000000..7bfa93ac8 --- /dev/null +++ b/apps/dokploy/__test__/deploy/application.command.test.ts @@ -0,0 +1,277 @@ +import * as adminService from "@dokploy/server/services/admin"; +import * as applicationService from "@dokploy/server/services/application"; +import { deployApplication } from "@dokploy/server/services/application"; +import * as deploymentService from "@dokploy/server/services/deployment"; +import * as builders from "@dokploy/server/utils/builders"; +import * as errorNotifications from "@dokploy/server/utils/notifications/build-error"; +import * as notifications from "@dokploy/server/utils/notifications/build-success"; +import * as execProcess from "@dokploy/server/utils/process/execAsync"; +import * as gitProvider from "@dokploy/server/utils/providers/git"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@dokploy/server/db", () => { + const createChainableMock = () => { + const chain = { + set: vi.fn(() => chain), + where: vi.fn(() => chain), + returning: vi.fn().mockResolvedValue([{}]), + }; + return chain; + }; + + return { + db: { + select: vi.fn(), + insert: vi.fn(), + update: vi.fn(() => createChainableMock()), + delete: vi.fn(), + query: { + applications: { + findFirst: vi.fn(), + }, + }, + }, + }; +}); + +vi.mock("@dokploy/server/services/application", async () => { + const actual = await vi.importActual< + typeof import("@dokploy/server/services/application") + >("@dokploy/server/services/application"); + return { + ...actual, + findApplicationById: vi.fn(), + updateApplicationStatus: vi.fn(), + }; +}); + +vi.mock("@dokploy/server/services/admin", () => ({ + getDokployUrl: vi.fn(), +})); + +vi.mock("@dokploy/server/services/deployment", () => ({ + createDeployment: vi.fn(), + updateDeploymentStatus: vi.fn(), + updateDeployment: vi.fn(), +})); + +vi.mock("@dokploy/server/utils/providers/git", async () => { + const actual = await vi.importActual< + typeof import("@dokploy/server/utils/providers/git") + >("@dokploy/server/utils/providers/git"); + return { + ...actual, + getGitCommitInfo: vi.fn(), + }; +}); + +vi.mock("@dokploy/server/utils/process/execAsync", () => ({ + execAsync: vi.fn(), + ExecError: class ExecError extends Error {}, +})); + +vi.mock("@dokploy/server/utils/builders", async () => { + const actual = await vi.importActual< + typeof import("@dokploy/server/utils/builders") + >("@dokploy/server/utils/builders"); + return { + ...actual, + mechanizeDockerContainer: vi.fn(), + getBuildCommand: vi.fn(), + }; +}); + +vi.mock("@dokploy/server/utils/notifications/build-success", () => ({ + sendBuildSuccessNotifications: vi.fn(), +})); + +vi.mock("@dokploy/server/utils/notifications/build-error", () => ({ + sendBuildErrorNotifications: vi.fn(), +})); + +vi.mock("@dokploy/server/services/rollbacks", () => ({ + createRollback: vi.fn(), +})); + +import { db } from "@dokploy/server/db"; +import { cloneGitRepository } from "@dokploy/server/utils/providers/git"; + +const createMockApplication = (overrides = {}) => ({ + applicationId: "test-app-id", + name: "Test App", + appName: "test-app", + sourceType: "git" as const, + customGitUrl: "https://github.com/Dokploy/examples.git", + customGitBranch: "main", + customGitSSHKeyId: null, + buildType: "nixpacks" as const, + buildPath: "/astro", + env: "NODE_ENV=production", + serverId: null, + rollbackActive: false, + enableSubmodules: false, + environmentId: "env-id", + environment: { + projectId: "project-id", + env: "", + name: "production", + project: { + name: "Test Project", + organizationId: "org-id", + env: "", + }, + }, + domains: [], + ...overrides, +}); + +const createMockDeployment = () => ({ + deploymentId: "deployment-id", + logPath: "/tmp/test-deployment.log", +}); + +describe("deployApplication - Command Generation Tests", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(db.query.applications.findFirst).mockResolvedValue( + createMockApplication() as any, + ); + vi.mocked(applicationService.findApplicationById).mockResolvedValue( + createMockApplication() as any, + ); + vi.mocked(adminService.getDokployUrl).mockResolvedValue( + "http://localhost:3000", + ); + vi.mocked(deploymentService.createDeployment).mockResolvedValue( + createMockDeployment() as any, + ); + vi.mocked(execProcess.execAsync).mockResolvedValue({ + stdout: "", + stderr: "", + } as any); + vi.mocked(builders.mechanizeDockerContainer).mockResolvedValue( + undefined as any, + ); + vi.mocked(deploymentService.updateDeploymentStatus).mockResolvedValue( + undefined as any, + ); + vi.mocked(applicationService.updateApplicationStatus).mockResolvedValue( + {} as any, + ); + vi.mocked(notifications.sendBuildSuccessNotifications).mockResolvedValue( + undefined as any, + ); + vi.mocked(gitProvider.getGitCommitInfo).mockResolvedValue({ + message: "test commit", + hash: "abc123", + }); + vi.mocked(deploymentService.updateDeployment).mockResolvedValue({} as any); + }); + + it("should generate correct git clone command for astro example", async () => { + const app = createMockApplication(); + const command = await cloneGitRepository(app); + console.log(command); + + expect(command).toContain("https://github.com/Dokploy/examples.git"); + expect(command).not.toContain("--recurse-submodules"); + expect(command).toContain("--branch main"); + expect(command).toContain("--depth 1"); + expect(command).toContain("git clone"); + }); + + it("should generate git clone with submodules when enabled", async () => { + const app = createMockApplication({ enableSubmodules: true }); + const command = await cloneGitRepository(app); + + expect(command).toContain("--recurse-submodules"); + expect(command).toContain("https://github.com/Dokploy/examples.git"); + }); + + it("should verify nixpacks command is called with correct app", async () => { + const mockNixpacksCommand = "nixpacks build /path/to/app --name test-app"; + vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand); + + await deployApplication({ + applicationId: "test-app-id", + titleLog: "Test deployment", + descriptionLog: "", + }); + + expect(builders.getBuildCommand).toHaveBeenCalledWith( + expect.objectContaining({ + buildType: "nixpacks", + customGitUrl: "https://github.com/Dokploy/examples.git", + buildPath: "/astro", + }), + ); + + expect(execProcess.execAsync).toHaveBeenCalledWith( + expect.stringContaining("nixpacks build"), + ); + }); + + it("should verify railpack command includes correct parameters", async () => { + const mockApp = createMockApplication({ buildType: "railpack" }); + vi.mocked(db.query.applications.findFirst).mockResolvedValue( + mockApp as any, + ); + vi.mocked(applicationService.findApplicationById).mockResolvedValue( + mockApp as any, + ); + + const mockRailpackCommand = "railpack prepare /path/to/app"; + vi.mocked(builders.getBuildCommand).mockReturnValue(mockRailpackCommand); + + await deployApplication({ + applicationId: "test-app-id", + titleLog: "Railpack test", + descriptionLog: "", + }); + + expect(builders.getBuildCommand).toHaveBeenCalledWith( + expect.objectContaining({ + buildType: "railpack", + }), + ); + + expect(execProcess.execAsync).toHaveBeenCalledWith( + expect.stringContaining("railpack prepare"), + ); + }); + + it("should execute commands in correct order", async () => { + const mockNixpacksCommand = "nixpacks build"; + vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand); + + await deployApplication({ + applicationId: "test-app-id", + titleLog: "Test", + descriptionLog: "", + }); + + const execCalls = vi.mocked(execProcess.execAsync).mock.calls; + expect(execCalls.length).toBeGreaterThan(0); + + const fullCommand = execCalls[0]?.[0]; + expect(fullCommand).toContain("set -e"); + expect(fullCommand).toContain("git clone"); + expect(fullCommand).toContain("nixpacks build"); + }); + + it("should include log redirection in command", async () => { + const mockCommand = "nixpacks build"; + vi.mocked(builders.getBuildCommand).mockReturnValue(mockCommand); + + await deployApplication({ + applicationId: "test-app-id", + titleLog: "Test", + descriptionLog: "", + }); + + const execCalls = vi.mocked(execProcess.execAsync).mock.calls; + const fullCommand = execCalls[0]?.[0]; + + expect(fullCommand).toContain(">> /tmp/test-deployment.log 2>&1"); + }); +}); diff --git a/apps/dokploy/__test__/deploy/application.real.test.ts b/apps/dokploy/__test__/deploy/application.real.test.ts new file mode 100644 index 000000000..2fed049ac --- /dev/null +++ b/apps/dokploy/__test__/deploy/application.real.test.ts @@ -0,0 +1,479 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import type { ApplicationNested } from "@dokploy/server"; +import { paths } from "@dokploy/server/constants"; +import { execAsync } from "@dokploy/server/utils/process/execAsync"; +import { format } from "date-fns"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const REAL_TEST_TIMEOUT = 180000; // 3 minutos + +// Mock SOLO la base de datos y notificaciones +vi.mock("@dokploy/server/db", () => { + const createChainableMock = (): any => { + const chain: any = { + set: vi.fn(() => chain), + where: vi.fn(() => chain), + returning: vi.fn().mockResolvedValue([{}]), + }; + return chain; + }; + + return { + db: { + select: vi.fn(), + insert: vi.fn(), + update: vi.fn(() => createChainableMock()), + delete: vi.fn(), + query: { + applications: { + findFirst: vi.fn(), + }, + }, + }, + }; +}); + +vi.mock("@dokploy/server/services/application", async () => { + const actual = await vi.importActual< + typeof import("@dokploy/server/services/application") + >("@dokploy/server/services/application"); + return { + ...actual, + findApplicationById: vi.fn(), + updateApplicationStatus: vi.fn(), + }; +}); + +vi.mock("@dokploy/server/services/admin", () => ({ + getDokployUrl: vi.fn().mockResolvedValue("http://localhost:3000"), +})); + +vi.mock("@dokploy/server/services/deployment", () => ({ + createDeployment: vi.fn(), + updateDeploymentStatus: vi.fn(), + updateDeployment: vi.fn(), +})); + +vi.mock("@dokploy/server/utils/notifications/build-success", () => ({ + sendBuildSuccessNotifications: vi.fn(), +})); + +vi.mock("@dokploy/server/utils/notifications/build-error", () => ({ + sendBuildErrorNotifications: vi.fn(), +})); + +vi.mock("@dokploy/server/services/rollbacks", () => ({ + createRollback: vi.fn(), +})); + +// NO mockeamos: +// - execAsync (queremos que se ejecute de verdad) +// - cloneGitRepository (queremos que se ejecute de verdad) +// - getBuildCommand (queremos que se ejecute de verdad) +// - mechanizeDockerContainer (queremos que se ejecute de verdad) + +import { db } from "@dokploy/server/db"; +import * as adminService from "@dokploy/server/services/admin"; +import * as applicationService from "@dokploy/server/services/application"; +import { deployApplication } from "@dokploy/server/services/application"; +import * as deploymentService from "@dokploy/server/services/deployment"; + +const createMockApplication = ( + overrides: Partial = {}, +): ApplicationNested => + ({ + applicationId: "test-app-id", + name: "Real Test App", + appName: `real-test-${Date.now()}`, + sourceType: "git" as const, + customGitUrl: "https://github.com/Dokploy/examples.git", + customGitBranch: "main", + customGitSSHKeyId: null, + customGitBuildPath: "/astro", + buildType: "nixpacks" as const, + env: "NODE_ENV=production", + serverId: null, + rollbackActive: false, + enableSubmodules: false, + environmentId: "env-id", + environment: { + projectId: "project-id", + env: "", + name: "production", + project: { + name: "Test Project", + organizationId: "org-id", + env: "", + }, + }, + domains: [], + mounts: [], + security: [], + redirects: [], + ports: [], + registry: null, + ...overrides, + }) as ApplicationNested; + +const createMockDeployment = async (appName: string) => { + const { LOGS_PATH } = paths(false); // false = local, no remote server + const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); + const fileName = `${appName}-${formattedDateTime}.log`; + const logFilePath = path.join(LOGS_PATH, appName, fileName); + + // Crear el directorio de logs REALMENTE + await execAsync(`mkdir -p ${path.dirname(logFilePath)}`); + await execAsync(`echo "Initializing deployment" > ${logFilePath}`); + + return { + deploymentId: "deployment-id", + logPath: logFilePath, + }; +}; + +async function cleanupDocker(appName: string) { + try { + await execAsync(`docker stop ${appName} 2>/dev/null || true`); + await execAsync(`docker rm ${appName} 2>/dev/null || true`); + await execAsync(`docker rmi ${appName} 2>/dev/null || true`); + } catch (error) { + console.log("Docker cleanup completed"); + } +} + +async function cleanupFiles(appName: string) { + try { + const { LOGS_PATH, APPLICATIONS_PATH } = paths(false); + + // Limpiar directorios de código clonado + const appPath = path.join(APPLICATIONS_PATH, appName); + await execAsync(`rm -rf ${appPath} 2>/dev/null || true`); + + // Limpiar logs del appName - elimina el folder completo + const logPath = path.join(LOGS_PATH, appName); + await execAsync(`rm -rf ${logPath} 2>/dev/null || true`); + + console.log(`✅ Cleaned up files and logs for ${appName}`); + } catch (error) { + console.error(`⚠️ Error during cleanup for ${appName}:`, error); + } +} + +describe( + "deployApplication - REAL Execution Tests", + () => { + let currentAppName: string; + let currentDeployment: any; + const allTestAppNames: string[] = []; + + beforeEach(async () => { + vi.clearAllMocks(); + currentAppName = `real-test-${Date.now()}`; + currentDeployment = await createMockDeployment(currentAppName); + allTestAppNames.push(currentAppName); + + const mockApp = createMockApplication({ appName: currentAppName }); + + vi.mocked(db.query.applications.findFirst).mockResolvedValue( + mockApp as any, + ); + vi.mocked(applicationService.findApplicationById).mockResolvedValue( + mockApp as any, + ); + vi.mocked(adminService.getDokployUrl).mockResolvedValue( + "http://localhost:3000", + ); + vi.mocked(deploymentService.createDeployment).mockResolvedValue( + currentDeployment as any, + ); + vi.mocked(deploymentService.updateDeploymentStatus).mockResolvedValue( + undefined as any, + ); + vi.mocked(applicationService.updateApplicationStatus).mockResolvedValue( + {} as any, + ); + vi.mocked(deploymentService.updateDeployment).mockResolvedValue( + {} as any, + ); + }); + + afterEach(async () => { + // SIEMPRE limpia, incluso si el test falló o pasó + console.log(`\n🧹 Cleaning up test: ${currentAppName}`); + + // Limpiar el appName actual + try { + await cleanupDocker(currentAppName); + await cleanupFiles(currentAppName); + } catch (error) { + console.error("⚠️ Error cleaning current app:", error); + } + + // Limpiar TODOS los folders de test por si acaso + try { + const { LOGS_PATH, APPLICATIONS_PATH } = paths(false); + await execAsync(`rm -rf ${LOGS_PATH}/real-* 2>/dev/null || true`); + await execAsync( + `rm -rf ${APPLICATIONS_PATH}/real-* 2>/dev/null || true`, + ); + console.log("✅ Cleaned up all test artifacts"); + } catch (error) { + console.error("⚠️ Error cleaning all artifacts:", error); + } + + console.log("✅ Cleanup completed\n"); + }); + + it( + "should REALLY clone git repo and build with nixpacks", + async () => { + console.log(`\n🚀 Testing real deployment with app: ${currentAppName}`); + + const result = await deployApplication({ + applicationId: "test-app-id", + titleLog: "Real Nixpacks Test", + descriptionLog: "Testing real execution", + }); + + expect(result).toBe(true); + + // Verificar que la imagen Docker fue creada DE VERDAD + const { stdout: dockerImages } = await execAsync( + `docker images ${currentAppName} --format "{{.Repository}}"`, + ); + console.log("dockerImages", dockerImages); + expect(dockerImages.trim()).toBe(currentAppName); + console.log(`✅ Docker image created: ${currentAppName}`); + + // Verificar que el log existe y tiene contenido + expect(existsSync(currentDeployment.logPath)).toBe(true); + const { stdout: logContent } = await execAsync( + `cat ${currentDeployment.logPath}`, + ); + expect(logContent).toContain("Cloning"); + expect(logContent).toContain("nixpacks"); + console.log(`✅ Build log created with ${logContent.length} chars`); + + // Verificar que las funciones de actualización fueron llamadas + expect(deploymentService.updateDeploymentStatus).toHaveBeenCalledWith( + "deployment-id", + "done", + ); + }, + REAL_TEST_TIMEOUT, + ); + + it.skip( + "should REALLY build with railpack (SKIPPED: requires special permissions)", + async () => { + const railpackAppName = `real-railpack-${Date.now()}`; + const railpackApp = createMockApplication({ + appName: railpackAppName, + buildType: "railpack", + railpackVersion: "3", + }); + currentAppName = railpackAppName; + allTestAppNames.push(railpackAppName); + + vi.mocked(db.query.applications.findFirst).mockResolvedValue( + railpackApp as any, + ); + vi.mocked(applicationService.findApplicationById).mockResolvedValue( + railpackApp as any, + ); + + console.log(`\n🚀 Testing real railpack deployment: ${currentAppName}`); + + const result = await deployApplication({ + applicationId: "test-app-id", + titleLog: "Real Railpack Test", + descriptionLog: "", + }); + + expect(result).toBe(true); + + const { stdout: dockerImages } = await execAsync( + `docker images ${currentAppName} --format "{{.Repository}}"`, + ); + expect(dockerImages.trim()).toBe(currentAppName); + console.log(`✅ Railpack image created: ${currentAppName}`); + + const { stdout: logContent } = await execAsync( + `cat ${currentDeployment.logPath}`, + ); + expect(logContent).toContain("railpack"); + console.log("✅ Railpack build completed"); + }, + REAL_TEST_TIMEOUT, + ); + + it( + "should handle REAL git clone errors", + async () => { + const errorAppName = `real-error-${Date.now()}`; + const errorApp = createMockApplication({ + appName: errorAppName, + customGitUrl: + "https://github.com/invalid/nonexistent-repo-123456.git", + }); + currentAppName = errorAppName; + allTestAppNames.push(errorAppName); + + vi.mocked(db.query.applications.findFirst).mockResolvedValue( + errorApp as any, + ); + vi.mocked(applicationService.findApplicationById).mockResolvedValue( + errorApp as any, + ); + + console.log(`\n🚀 Testing real error handling: ${currentAppName}`); + + await expect( + deployApplication({ + applicationId: "test-app-id", + titleLog: "Real Error Test", + descriptionLog: "", + }), + ).rejects.toThrow(); + + // Verificar que se llamó con estado de error + expect(deploymentService.updateDeploymentStatus).toHaveBeenCalledWith( + "deployment-id", + "error", + ); + + // Verificar que el log contiene el error + const { stdout: logContent } = await execAsync( + `cat ${currentDeployment.logPath}`, + ); + expect(logContent.toLowerCase()).toContain("error"); + console.log("✅ Error handling verified"); + }, + REAL_TEST_TIMEOUT, + ); + + it( + "should REALLY clone with submodules when enabled", + async () => { + const submodulesAppName = `real-submodules-${Date.now()}`; + const submodulesApp = createMockApplication({ + appName: submodulesAppName, + enableSubmodules: true, + }); + currentAppName = submodulesAppName; + allTestAppNames.push(submodulesAppName); + + vi.mocked(db.query.applications.findFirst).mockResolvedValue( + submodulesApp as any, + ); + vi.mocked(applicationService.findApplicationById).mockResolvedValue( + submodulesApp as any, + ); + + console.log(`\n🚀 Testing real submodules support: ${currentAppName}`); + + const result = await deployApplication({ + applicationId: "test-app-id", + titleLog: "Real Submodules Test", + descriptionLog: "", + }); + + expect(result).toBe(true); + + // Verificar que el deployment completó exitosamente + const { stdout: logContent } = await execAsync( + `cat ${currentDeployment.logPath}`, + ); + expect(logContent).toContain("Cloning"); + expect(logContent.length).toBeGreaterThan(100); + console.log("✅ Submodules deployment completed"); + + // Verificar imagen + const { stdout: dockerImages } = await execAsync( + `docker images ${currentAppName} --format "{{.Repository}}"`, + ); + expect(dockerImages.trim()).toBe(currentAppName); + }, + REAL_TEST_TIMEOUT, + ); + + it( + "should verify REAL commit info extraction", + async () => { + console.log(`\n🚀 Testing real commit info: ${currentAppName}`); + + await deployApplication({ + applicationId: "test-app-id", + titleLog: "Real Commit Test", + descriptionLog: "", + }); + + // Verificar que se llamó updateDeployment con info del commit + expect(deploymentService.updateDeployment).toHaveBeenCalled(); + const updateCall = vi.mocked(deploymentService.updateDeployment).mock + .calls[0]; + + // El commit info real debería tener título y hash + expect(updateCall?.[1]).toHaveProperty("title"); + expect(updateCall?.[1]).toHaveProperty("description"); + expect(updateCall?.[1]?.description).toContain("Commit:"); + + console.log( + `✅ Real commit extracted: ${updateCall?.[1]?.title?.substring(0, 50)}...`, + ); + }, + REAL_TEST_TIMEOUT, + ); + + it( + "should REALLY build with Dockerfile", + async () => { + const dockerfileAppName = `real-dockerfile-${Date.now()}`; + const dockerfileApp = createMockApplication({ + appName: dockerfileAppName, + buildType: "dockerfile", + customGitBuildPath: "/deno", + dockerfile: "Dockerfile", + }); + currentAppName = dockerfileAppName; + allTestAppNames.push(dockerfileAppName); + + vi.mocked(db.query.applications.findFirst).mockResolvedValue( + dockerfileApp as any, + ); + vi.mocked(applicationService.findApplicationById).mockResolvedValue( + dockerfileApp as any, + ); + + console.log(`\n🚀 Testing real Dockerfile build: ${currentAppName}`); + + const result = await deployApplication({ + applicationId: "test-app-id", + titleLog: "Real Dockerfile Test", + descriptionLog: "", + }); + + expect(result).toBe(true); + + // Verificar el log + const { stdout: logContent } = await execAsync( + `cat ${currentDeployment.logPath}`, + ); + expect(logContent).toContain("Building"); + expect(logContent).toContain(dockerfileAppName); + console.log("✅ Dockerfile build log verified"); + + // Verificar imagen + const { stdout: dockerImages } = await execAsync( + `docker images ${currentAppName} --format "{{.Repository}}"`, + ); + console.log("dockerImages", dockerImages); + expect(dockerImages.trim()).toBe(currentAppName); + console.log(`✅ Docker image created: ${currentAppName}`); + }, + REAL_TEST_TIMEOUT, + ); + }, + REAL_TEST_TIMEOUT, +); diff --git a/apps/dokploy/__test__/vitest.config.ts b/apps/dokploy/__test__/vitest.config.ts index ddc84d6ac..7270b828a 100644 --- a/apps/dokploy/__test__/vitest.config.ts +++ b/apps/dokploy/__test__/vitest.config.ts @@ -13,7 +13,11 @@ export default defineConfig({ NODE: "test", }, }, - plugins: [tsconfigPaths()], + plugins: [ + tsconfigPaths({ + projects: [path.resolve(__dirname, "../tsconfig.json")], + }), + ], resolve: { alias: { "@dokploy/server": path.resolve( From f77a67ba33758f178761810be7fc4e20391f8bed Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 29 Nov 2025 00:47:31 -0600 Subject: [PATCH 2/5] refactor: improve type safety in application command test mock - Updated the type definition for the createChainableMock function to enhance type safety. - Ensured that the returning method in the mock returns a properly typed value. --- apps/dokploy/__test__/deploy/application.command.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/__test__/deploy/application.command.test.ts b/apps/dokploy/__test__/deploy/application.command.test.ts index 7bfa93ac8..a0c4387c8 100644 --- a/apps/dokploy/__test__/deploy/application.command.test.ts +++ b/apps/dokploy/__test__/deploy/application.command.test.ts @@ -3,19 +3,18 @@ import * as applicationService from "@dokploy/server/services/application"; import { deployApplication } from "@dokploy/server/services/application"; import * as deploymentService from "@dokploy/server/services/deployment"; import * as builders from "@dokploy/server/utils/builders"; -import * as errorNotifications from "@dokploy/server/utils/notifications/build-error"; import * as notifications from "@dokploy/server/utils/notifications/build-success"; import * as execProcess from "@dokploy/server/utils/process/execAsync"; import * as gitProvider from "@dokploy/server/utils/providers/git"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@dokploy/server/db", () => { - const createChainableMock = () => { + const createChainableMock = (): any => { const chain = { set: vi.fn(() => chain), where: vi.fn(() => chain), - returning: vi.fn().mockResolvedValue([{}]), - }; + returning: vi.fn().mockResolvedValue([{}] as any), + } as any; return chain; }; From 067777f28efa7dca22f33a72cfb6722ec2707c75 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 29 Nov 2025 00:55:14 -0600 Subject: [PATCH 3/5] feat: initialize Docker Swarm in CI workflow - Added a step to initialize Docker Swarm and create an overlay network for testing jobs. - This enhancement improves the CI environment setup for containerized testing. --- .github/workflows/pull-request.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7ea03fe0f..31dbc48fb 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -39,6 +39,13 @@ jobs: if: matrix.job == 'test' run: echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Initialize Docker Swarm + if: matrix.job == 'test' + run: | + docker swarm init + docker network create --driver overlay dokploy-network || true + echo "✅ Docker Swarm initialized" + - run: pnpm install --frozen-lockfile - run: pnpm server:build - run: pnpm ${{ matrix.job }} From a72281c0185a9fa594966b7953cc2ba1858036fb Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 29 Nov 2025 01:07:22 -0600 Subject: [PATCH 4/5] refactor: enhance StopGracePeriod handling in database builders - Updated the condition for StopGracePeriod in various database builder files to check for null and undefined values, improving code robustness and clarity. --- packages/server/src/utils/builders/index.ts | 3 ++- packages/server/src/utils/databases/mariadb.ts | 3 ++- packages/server/src/utils/databases/mongo.ts | 3 ++- packages/server/src/utils/databases/mysql.ts | 3 ++- packages/server/src/utils/databases/postgres.ts | 3 ++- packages/server/src/utils/databases/redis.ts | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 11912f6c5..a70e874c2 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -125,7 +125,8 @@ export const mechanizeDockerContainer = async ( Image: image, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(StopGracePeriod && { StopGracePeriod }), + ...(StopGracePeriod !== null && + StopGracePeriod !== undefined && { StopGracePeriod }), ...(command ? { Command: ["/bin/sh"], diff --git a/packages/server/src/utils/databases/mariadb.ts b/packages/server/src/utils/databases/mariadb.ts index 1db457018..852e174ef 100644 --- a/packages/server/src/utils/databases/mariadb.ts +++ b/packages/server/src/utils/databases/mariadb.ts @@ -73,7 +73,8 @@ export const buildMariadb = async (mariadb: MariadbNested) => { Image: dockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(StopGracePeriod && { StopGracePeriod }), + ...(StopGracePeriod !== null && + StopGracePeriod !== undefined && { StopGracePeriod }), ...(command ? { Command: ["/bin/sh"], diff --git a/packages/server/src/utils/databases/mongo.ts b/packages/server/src/utils/databases/mongo.ts index 3b71f323b..af5dc8cbd 100644 --- a/packages/server/src/utils/databases/mongo.ts +++ b/packages/server/src/utils/databases/mongo.ts @@ -121,7 +121,8 @@ ${command ?? "wait $MONGOD_PID"}`; Image: dockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(StopGracePeriod && { StopGracePeriod }), + ...(StopGracePeriod !== null && + StopGracePeriod !== undefined && { StopGracePeriod }), ...(replicaSets ? { Command: ["/bin/bash"], diff --git a/packages/server/src/utils/databases/mysql.ts b/packages/server/src/utils/databases/mysql.ts index 069803a7c..a1f75dd40 100644 --- a/packages/server/src/utils/databases/mysql.ts +++ b/packages/server/src/utils/databases/mysql.ts @@ -79,7 +79,8 @@ export const buildMysql = async (mysql: MysqlNested) => { Image: dockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(StopGracePeriod && { StopGracePeriod }), + ...(StopGracePeriod !== null && + StopGracePeriod !== undefined && { StopGracePeriod }), ...(command ? { Command: ["/bin/sh"], diff --git a/packages/server/src/utils/databases/postgres.ts b/packages/server/src/utils/databases/postgres.ts index afcd49ca1..e15f8a641 100644 --- a/packages/server/src/utils/databases/postgres.ts +++ b/packages/server/src/utils/databases/postgres.ts @@ -72,7 +72,8 @@ export const buildPostgres = async (postgres: PostgresNested) => { Image: dockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(StopGracePeriod && { StopGracePeriod }), + ...(StopGracePeriod !== null && + StopGracePeriod !== undefined && { StopGracePeriod }), ...(command ? { Command: ["/bin/sh"], diff --git a/packages/server/src/utils/databases/redis.ts b/packages/server/src/utils/databases/redis.ts index 7aa684565..2d9ae273d 100644 --- a/packages/server/src/utils/databases/redis.ts +++ b/packages/server/src/utils/databases/redis.ts @@ -70,7 +70,8 @@ export const buildRedis = async (redis: RedisNested) => { Image: dockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(StopGracePeriod && { StopGracePeriod }), + ...(StopGracePeriod !== null && + StopGracePeriod !== undefined && { StopGracePeriod }), Command: ["/bin/sh"], Args: [ "-c", From 27b605f961033202fc8e03cd57804872062ddde6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 29 Nov 2025 01:16:14 -0600 Subject: [PATCH 5/5] refactor: update comments and improve clarity in application real tests - Translated comments from Spanish to English for better accessibility. - Enhanced comment clarity to improve understanding of test behavior and expectations. --- .../__test__/deploy/application.real.test.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/dokploy/__test__/deploy/application.real.test.ts b/apps/dokploy/__test__/deploy/application.real.test.ts index 2fed049ac..43ff07836 100644 --- a/apps/dokploy/__test__/deploy/application.real.test.ts +++ b/apps/dokploy/__test__/deploy/application.real.test.ts @@ -6,9 +6,9 @@ import { execAsync } from "@dokploy/server/utils/process/execAsync"; import { format } from "date-fns"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const REAL_TEST_TIMEOUT = 180000; // 3 minutos +const REAL_TEST_TIMEOUT = 180000; // 3 minutes -// Mock SOLO la base de datos y notificaciones +// Mock ONLY database and notifications vi.mock("@dokploy/server/db", () => { const createChainableMock = (): any => { const chain: any = { @@ -67,11 +67,11 @@ vi.mock("@dokploy/server/services/rollbacks", () => ({ createRollback: vi.fn(), })); -// NO mockeamos: -// - execAsync (queremos que se ejecute de verdad) -// - cloneGitRepository (queremos que se ejecute de verdad) -// - getBuildCommand (queremos que se ejecute de verdad) -// - mechanizeDockerContainer (queremos que se ejecute de verdad) +// NOT mocked (executed for real): +// - execAsync +// - cloneGitRepository +// - getBuildCommand +// - mechanizeDockerContainer (requires Docker Swarm) import { db } from "@dokploy/server/db"; import * as adminService from "@dokploy/server/services/admin"; @@ -122,7 +122,7 @@ const createMockDeployment = async (appName: string) => { const fileName = `${appName}-${formattedDateTime}.log`; const logFilePath = path.join(LOGS_PATH, appName, fileName); - // Crear el directorio de logs REALMENTE + // Actually create the log directory await execAsync(`mkdir -p ${path.dirname(logFilePath)}`); await execAsync(`echo "Initializing deployment" > ${logFilePath}`); @@ -146,11 +146,11 @@ async function cleanupFiles(appName: string) { try { const { LOGS_PATH, APPLICATIONS_PATH } = paths(false); - // Limpiar directorios de código clonado + // Clean cloned code directories const appPath = path.join(APPLICATIONS_PATH, appName); await execAsync(`rm -rf ${appPath} 2>/dev/null || true`); - // Limpiar logs del appName - elimina el folder completo + // Clean logs for appName - removes entire folder const logPath = path.join(LOGS_PATH, appName); await execAsync(`rm -rf ${logPath} 2>/dev/null || true`); @@ -199,10 +199,10 @@ describe( }); afterEach(async () => { - // SIEMPRE limpia, incluso si el test falló o pasó + // ALWAYS cleanup, even if test failed or passed console.log(`\n🧹 Cleaning up test: ${currentAppName}`); - // Limpiar el appName actual + // Clean current appName try { await cleanupDocker(currentAppName); await cleanupFiles(currentAppName); @@ -210,7 +210,7 @@ describe( console.error("⚠️ Error cleaning current app:", error); } - // Limpiar TODOS los folders de test por si acaso + // Clean ALL test folders just in case try { const { LOGS_PATH, APPLICATIONS_PATH } = paths(false); await execAsync(`rm -rf ${LOGS_PATH}/real-* 2>/dev/null || true`); @@ -238,7 +238,7 @@ describe( expect(result).toBe(true); - // Verificar que la imagen Docker fue creada DE VERDAD + // Verify that Docker image was actually created const { stdout: dockerImages } = await execAsync( `docker images ${currentAppName} --format "{{.Repository}}"`, ); @@ -246,7 +246,7 @@ describe( expect(dockerImages.trim()).toBe(currentAppName); console.log(`✅ Docker image created: ${currentAppName}`); - // Verificar que el log existe y tiene contenido + // Verify log exists and has content expect(existsSync(currentDeployment.logPath)).toBe(true); const { stdout: logContent } = await execAsync( `cat ${currentDeployment.logPath}`, @@ -255,7 +255,7 @@ describe( expect(logContent).toContain("nixpacks"); console.log(`✅ Build log created with ${logContent.length} chars`); - // Verificar que las funciones de actualización fueron llamadas + // Verify update functions were called expect(deploymentService.updateDeploymentStatus).toHaveBeenCalledWith( "deployment-id", "done", @@ -337,13 +337,13 @@ describe( }), ).rejects.toThrow(); - // Verificar que se llamó con estado de error + // Verify error status was called expect(deploymentService.updateDeploymentStatus).toHaveBeenCalledWith( "deployment-id", "error", ); - // Verificar que el log contiene el error + // Verify log contains error const { stdout: logContent } = await execAsync( `cat ${currentDeployment.logPath}`, ); @@ -381,7 +381,7 @@ describe( expect(result).toBe(true); - // Verificar que el deployment completó exitosamente + // Verify deployment completed successfully const { stdout: logContent } = await execAsync( `cat ${currentDeployment.logPath}`, ); @@ -389,7 +389,7 @@ describe( expect(logContent.length).toBeGreaterThan(100); console.log("✅ Submodules deployment completed"); - // Verificar imagen + // Verify image const { stdout: dockerImages } = await execAsync( `docker images ${currentAppName} --format "{{.Repository}}"`, ); @@ -409,12 +409,12 @@ describe( descriptionLog: "", }); - // Verificar que se llamó updateDeployment con info del commit + // Verify updateDeployment was called with commit info expect(deploymentService.updateDeployment).toHaveBeenCalled(); const updateCall = vi.mocked(deploymentService.updateDeployment).mock .calls[0]; - // El commit info real debería tener título y hash + // Real commit info should have title and hash expect(updateCall?.[1]).toHaveProperty("title"); expect(updateCall?.[1]).toHaveProperty("description"); expect(updateCall?.[1]?.description).toContain("Commit:"); @@ -456,7 +456,7 @@ describe( expect(result).toBe(true); - // Verificar el log + // Verify log const { stdout: logContent } = await execAsync( `cat ${currentDeployment.logPath}`, ); @@ -464,7 +464,7 @@ describe( expect(logContent).toContain(dockerfileAppName); console.log("✅ Dockerfile build log verified"); - // Verificar imagen + // Verify image const { stdout: dockerImages } = await execAsync( `docker images ${currentAppName} --format "{{.Repository}}"`, );