From 2739690758cbc96810b0c66e1923bc9d912a60af Mon Sep 17 00:00:00 2001 From: dmtrTm <13824405+dmtrTm@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:39:46 +0700 Subject: [PATCH] fix: resolve environment variables on application rollback rollbackApplication() called prepareEnvironmentVariables() without the environment env, so every ${{environment.*}} reference threw "Invalid environment variable: environment.X" and the rollback failed with an opaque 400 before the swarm service was updated. fb749cd86 added the third argument to every other call site but left services/rollbacks.ts, which had been calling the helper since 24bff9689. The value is already captured in the snapshot by createRollback(); both fullContext declarations simply typed environment as { project: Project }, so the missing argument was invisible to the compiler. Co-Authored-By: Claude Opus 5 --- .../__test__/env/rollback-environment.test.ts | 108 ++++++++++++++++++ packages/server/src/db/schema/rollbacks.ts | 3 +- packages/server/src/services/rollbacks.ts | 4 +- 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 apps/dokploy/__test__/env/rollback-environment.test.ts diff --git a/apps/dokploy/__test__/env/rollback-environment.test.ts b/apps/dokploy/__test__/env/rollback-environment.test.ts new file mode 100644 index 0000000000..7d71059e05 --- /dev/null +++ b/apps/dokploy/__test__/env/rollback-environment.test.ts @@ -0,0 +1,108 @@ +import { prepareEnvironmentVariables } from "@dokploy/server/index"; +import { describe, expect, it } from "vitest"; + +const projectEnv = ` +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db +`; + +const environmentEnv = ` +NODE_ENV=production +POSTGRES_HOST=postgres.internal +POSTGRES_PORT=5432 +REDIS_URL=redis://redis.internal:6379 +`; + +const serviceEnv = ` +NODE_ENV=\${{environment.NODE_ENV}} +REDIS_URL=\${{environment.REDIS_URL}} +PORT=3000 +`; + +/** + * A rollback replays the snapshot stored in `rollbacks.fullContext`, which keeps + * the service env, the environment env and the project env captured at deploy time. + */ +const fullContext = { + env: serviceEnv, + environment: { + env: environmentEnv, + project: { + env: projectEnv, + }, + }, +}; + +describe("prepareEnvironmentVariables for application rollback", () => { + it("resolves environment variables from the rollback snapshot", () => { + const result = prepareEnvironmentVariables( + fullContext.env, + fullContext.environment.project.env, + fullContext.environment.env, + ); + + expect(result).toEqual([ + "NODE_ENV=production", + "REDIS_URL=redis://redis.internal:6379", + "PORT=3000", + ]); + }); + + it("resolves project and environment variables together on rollback", () => { + const rollbackEnv = ` +DATABASE_URL=\${{project.DATABASE_URL}} +POSTGRES_URL=postgres://\${{environment.POSTGRES_HOST}}:\${{environment.POSTGRES_PORT}}/app +ENVIRONMENT=\${{project.ENVIRONMENT}} +`; + + const result = prepareEnvironmentVariables( + rollbackEnv, + projectEnv, + environmentEnv, + ); + + expect(result).toEqual([ + "DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db", + "POSTGRES_URL=postgres://postgres.internal:5432/app", + "ENVIRONMENT=staging", + ]); + }); + + it("throws when the environment env of the snapshot is not passed", () => { + expect(() => + prepareEnvironmentVariables(fullContext.env, projectEnv), + ).toThrow("Invalid environment variable: environment.NODE_ENV"); + }); + + it("maintains precedence: service > environment > project on rollback", () => { + const conflictingProjectEnv = ` +NODE_ENV=project +API_URL=https://project.api.com +`; + + const conflictingEnvironmentEnv = ` +NODE_ENV=environment +API_URL=https://environment.api.com +`; + + const rollbackEnv = ` +NODE_ENV=service +PROJECT_API_URL=\${{project.API_URL}} +ENVIRONMENT_API_URL=\${{environment.API_URL}} +SELF_REFERENCE=\${{NODE_ENV}} +`; + + const result = prepareEnvironmentVariables( + rollbackEnv, + conflictingProjectEnv, + conflictingEnvironmentEnv, + ); + + expect(result).toEqual([ + "NODE_ENV=service", + "PROJECT_API_URL=https://project.api.com", + "ENVIRONMENT_API_URL=https://environment.api.com", + "SELF_REFERENCE=service", + ]); + }); +}); diff --git a/packages/server/src/db/schema/rollbacks.ts b/packages/server/src/db/schema/rollbacks.ts index ec27d683f8..5e3d407768 100644 --- a/packages/server/src/db/schema/rollbacks.ts +++ b/packages/server/src/db/schema/rollbacks.ts @@ -1,4 +1,5 @@ import type { Application } from "@dokploy/server/services/application"; +import type { Environment } from "@dokploy/server/services/environment"; import type { Mount } from "@dokploy/server/services/mount"; import type { Port } from "@dokploy/server/services/port"; import type { Project } from "@dokploy/server/services/project"; @@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", { .$defaultFn(() => new Date().toISOString()), fullContext: jsonb("fullContext").$type< Application & { - environment: { + environment: Environment & { project: Project; }; mounts: Mount[]; diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index b3f2fc5a40..38abaf68ac 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { getRemoteDocker } from "../utils/servers/remote-docker"; import { type Application, findApplicationById } from "./application"; import { findDeploymentById } from "./deployment"; +import type { Environment } from "./environment"; import type { Mount } from "./mount"; import { resolveServiceNetworks } from "./network"; import type { Port } from "./port"; @@ -214,7 +215,7 @@ const rollbackApplication = async ( image: string, serverId?: string | null, fullContext?: Application & { - environment: { + environment: Environment & { project: Project; }; mounts: Mount[]; @@ -279,6 +280,7 @@ const rollbackApplication = async ( const envVariables = prepareEnvironmentVariables( env, fullContext.environment.project.env, + fullContext.environment.env, ); let rollbackImage = image;