diff --git a/app/Traits/HasSecretManager.php b/app/Traits/HasSecretManager.php index 621a29ca01..35a9c8da47 100644 --- a/app/Traits/HasSecretManager.php +++ b/app/Traits/HasSecretManager.php @@ -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, "'"); } /** diff --git a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php index a05610f9c6..6e906ba30d 100644 --- a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php +++ b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php @@ -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(); diff --git a/tests/Feature/EnvironmentVariableLegacyEscapingTest.php b/tests/Feature/EnvironmentVariableLegacyEscapingTest.php index 6823dfc7b5..23930493b4 100644 --- a/tests/Feature/EnvironmentVariableLegacyEscapingTest.php +++ b/tests/Feature/EnvironmentVariableLegacyEscapingTest.php @@ -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',