fix(env): quote old multi-line JSON values in the .env file

Variables saved before exact escaping wrote JSON objects and arrays
without quotes. A multi-line JSON value broke the .env file, so Docker
Compose stopped the deployment. These values now use exact escaping,
which gives the container the same value as before beta.463.

Closes #8279

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-11-03 22:34:42 +01:00
co-authored by Claude Opus 5.5
parent a7b7aa350a
commit 0b45d8dd6c
3 changed files with 39 additions and 7 deletions
+10 -7
View File
@@ -172,20 +172,23 @@ trait HasSecretManager
}
/**
* The old escaping puts literal and multiline values in single quotes without escaping. Docker
* Compose ends the value at the next quote, so a value with a quote breaks the whole .env file.
* The old escaping writes JSON objects and arrays without quotes, so a JSON value with a line
* break breaks the whole .env file (#8279). It puts other literal and multiline values in single
* quotes without escaping, and Docker Compose ends the value at the next quote.
*/
private function legacyEnvFileLineIsUnreadable(EnvironmentVariable $environmentVariable): bool
{
if (! $environmentVariable->is_literal && ! $environmentVariable->is_multiline) {
$value = $this->resolvedEnvironmentVariableValue($environmentVariable);
if (! is_string($value)) {
return false;
}
$value = $this->resolvedEnvironmentVariableValue($environmentVariable);
if (json_validate($value) && (str_starts_with($value, '{') || str_starts_with($value, '['))) {
return str_contains($value, "\n");
}
return is_string($value)
&& str_contains($value, "'")
&& ! (json_validate($value) && (str_starts_with($value, '{') || str_starts_with($value, '[')));
return ($environmentVariable->is_literal || $environmentVariable->is_multiline) && str_contains($value, "'");
}
/**
@@ -1936,6 +1936,22 @@ it('writes a readable runtime env line for legacy literal values with a single q
expect($runtimeEnvs)->toContain('LEGACY_QUOTE="it\'s"');
});
it('writes a readable runtime env line for legacy multiline JSON values', function () {
[$application, $server] = makeDeploymentControlVarFixture();
$json = "{\n \"type\": \"service_account\"\n}";
$variable = createApplicationEnvironmentVariable($application, ['key' => 'GOOGLE_CREDENTIALS_JSON', 'value' => $json, 'is_multiline' => true, 'is_runtime' => true, 'is_buildtime' => false]);
$variable->uses_legacy_escaping = true;
$variable->save();
[$job, $reflection] = makeControlVarFilteringJob($application->fresh(), $server);
/** @var Collection $runtimeEnvs */
$runtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
expect($runtimeEnvs)->toContain('GOOGLE_CREDENTIALS_JSON='.escapeComposeEnvFileValue($json));
});
it('keeps the old runtime env format for preview variables saved before exact escaping', function () {
[$application, $server] = makeDeploymentControlVarFixture();
@@ -129,6 +129,19 @@ test('legacy literal and multiline variables with a single quote get a line that
->and($this->service->legacyEscapingChangesValue($literal))->toBeFalse();
});
test('legacy multiline json variables get a line that docker compose can read', function (array $flags) {
$json = "{\n \"type\": \"service_account\"\n}";
$multiline = legacyVariable($this->service, ['key' => 'GOOGLE_CREDENTIALS_JSON', 'value' => $json, ...$flags]);
$singleLine = legacyVariable($this->service, ['key' => 'SINGLE_LINE_JSON', 'value' => '{"a":"b"}', ...$flags]);
expect($this->service->composeEnvironmentFileLine($multiline))->toBe("GOOGLE_CREDENTIALS_JSON=\"{\n \\\"type\\\": \\\"service_account\\\"\n}\"")
->and($this->service->composeEnvironmentFileLine($singleLine))->toBe('SINGLE_LINE_JSON={"a":"b"}')
->and($this->service->legacyEscapingChangesValue($multiline))->toBeFalse();
})->with([
'multiline' => [['is_multiline' => true]],
'plain' => [[]],
]);
test('legacy database variables keep the old compose environment format', function () {
$database = StandalonePostgresql::forceCreate([
'uuid' => 'postgres-legacy-test',