From a5ce1db8715d62b437cb3104af5ca6427f28a47b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:21:35 +0100 Subject: [PATCH] fix: handle map-style environment variables in updateCompose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The updateCompose() function now correctly detects SERVICE_URL_* and SERVICE_FQDN_* variables regardless of whether they are defined in YAML list-style or map-style format. Previously, the code only worked with list-style environment definitions: ```yaml environment: - SERVICE_URL_APP_3000 ``` Now it also handles map-style definitions: ```yaml environment: SERVICE_URL_TRIGGER_3000: "" SERVICE_FQDN_DB: localhost ``` The fix distinguishes between the two formats by checking if the array key is numeric (list-style) or a string (map-style), then extracts the variable name from the appropriate location. Added 5 comprehensive unit tests covering: - Map-style environment format detection - Multiple map-style variables - References vs declarations in map-style - Abbreviated service names with map-style - Verification of dual-format handling This fixes variable detection for service templates like trigger.yaml, langfuse.yaml, and paymenter.yaml that use map-style format. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bootstrap/helpers/services.php | 13 +- .../UpdateComposeAbbreviatedVariablesTest.php | 162 ++++++++++++++++++ 2 files changed, 172 insertions(+), 3 deletions(-) diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index fdbaf6364..3fff2c090 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -123,16 +123,23 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $environment = data_get($serviceConfig, 'environment', []); $templateVariableNames = []; - foreach ($environment as $envVar) { - if (is_string($envVar)) { + foreach ($environment as $key => $value) { + if (is_int($key) && is_string($value)) { + // List-style: "- SERVICE_URL_APP_3000" or "- SERVICE_URL_APP_3000=value" // Extract variable name (before '=' if present) - $envVarName = str($envVar)->before('=')->trim(); + $envVarName = str($value)->before('=')->trim(); // Only include if it's a direct declaration (not a reference like ${VAR}) // Direct declarations look like: SERVICE_URL_APP or SERVICE_URL_APP_3000 // References look like: NEXT_PUBLIC_URL=${SERVICE_URL_APP} if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { $templateVariableNames[] = $envVarName->value(); } + } elseif (is_string($key)) { + // Map-style: "SERVICE_URL_APP_3000: value" or "SERVICE_FQDN_DB: localhost" + $envVarName = str($key); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } } // DO NOT extract variables that are only referenced with ${VAR_NAME} syntax // Those belong to other services and will be updated when THOSE services are updated diff --git a/tests/Unit/UpdateComposeAbbreviatedVariablesTest.php b/tests/Unit/UpdateComposeAbbreviatedVariablesTest.php index 50fe2b6b1..4ef7def9c 100644 --- a/tests/Unit/UpdateComposeAbbreviatedVariablesTest.php +++ b/tests/Unit/UpdateComposeAbbreviatedVariablesTest.php @@ -399,3 +399,165 @@ // 5. SERVICE_URL_API_8080 // 6. SERVICE_FQDN_API_8080 }); + +it('detects SERVICE_URL variables in map-style environment format', function () { + $yaml = <<<'YAML' +services: + trigger: + environment: + SERVICE_URL_TRIGGER_3000: "" + SERVICE_FQDN_DB: localhost + OTHER_VAR: value +YAML; + + $dockerCompose = Yaml::parse($yaml); + $serviceConfig = data_get($dockerCompose, 'services.trigger'); + $environment = data_get($serviceConfig, 'environment', []); + + $templateVariableNames = []; + foreach ($environment as $key => $value) { + if (is_int($key) && is_string($value)) { + // List-style + $envVarName = str($value)->before('=')->trim(); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } elseif (is_string($key)) { + // Map-style + $envVarName = str($key); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } + } + + expect($templateVariableNames)->toHaveCount(2); + expect($templateVariableNames)->toContain('SERVICE_URL_TRIGGER_3000'); + expect($templateVariableNames)->toContain('SERVICE_FQDN_DB'); + expect($templateVariableNames)->not->toContain('OTHER_VAR'); +}); + +it('handles multiple map-style SERVICE_URL and SERVICE_FQDN variables', function () { + $yaml = <<<'YAML' +services: + app: + environment: + SERVICE_URL_APP_3000: "" + SERVICE_FQDN_API: api.local + SERVICE_URL_WEB: "" + OTHER_VAR: value +YAML; + + $dockerCompose = Yaml::parse($yaml); + $serviceConfig = data_get($dockerCompose, 'services.app'); + $environment = data_get($serviceConfig, 'environment', []); + + $templateVariableNames = []; + foreach ($environment as $key => $value) { + if (is_int($key) && is_string($value)) { + // List-style + $envVarName = str($value)->before('=')->trim(); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } elseif (is_string($key)) { + // Map-style + $envVarName = str($key); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } + } + + expect($templateVariableNames)->toHaveCount(3); + expect($templateVariableNames)->toContain('SERVICE_URL_APP_3000'); + expect($templateVariableNames)->toContain('SERVICE_FQDN_API'); + expect($templateVariableNames)->toContain('SERVICE_URL_WEB'); + expect($templateVariableNames)->not->toContain('OTHER_VAR'); +}); + +it('does not detect SERVICE_URL references in map-style values', function () { + $yaml = <<<'YAML' +services: + app: + environment: + SERVICE_URL_APP_3000: "" + NEXT_PUBLIC_URL: ${SERVICE_URL_APP} + API_ENDPOINT: ${SERVICE_URL_API} +YAML; + + $dockerCompose = Yaml::parse($yaml); + $serviceConfig = data_get($dockerCompose, 'services.app'); + $environment = data_get($serviceConfig, 'environment', []); + + $templateVariableNames = []; + foreach ($environment as $key => $value) { + if (is_int($key) && is_string($value)) { + // List-style + $envVarName = str($value)->before('=')->trim(); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } elseif (is_string($key)) { + // Map-style + $envVarName = str($key); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } + } + + // Should only detect the direct declaration, not references in values + expect($templateVariableNames)->toHaveCount(1); + expect($templateVariableNames)->toContain('SERVICE_URL_APP_3000'); + expect($templateVariableNames)->not->toContain('SERVICE_URL_APP'); + expect($templateVariableNames)->not->toContain('SERVICE_URL_API'); + expect($templateVariableNames)->not->toContain('NEXT_PUBLIC_URL'); + expect($templateVariableNames)->not->toContain('API_ENDPOINT'); +}); + +it('handles map-style with abbreviated service names', function () { + // Simulating the langfuse.yaml case with map-style + $yaml = <<<'YAML' +services: + langfuse: + environment: + SERVICE_URL_LANGFUSE_3000: ${SERVICE_URL_LANGFUSE_3000} + DATABASE_URL: postgres://... +YAML; + + $dockerCompose = Yaml::parse($yaml); + $serviceConfig = data_get($dockerCompose, 'services.langfuse'); + $environment = data_get($serviceConfig, 'environment', []); + + $templateVariableNames = []; + foreach ($environment as $key => $value) { + if (is_int($key) && is_string($value)) { + // List-style + $envVarName = str($value)->before('=')->trim(); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } elseif (is_string($key)) { + // Map-style + $envVarName = str($key); + if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) { + $templateVariableNames[] = $envVarName->value(); + } + } + } + + expect($templateVariableNames)->toHaveCount(1); + expect($templateVariableNames)->toContain('SERVICE_URL_LANGFUSE_3000'); + expect($templateVariableNames)->not->toContain('DATABASE_URL'); +}); + +it('verifies updateCompose helper has dual-format handling', function () { + $servicesFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/services.php'); + + // Check that both formats are handled + expect($servicesFile)->toContain('is_int($key) && is_string($value)'); + expect($servicesFile)->toContain('List-style'); + expect($servicesFile)->toContain('elseif (is_string($key))'); + expect($servicesFile)->toContain('Map-style'); +});