fix(env): keep quoted values when saving the developer view

Closes #10406
This commit is contained in:
Andras Bacsai
2026-10-27 15:24:08 +01:00
parent adcb182a8a
commit 90e7533a65
8 changed files with 160 additions and 10 deletions
@@ -1001,8 +1001,12 @@ class All extends Component
if ($item->is_multiline) {
return "$item->key=(Multiline environment variable, edit in normal view)";
}
$value = formatEnvValueForDevView($item->value);
if ($value === null) {
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
}
return "$item->key=$item->value";
return "$item->key=$value";
})->join("\n");
}
@@ -1224,7 +1228,7 @@ class All extends Component
$found = $this->resource->$method()->where('key', $key)->first();
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
$changed = false;
// Update value if it changed
@@ -93,8 +93,12 @@ class Show extends Component
if ($item->is_multiline) {
return "$item->key=(Multiline environment variable, edit in normal view)";
}
$value = formatEnvValueForDevView($item->value);
if ($value === null) {
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
}
return "$item->key=$item->value";
return "$item->key=$value";
})->join("\n");
}
@@ -158,7 +162,7 @@ class Show extends Component
$found = $this->environment->environment_variables()->where('key', $key)->first();
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
if ($found->value !== $value) {
$found->value = $value;
$found->save();
@@ -91,8 +91,12 @@ class Show extends Component
if ($item->is_multiline) {
return "$item->key=(Multiline environment variable, edit in normal view)";
}
$value = formatEnvValueForDevView($item->value);
if ($value === null) {
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
}
return "$item->key=$item->value";
return "$item->key=$value";
})->join("\n");
}
@@ -150,7 +154,7 @@ class Show extends Component
$found = $this->project->environment_variables()->where('key', $key)->first();
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
if ($found->value !== $value) {
$found->value = $value;
$found->save();
+6 -2
View File
@@ -92,8 +92,12 @@ class Show extends Component
if ($item->is_multiline) {
return "$item->key=(Multiline environment variable, edit in normal view)";
}
$value = formatEnvValueForDevView($item->value);
if ($value === null) {
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
}
return "$item->key=$item->value";
return "$item->key=$value";
})->join("\n");
}
@@ -162,7 +166,7 @@ class Show extends Component
$found = $this->server->environment_variables()->where('key', $key)->first();
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
if ($found->value !== $value || $found->comment !== $comment) {
$found->value = $value;
$found->comment = $comment;
+6 -2
View File
@@ -85,8 +85,12 @@ class Index extends Component
if ($item->is_multiline) {
return "$item->key=(Multiline environment variable, edit in normal view)";
}
$value = formatEnvValueForDevView($item->value);
if ($value === null) {
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
}
return "$item->key=$item->value";
return "$item->key=$value";
})->join("\n");
}
@@ -149,7 +153,7 @@ class Index extends Component
$found = $this->team->environment_variables()->where('key', $key)->first();
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
if ($found->value !== $value) {
$found->value = $value;
$found->save();
+26
View File
@@ -1258,6 +1258,32 @@ function parseEnvFormatToArray($env_file_contents)
return $env_array;
}
/**
* Format a value for a KEY=VALUE line of the developer view, so that parseEnvFormatToArray()
* reads it back unchanged. Returns null when the format cannot hold the value: it has a line
* break, or it needs quotes and has both quote types.
*/
function formatEnvValueForDevView(?string $value): ?string
{
$value ??= '';
if (str_contains($value, "\n")) {
return null;
}
$needsQuotes = str_starts_with($value, '"') || str_starts_with($value, "'") || preg_match('/\s#/', $value) === 1;
if (! $needsQuotes) {
return $value;
}
if (! str_contains($value, "'")) {
return "'{$value}'";
}
if (! str_contains($value, '"')) {
return "\"{$value}\"";
}
return null;
}
/**
* Extract inline comments from environment variables in raw docker-compose YAML.
*
@@ -0,0 +1,79 @@
<?php
use App\Livewire\Project\Shared\EnvironmentVariable\All;
use App\Livewire\SharedVariables\Team\Index;
use App\Models\Application;
use App\Models\Environment;
use App\Models\EnvironmentVariable;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\SharedEnvironmentVariable;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 0]);
$this->user = User::factory()->create();
$this->team = Team::factory()->create();
$this->team->members()->attach($this->user, ['role' => 'owner']);
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$this->application = Application::factory()->create(['environment_id' => $this->environment->id]);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
});
$values = [
'QUOTED' => '"abc"',
'SINGLE_QUOTED' => "'\$FOO'",
'HASH' => 'value #not-a-comment',
'BOTH_QUOTES' => '\'"Company :: App" <noreply@example.com>\'',
];
it('keeps values unchanged when the developer view is saved without edits', function () use ($values) {
foreach ($values as $key => $value) {
EnvironmentVariable::create([
'key' => $key,
'value' => $value,
'is_literal' => true,
'resourceable_type' => Application::class,
'resourceable_id' => $this->application->id,
]);
}
$component = Livewire::test(All::class, ['resource' => $this->application])
->call('loadEnvironmentVariables')
->call('switch');
$component->call('submit')->assertHasNoErrors();
$component->call('submit')->assertHasNoErrors();
expect($this->application->environment_variables()->whereIn('key', array_keys($values))->get()->pluck('value', 'key')->all())->toBe($values)
->and($this->application->environment_variables()->whereIn('key', array_keys($values))->whereNotNull('comment')->count())->toBe(0)
->and($component->get('variables'))->toContain("QUOTED='\"abc\"'")
->and($component->get('variables'))->toContain('BOTH_QUOTES=(Value cannot be shown in this view, edit in normal view)');
});
it('keeps shared variable values unchanged when the developer view is saved without edits', function () use ($values) {
foreach ($values as $key => $value) {
SharedEnvironmentVariable::create([
'key' => $key,
'value' => $value,
'type' => 'team',
'team_id' => $this->team->id,
]);
}
$component = Livewire::test(Index::class)->call('switch');
$component->call('submit')->assertHasNoErrors();
$component->call('submit')->assertHasNoErrors();
expect($this->team->environment_variables()->get()->pluck('value', 'key')->all())->toBe($values);
});
@@ -0,0 +1,25 @@
<?php
it('formats values so that parseEnvFormatToArray reads them back unchanged', function (string $value) {
$formatted = formatEnvValueForDevView($value);
expect(parseEnvFormatToArray("KEY={$formatted}"))
->toBe(['KEY' => ['value' => $value, 'comment' => null]]);
})->with([
'plain' => ['plain-value'],
'empty' => [''],
'double quoted' => ['"abc"'],
'single quoted' => ["'\$FOO'"],
'leading double quote only' => ['"abc'],
'quotes inside' => ['He said "hi" and \'bye\''],
'hash after whitespace' => ['value #not-a-comment'],
'hash without whitespace' => ['value#hash'],
'double quoted with suffix' => ['"Company :: App" <noreply@example.com>'],
]);
it('returns null for values that the env format cannot hold', function (string $value) {
expect(formatEnvValueForDevView($value))->toBeNull();
})->with([
'both quote types and a leading quote' => ['\'"Company :: App" <noreply@example.com>\''],
'line break' => ["first\nsecond"],
]);