diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 89130799a..f5516d2a1 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -714,6 +714,12 @@ class All extends Component // Extract all hard-coded variables $hardcodedVars = extractHardcodedEnvironmentVariables($dockerComposeRaw); + // Compose self-references are inputs supplied through Coolify's .env file, + // not hard-coded values. Keep them editable in the environment variables UI. + $hardcodedVars = $hardcodedVars->reject( + fn (array $variable): bool => $this->isSelfReferencingComposeVariable($variable) + ); + // Filter out magic variables (SERVICE_FQDN_*, SERVICE_URL_*, SERVICE_NAME_*) $hardcodedVars = $hardcodedVars->filter(function ($var) { $key = $var['key']; @@ -755,7 +761,14 @@ class All extends Component return []; } - return extractHardcodedEnvironmentVariables($dockerComposeRaw) + $assignments = extractHardcodedEnvironmentVariables($dockerComposeRaw); + $editableKeys = $assignments + ->filter(fn (array $variable): bool => $this->isSelfReferencingComposeVariable($variable)) + ->pluck('key') + ->unique(); + + return $assignments + ->reject(fn (array $variable): bool => $editableKeys->contains($variable['key'])) ->pluck('key') ->reject(fn (string $key): bool => str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_'])) ->unique() @@ -763,6 +776,28 @@ class All extends Component ->all(); } + private function isSelfReferencingComposeVariable(array $variable): bool + { + $value = $variable['value'] ?? null; + if (! is_string($value)) { + return false; + } + + if ($value === '$'.$variable['key']) { + return true; + } + + $reference = extractBalancedBraceContent($value); + if ($reference === null || $reference['start'] !== 1 || $reference['end'] !== strlen($value) - 1) { + return false; + } + + $splitReference = splitOnOperatorOutsideNested($reference['content']); + $referencedKey = $splitReference['variable'] ?? $reference['content']; + + return $referencedKey === $variable['key']; + } + public function getDevView() { $this->variables = $this->formatEnvironmentVariables($this->getEnvironmentVariables(false, false)); diff --git a/tests/Feature/EnvironmentVariableSearchTest.php b/tests/Feature/EnvironmentVariableSearchTest.php index a44124ea7..5ce4c09c4 100644 --- a/tests/Feature/EnvironmentVariableSearchTest.php +++ b/tests/Feature/EnvironmentVariableSearchTest.php @@ -182,6 +182,42 @@ YAML, ->toBe(['API_TOKEN']); }); +it('keeps Compose self-referencing environment variables editable without changing Compose', function () { + $dockerCompose = <<<'YAML' +services: + app: + image: nginx + environment: + API_TOKEN: ${API_TOKEN} + LOG_LEVEL: ${LOG_LEVEL:-info} + FIXED_VALUE: production +YAML; + + $service = Service::factory()->create([ + 'environment_id' => $this->environment->id, + 'docker_compose_raw' => $dockerCompose, + ]); + + foreach (['API_TOKEN', 'LOG_LEVEL'] as $key) { + EnvironmentVariable::create([ + 'key' => $key, + 'resourceable_type' => Service::class, + 'resourceable_id' => $service->id, + ]); + } + + $rows = Livewire::test(All::class, ['resource' => $service]) + ->call('loadEnvironmentVariables') + ->instance() + ->environmentVariablePageRows; + + expect($rows->where('kind', 'managed')->pluck('environmentVariable.key')->all()) + ->toBe(['API_TOKEN', 'LOG_LEVEL']) + ->and($rows->where('kind', 'hardcoded')->pluck('environmentVariable.key')->all()) + ->toBe(['FIXED_VALUE']) + ->and($service->fresh()->docker_compose_raw)->toBe($dockerCompose); +}); + it('shows a Compose-defined value as read-only when a managed variable has the same key', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id,