From 5ff21103974a30e107ce046190fb478658a214e7 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 4 Sep 2026 17:01:42 +0200
Subject: [PATCH] fix(environment-variables): preserve generated Compose
variables
Exclude protected Compose variables from bulk deletion and developer views, and propagate deletion failures correctly.
---
.../Shared/EnvironmentVariable/All.php | 42 +++++++-----
.../EnvironmentVariableAsyncLoadTest.php | 66 +++++++++++++++++++
2 files changed, 90 insertions(+), 18 deletions(-)
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index ea8394c1b1..47080cd86b 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -818,19 +818,21 @@ class All extends Component
{
$isMember = auth()->user()?->isMember();
- return $variables->map(function ($item) use ($isMember) {
- if ($isMember) {
- return "$item->key=(Hidden, only admins can view)";
- }
- if ($item->is_shown_once) {
- return "$item->key=(Locked Secret, delete and add again to change)";
- }
- if ($item->is_multiline) {
- return "$item->key=(Multiline environment variable, edit in normal view)";
- }
+ return $variables
+ ->reject(fn ($item): bool => $this->isProtectedEnvironmentVariable($item->key))
+ ->map(function ($item) use ($isMember) {
+ if ($isMember) {
+ return "$item->key=(Hidden, only admins can view)";
+ }
+ if ($item->is_shown_once) {
+ return "$item->key=(Locked Secret, delete and add again to change)";
+ }
+ if ($item->is_multiline) {
+ return "$item->key=(Multiline environment variable, edit in normal view)";
+ }
- return "$item->key=$item->value";
- })->join("\n");
+ return "$item->key=$item->value";
+ })->join("\n");
}
public function switch()
@@ -908,8 +910,7 @@ class All extends Component
$deletedCount = $this->deleteRemovedVariables(false, $variables);
if ($deletedCount > 0) {
$changesMade = true;
- } elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) {
- // If we tried to delete but couldn't (due to Docker Compose), mark as error
+ } elseif ($deletedCount < 0) {
$errorOccurred = true;
}
@@ -926,8 +927,7 @@ class All extends Component
$deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
if ($deletedPreviewCount > 0) {
$changesMade = true;
- } elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) {
- // If we tried to delete but couldn't (due to Docker Compose), mark as error
+ } elseif ($deletedPreviewCount < 0) {
$errorOccurred = true;
}
@@ -988,6 +988,12 @@ class All extends Component
// Get all environment variables that will be deleted
$variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get();
+ // Generated Compose variables are managed by Coolify and must survive a bulk
+ // replacement even when they are omitted from the pasted environment file.
+ $variablesToDelete = $variablesToDelete->reject(
+ fn (EnvironmentVariable $environmentVariable): bool => $this->isProtectedEnvironmentVariable($environmentVariable->key)
+ );
+
// If there are no variables to delete, return 0
if ($variablesToDelete->isEmpty()) {
return 0;
@@ -1001,13 +1007,13 @@ class All extends Component
if ($isUsed) {
$this->dispatch('error', "Cannot delete environment variable '{$envVar->key}'
Please remove it from the Docker Compose file first.");
- return 0;
+ return -1;
}
}
}
// If we get here, no variables are used in Docker Compose, so we can delete them
- $this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
+ $this->resource->$method()->whereKey($variablesToDelete->modelKeys())->delete();
return $variablesToDelete->count();
}
diff --git a/tests/Feature/EnvironmentVariableAsyncLoadTest.php b/tests/Feature/EnvironmentVariableAsyncLoadTest.php
index 2cbc5537a1..4d83862b6e 100644
--- a/tests/Feature/EnvironmentVariableAsyncLoadTest.php
+++ b/tests/Feature/EnvironmentVariableAsyncLoadTest.php
@@ -121,3 +121,69 @@ it('is idempotent when loadEnvironmentVariables is called twice', function () {
expect($component->instance()->environmentVariables->pluck('key')->all())
->toContain('API_KEY');
});
+
+it('preserves generated compose variables during bulk replacement', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'build_pack' => 'dockercompose',
+ 'docker_compose' => <<<'YAML'
+services:
+ api:
+ image: nginx:alpine
+ environment:
+ SERVICE_URL_API: /api
+YAML,
+ ]);
+
+ foreach ([
+ 'SERVICE_URL_API' => 'https://api.example.com/api',
+ 'SERVICE_FQDN_API' => 'api.example.com/api',
+ 'OLD_VARIABLE' => 'remove-me',
+ ] as $key => $value) {
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $value,
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $application->id,
+ ]);
+ }
+
+ Livewire::test(All::class, ['resource' => $application])
+ ->set('variables', 'NEW_VARIABLE=keep-me')
+ ->call('submit')
+ ->assertDispatched('success')
+ ->assertNotDispatched('error');
+
+ expect($application->environment_variables()->pluck('value', 'key')->all())
+ ->toBe([
+ 'SERVICE_URL_API' => 'https://api.example.com/api',
+ 'SERVICE_FQDN_API' => 'api.example.com/api',
+ 'NEW_VARIABLE' => 'keep-me',
+ ]);
+});
+
+it('hides generated compose variables from developer view', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'build_pack' => 'dockercompose',
+ ]);
+
+ foreach ([
+ 'SERVICE_URL_API' => 'https://api.example.com',
+ 'SERVICE_FQDN_API' => 'api.example.com',
+ 'API_URL' => '$SERVICE_URL_API',
+ ] as $key => $value) {
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $value,
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $application->id,
+ ]);
+ }
+
+ $component = Livewire::test(All::class, ['resource' => $application])
+ ->call('switch');
+
+ expect($component->get('variables'))
+ ->toBe('API_URL=$SERVICE_URL_API');
+});