fix: handle map-style environment variables in updateCompose

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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-11-21 11:21:35 +01:00
parent 56f32d0f87
commit a5ce1db871
2 changed files with 172 additions and 3 deletions

View file

@ -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

View file

@ -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');
});