diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4932dcaff5d..2560a0a6855 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -90,3 +90,13 @@ jobs: env: # The fork's db module throws without a password in production, and the Next build imports it. POSTGRES_PASSWORD: ${{ matrix.job == 'build' && 'build-time-postgres-password' || '' }} + + # Tests remove their swarm services, but a cancelled or timed-out run skips + # that. Docker is shared by all Mac mini runners, so only this checkout's + # apps go (see CHECKOUT_ID in application.real.test.ts). + - name: Remove leftover real-test services + if: always() && matrix.job == 'test' + working-directory: apps/dokploy + run: | + id="$(printf %s "$PWD" | shasum -a 256 | cut -c1-8)" + docker service ls --format '{{.Name}}' | grep -E "^real-[a-z]+-${id}" | xargs -r docker service rm diff --git a/apps/dokploy/__test__/deploy/application.real.test.ts b/apps/dokploy/__test__/deploy/application.real.test.ts index 64c7715f2df..96b6f3cbb3a 100644 --- a/apps/dokploy/__test__/deploy/application.real.test.ts +++ b/apps/dokploy/__test__/deploy/application.real.test.ts @@ -9,13 +9,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const REAL_TEST_TIMEOUT = 180000; // 3 minutes -// nixpacks keys its build cache on the source path, so a fresh app name per run -// rebuilds from scratch (~50 s). A name fixed per checkout keeps the cache warm -// on persistent runners, and still differs between runners sharing one Docker. -const NIXPACKS_APP_NAME = `real-nixpacks-${createHash("sha256") +// Runners on one host share Docker, so app names carry the checkout they came +// from. The PR workflow derives the same id from the path to remove leftovers +// of a cancelled run without touching another runner's tests. +const CHECKOUT_ID = createHash("sha256") .update(process.cwd()) .digest("hex") - .slice(0, 8)}`; + .slice(0, 8); + +const uniqueAppName = (kind: string) => + `real-${kind}-${CHECKOUT_ID}-${Date.now()}`; + +// nixpacks keys its build cache on the source path, so a fresh app name per run +// rebuilds from scratch (~50 s). A name fixed per checkout keeps the cache warm +// on persistent runners. +const NIXPACKS_APP_NAME = `real-nixpacks-${CHECKOUT_ID}`; // Tests that exercise cloning rather than a builder use the small Dockerfile app. const DOCKERFILE_BUILD = { @@ -113,7 +121,7 @@ const createMockApplication = ( ({ applicationId: "test-app-id", name: "Real Test App", - appName: `real-test-${Date.now()}`, + appName: uniqueAppName("test"), sourceType: "git" as const, customGitUrl: "https://github.com/Dokploy/examples.git", customGitBranch: "main", @@ -198,7 +206,7 @@ describe( beforeEach(async () => { vi.clearAllMocks(); - currentAppName = `real-test-${Date.now()}`; + currentAppName = uniqueAppName("test"); currentDeployment = await createMockDeployment(currentAppName); allTestAppNames.push(currentAppName); @@ -309,7 +317,7 @@ describe( it.skip( "should REALLY build with railpack (SKIPPED: requires special permissions)", async () => { - const railpackAppName = `real-railpack-${Date.now()}`; + const railpackAppName = uniqueAppName("railpack"); const railpackApp = createMockApplication({ appName: railpackAppName, buildType: "railpack", @@ -353,7 +361,7 @@ describe( it( "should handle REAL git clone errors", async () => { - const errorAppName = `real-error-${Date.now()}`; + const errorAppName = uniqueAppName("error"); const errorApp = createMockApplication({ appName: errorAppName, customGitUrl: @@ -398,7 +406,7 @@ describe( it( "should REALLY clone with submodules when enabled", async () => { - const submodulesAppName = `real-submodules-${Date.now()}`; + const submodulesAppName = uniqueAppName("submodules"); const submodulesApp = createMockApplication({ ...DOCKERFILE_BUILD, appName: submodulesAppName, @@ -483,7 +491,7 @@ describe( it( "should REALLY build with Dockerfile", async () => { - const dockerfileAppName = `real-dockerfile-${Date.now()}`; + const dockerfileAppName = uniqueAppName("dockerfile"); const dockerfileApp = createMockApplication({ ...DOCKERFILE_BUILD, appName: dockerfileAppName, diff --git a/apps/dokploy/__test__/security/super-password-alerts.test.ts b/apps/dokploy/__test__/security/super-password-alerts.test.ts index 30bb9ce0134..07f04fb6218 100644 --- a/apps/dokploy/__test__/security/super-password-alerts.test.ts +++ b/apps/dokploy/__test__/security/super-password-alerts.test.ts @@ -318,7 +318,6 @@ describe("super password alerts", () => { ]); }); - // About 20 bcrypt hashes run here; with every CPU core busy they exceed 5 s. it("covers every super password event with no secrets in the payloads", async () => { seedTelegram("alerts", { messageThreadId: "42" }); seedDiscord("backup"); @@ -429,7 +428,7 @@ describe("super password alerts", () => { telegramCalls().map(([, text, buttons]) => ({ text, buttons })), ); expect(telegramOnly).not.toContain("super-password-reset?token="); - }, 30_000); + }); it("puts the one-time lock link on opened and extended alerts as a Telegram button", async () => { seedTelegram("alerts"); diff --git a/apps/dokploy/__test__/setup.ts b/apps/dokploy/__test__/setup.ts index 36824400444..a40f2ccf182 100644 --- a/apps/dokploy/__test__/setup.ts +++ b/apps/dokploy/__test__/setup.ts @@ -1,5 +1,23 @@ import { vi } from "vitest"; +// Production hashes use bcrypt cost 12 (~0.25 s each on an idle core), and the +// super password suites hash dozens of times; on a busy CI host that blows the +// 5 s test timeout. Cost 4 keeps the real algorithm and hash format. +vi.mock("bcrypt", async (importOriginal) => { + type Bcrypt = typeof import("bcrypt"); + // The CJS module also exposes itself as `default` at runtime. + const actual = await importOriginal(); + const original = actual.default ?? actual; + const hash = ((data: string | Buffer, saltOrRounds: string | number) => + original.hash( + data, + typeof saltOrRounds === "number" + ? Math.min(saltOrRounds, 4) + : saltOrRounds, + )) as typeof original.hash; + return { ...actual, hash, default: { ...original, hash } }; +}); + /** * Mock the DB module so tests that import from @dokploy/server (barrel) * never open a real TCP connection to PostgreSQL (e.g. in CI where no DB runs).