Merge remote-tracking branch 'origin/next' into next
This commit is contained in:
commit
9f13f25149
8 changed files with 439 additions and 62 deletions
|
|
@ -1333,6 +1333,22 @@ private function generate_runtime_environment_variables()
|
|||
foreach ($runtime_environment_variables_preview as $env) {
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
}
|
||||
|
||||
// Fall back to production env vars for keys not overridden by preview vars,
|
||||
// but only when preview vars are configured. This ensures variables like
|
||||
// DB_PASSWORD that are only set for production will be available in the
|
||||
// preview .env file (fixing ${VAR} interpolation in docker-compose YAML),
|
||||
// while avoiding leaking production values when previews aren't configured.
|
||||
if ($runtime_environment_variables_preview->isNotEmpty()) {
|
||||
$previewKeys = $runtime_environment_variables_preview->pluck('key')->toArray();
|
||||
$fallback_production_vars = $sorted_environment_variables->filter(function ($env) use ($previewKeys) {
|
||||
return $env->is_runtime && ! in_array($env->key, $previewKeys);
|
||||
});
|
||||
foreach ($fallback_production_vars as $env) {
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($this->build_pack !== 'dockercompose') {
|
||||
if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -399,7 +399,15 @@ public function handle(): void
|
|||
's3_uploaded' => null,
|
||||
]);
|
||||
}
|
||||
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->error_output ?? $this->backup_output ?? $e->getMessage(), $database));
|
||||
try {
|
||||
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->error_output ?? $this->backup_output ?? $e->getMessage(), $database));
|
||||
} catch (\Throwable $notifyException) {
|
||||
Log::channel('scheduled-errors')->warning('Failed to send backup failure notification', [
|
||||
'backup_id' => $this->backup->uuid,
|
||||
'database' => $database,
|
||||
'error' => $notifyException->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
|
@ -439,11 +447,20 @@ public function handle(): void
|
|||
'local_storage_deleted' => $localStorageDeleted,
|
||||
]);
|
||||
|
||||
// Send appropriate notification
|
||||
if ($s3UploadError) {
|
||||
$this->team->notify(new BackupSuccessWithS3Warning($this->backup, $this->database, $database, $s3UploadError));
|
||||
} else {
|
||||
$this->team->notify(new BackupSuccess($this->backup, $this->database, $database));
|
||||
// Send appropriate notification (wrapped in try-catch so notification
|
||||
// failures never affect backup status — see GitHub issue #9088)
|
||||
try {
|
||||
if ($s3UploadError) {
|
||||
$this->team->notify(new BackupSuccessWithS3Warning($this->backup, $this->database, $database, $s3UploadError));
|
||||
} else {
|
||||
$this->team->notify(new BackupSuccess($this->backup, $this->database, $database));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::channel('scheduled-errors')->warning('Failed to send backup success notification', [
|
||||
'backup_id' => $this->backup->uuid,
|
||||
'database' => $database,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -710,20 +727,32 @@ public function failed(?Throwable $exception): void
|
|||
$log = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->first();
|
||||
|
||||
if ($log) {
|
||||
$log->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Job permanently failed after '.$this->attempts().' attempts: '.($exception?->getMessage() ?? 'Unknown error'),
|
||||
'size' => 0,
|
||||
'filename' => null,
|
||||
'finished_at' => Carbon::now(),
|
||||
]);
|
||||
// Don't overwrite a successful backup status — a post-backup error
|
||||
// (e.g. notification failure) should not retroactively mark the backup
|
||||
// as failed (see GitHub issue #9088)
|
||||
if ($log->status !== 'success') {
|
||||
$log->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Job permanently failed after '.$this->attempts().' attempts: '.($exception?->getMessage() ?? 'Unknown error'),
|
||||
'size' => 0,
|
||||
'filename' => null,
|
||||
'finished_at' => Carbon::now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify team about permanent failure
|
||||
if ($this->team) {
|
||||
// Notify team about permanent failure (only if backup didn't already succeed)
|
||||
if ($this->team && $log?->status !== 'success') {
|
||||
$databaseName = $log?->database_name ?? 'unknown';
|
||||
$output = $this->backup_output ?? $exception?->getMessage() ?? 'Unknown error';
|
||||
$this->team->notify(new BackupFailed($this->backup, $this->database, $output, $databaseName));
|
||||
try {
|
||||
$this->team->notify(new BackupFailed($this->backup, $this->database, $output, $databaseName));
|
||||
} catch (\Throwable $e) {
|
||||
Log::channel('scheduled-errors')->warning('Failed to send backup permanent failure notification', [
|
||||
'backup_id' => $this->backup->uuid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@
|
|||
)]
|
||||
class EnvironmentVariable extends BaseModel
|
||||
{
|
||||
protected $attributes = [
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => true,
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
// Core identification
|
||||
'key',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
</x-slot>
|
||||
@if (
|
||||
(data_get($application, 'fqdn') ||
|
||||
collect(json_decode($this->application->docker_compose_domains))->count() > 0 ||
|
||||
collect(json_decode($this->application->docker_compose_domains))->contains(fn($fqdn) => !empty(data_get($fqdn, 'domain'))) ||
|
||||
data_get($application, 'previews', collect([]))->count() > 0 ||
|
||||
data_get($application, 'ports_mappings_array')) &&
|
||||
data_get($application, 'settings.is_raw_compose_deployment_enabled') !== true)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,13 @@
|
|||
!is_null($parsedServices) &&
|
||||
count($parsedServices) > 0 &&
|
||||
!$application->settings->is_raw_compose_deployment_enabled)
|
||||
<h3 class="pt-6">Domains</h3>
|
||||
@php
|
||||
$hasNonDatabaseService = collect(data_get($parsedServices, 'services', []))
|
||||
->contains(fn($service) => !isDatabaseImage(data_get($service, 'image')));
|
||||
@endphp
|
||||
@if ($hasNonDatabaseService)
|
||||
<h3 class="pt-6">Domains</h3>
|
||||
@endif
|
||||
@foreach (data_get($parsedServices, 'services') as $serviceName => $service)
|
||||
@if (!isDatabaseImage(data_get($service, 'image')))
|
||||
<div class="flex items-end gap-2">
|
||||
|
|
@ -86,18 +92,20 @@
|
|||
]" />
|
||||
@endcan
|
||||
@endif
|
||||
<div class="w-96 pb-6">
|
||||
@if ($application->could_set_build_commands())
|
||||
<x-forms.checkbox instantSave id="isStatic" label="Is it a static site?"
|
||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if ($isStatic && $buildPack !== 'static')
|
||||
<x-forms.checkbox label="Is it a SPA (Single Page Application)?"
|
||||
helper="If your application is a SPA, enable this." id="isSpa" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
@endif
|
||||
</div>
|
||||
@if ($application->could_set_build_commands() || ($isStatic && $buildPack !== 'static'))
|
||||
<div class="w-96 pb-6">
|
||||
@if ($application->could_set_build_commands())
|
||||
<x-forms.checkbox instantSave id="isStatic" label="Is it a static site?"
|
||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if ($isStatic && $buildPack !== 'static')
|
||||
<x-forms.checkbox label="Is it a SPA (Single Page Application)?"
|
||||
helper="If your application is a SPA, enable this." id="isSpa" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if ($buildPack !== 'dockercompose')
|
||||
<div class="flex items-end gap-2">
|
||||
@if ($application->settings->is_container_label_readonly_enabled == false)
|
||||
|
|
@ -209,7 +217,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="pt-6">
|
||||
<h3>Build</h3>
|
||||
@if ($application->build_pack === 'dockerimage')
|
||||
<x-forms.input
|
||||
|
|
|
|||
|
|
@ -8,39 +8,35 @@
|
|||
<livewire:project.application.heading :application="$resource" />
|
||||
<div>
|
||||
<h2>Logs</h2>
|
||||
@if (str($status)->contains('exited'))
|
||||
<div class="pt-4">The resource is not running.</div>
|
||||
@else
|
||||
<div class="pt-2" wire:loading wire:target="loadAllContainers">
|
||||
Loading containers...
|
||||
</div>
|
||||
<div x-init="$wire.loadAllContainers()" wire:loading.remove wire:target="loadAllContainers">
|
||||
@forelse ($servers as $server)
|
||||
<div class="py-2">
|
||||
<h4>Server: {{ $server->name }}</h4>
|
||||
@if ($server->isFunctional())
|
||||
@if (isset($serverContainers[$server->id]) && count($serverContainers[$server->id]) > 0)
|
||||
@php
|
||||
$totalContainers = collect($serverContainers)->flatten(1)->count();
|
||||
@endphp
|
||||
@foreach ($serverContainers[$server->id] as $container)
|
||||
<livewire:project.shared.get-logs
|
||||
wire:key="{{ data_get($container, 'ID', uniqid()) }}" :server="$server"
|
||||
:resource="$resource" :container="data_get($container, 'Names')"
|
||||
:expandByDefault="$totalContainers === 1" />
|
||||
@endforeach
|
||||
@else
|
||||
<div class="pt-2">No containers are running on server: {{ $server->name }}</div>
|
||||
@endif
|
||||
<div class="pt-2" wire:loading wire:target="loadAllContainers">
|
||||
Loading containers...
|
||||
</div>
|
||||
<div x-init="$wire.loadAllContainers()" wire:loading.remove wire:target="loadAllContainers">
|
||||
@forelse ($servers as $server)
|
||||
<div class="py-2">
|
||||
<h4>Server: {{ $server->name }}</h4>
|
||||
@if ($server->isFunctional())
|
||||
@if (isset($serverContainers[$server->id]) && count($serverContainers[$server->id]) > 0)
|
||||
@php
|
||||
$totalContainers = collect($serverContainers)->flatten(1)->count();
|
||||
@endphp
|
||||
@foreach ($serverContainers[$server->id] as $container)
|
||||
<livewire:project.shared.get-logs
|
||||
wire:key="{{ data_get($container, 'ID', uniqid()) }}" :server="$server"
|
||||
:resource="$resource" :container="data_get($container, 'Names')"
|
||||
:expandByDefault="$totalContainers === 1" />
|
||||
@endforeach
|
||||
@else
|
||||
<div class="pt-2">Server {{ $server->name }} is not functional.</div>
|
||||
<div class="pt-2">No containers are running on server: {{ $server->name }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div>No functional server found for the application.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="pt-2">Server {{ $server->name }} is not functional.</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div>No functional server found for the application.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@elseif ($type === 'database')
|
||||
<h1>Logs</h1>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,82 @@
|
|||
expect($unrelatedBackup->save_s3)->toBeTruthy();
|
||||
});
|
||||
|
||||
test('failed method does not overwrite successful backup status', function () {
|
||||
$team = Team::factory()->create();
|
||||
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'frequency' => '0 0 * * *',
|
||||
'save_s3' => false,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'database_id' => 1,
|
||||
'team_id' => $team->id,
|
||||
]);
|
||||
|
||||
$log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => 'test-uuid-success-guard',
|
||||
'database_name' => 'test_db',
|
||||
'filename' => '/backup/test.dmp',
|
||||
'scheduled_database_backup_id' => $backup->id,
|
||||
'status' => 'success',
|
||||
'message' => 'Backup completed successfully',
|
||||
'size' => 1024,
|
||||
]);
|
||||
|
||||
$job = new DatabaseBackupJob($backup);
|
||||
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$teamProp = $reflection->getProperty('team');
|
||||
$teamProp->setValue($job, $team);
|
||||
|
||||
$logUuidProp = $reflection->getProperty('backup_log_uuid');
|
||||
$logUuidProp->setValue($job, 'test-uuid-success-guard');
|
||||
|
||||
// Simulate a post-backup failure (e.g. notification error)
|
||||
$job->failed(new Exception('Request to the Resend API failed'));
|
||||
|
||||
$log->refresh();
|
||||
expect($log->status)->toBe('success');
|
||||
expect($log->message)->toBe('Backup completed successfully');
|
||||
expect($log->size)->toBe(1024);
|
||||
});
|
||||
|
||||
test('failed method updates status when backup was not successful', function () {
|
||||
$team = Team::factory()->create();
|
||||
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'frequency' => '0 0 * * *',
|
||||
'save_s3' => false,
|
||||
'database_type' => 'App\Models\StandalonePostgresql',
|
||||
'database_id' => 1,
|
||||
'team_id' => $team->id,
|
||||
]);
|
||||
|
||||
$log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => 'test-uuid-pending-guard',
|
||||
'database_name' => 'test_db',
|
||||
'filename' => '/backup/test.dmp',
|
||||
'scheduled_database_backup_id' => $backup->id,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$job = new DatabaseBackupJob($backup);
|
||||
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$teamProp = $reflection->getProperty('team');
|
||||
$teamProp->setValue($job, $team);
|
||||
|
||||
$logUuidProp = $reflection->getProperty('backup_log_uuid');
|
||||
$logUuidProp->setValue($job, 'test-uuid-pending-guard');
|
||||
|
||||
$job->failed(new Exception('Some real failure'));
|
||||
|
||||
$log->refresh();
|
||||
expect($log->status)->toBe('failed');
|
||||
expect($log->message)->toContain('Some real failure');
|
||||
});
|
||||
|
||||
test('s3 storage has scheduled backups relationship', function () {
|
||||
$team = Team::factory()->create();
|
||||
|
||||
|
|
|
|||
247
tests/Feature/PreviewEnvVarFallbackTest.php
Normal file
247
tests/Feature/PreviewEnvVarFallbackTest.php
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user->teams()->attach($this->team);
|
||||
|
||||
$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);
|
||||
});
|
||||
|
||||
/**
|
||||
* Simulate the preview .env generation logic from
|
||||
* ApplicationDeploymentJob::generate_runtime_environment_variables()
|
||||
* including the production fallback fix.
|
||||
*/
|
||||
function simulatePreviewEnvGeneration(Application $application): \Illuminate\Support\Collection
|
||||
{
|
||||
$sorted_environment_variables = $application->environment_variables->sortBy('id');
|
||||
$sorted_environment_variables_preview = $application->environment_variables_preview->sortBy('id');
|
||||
|
||||
$envs = collect([]);
|
||||
|
||||
// Preview vars
|
||||
$runtime_environment_variables_preview = $sorted_environment_variables_preview->filter(fn ($env) => $env->is_runtime);
|
||||
foreach ($runtime_environment_variables_preview as $env) {
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
}
|
||||
|
||||
// Fallback: production vars not overridden by preview,
|
||||
// only when preview vars are configured
|
||||
if ($runtime_environment_variables_preview->isNotEmpty()) {
|
||||
$previewKeys = $runtime_environment_variables_preview->pluck('key')->toArray();
|
||||
$fallback_production_vars = $sorted_environment_variables->filter(function ($env) use ($previewKeys) {
|
||||
return $env->is_runtime && ! in_array($env->key, $previewKeys);
|
||||
});
|
||||
foreach ($fallback_production_vars as $env) {
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
}
|
||||
}
|
||||
|
||||
return $envs;
|
||||
}
|
||||
|
||||
test('production vars fall back when preview vars exist but do not cover all keys', function () {
|
||||
// Create two production vars (booted hook auto-creates preview copies)
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'DB_PASSWORD',
|
||||
'value' => 'secret123',
|
||||
'is_preview' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'APP_KEY',
|
||||
'value' => 'app_key_value',
|
||||
'is_preview' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
// Delete only the DB_PASSWORD preview copy — APP_KEY preview copy remains
|
||||
$this->application->environment_variables_preview()->where('key', 'DB_PASSWORD')->delete();
|
||||
$this->application->refresh();
|
||||
|
||||
// Preview has APP_KEY but not DB_PASSWORD
|
||||
expect($this->application->environment_variables_preview()->where('key', 'APP_KEY')->count())->toBe(1);
|
||||
expect($this->application->environment_variables_preview()->where('key', 'DB_PASSWORD')->count())->toBe(0);
|
||||
|
||||
$envs = simulatePreviewEnvGeneration($this->application);
|
||||
|
||||
$envString = $envs->implode("\n");
|
||||
// DB_PASSWORD should fall back from production
|
||||
expect($envString)->toContain('DB_PASSWORD=');
|
||||
// APP_KEY should use the preview value
|
||||
expect($envString)->toContain('APP_KEY=');
|
||||
});
|
||||
|
||||
test('no fallback when no preview vars are configured at all', function () {
|
||||
// Create a production-only var (booted hook auto-creates preview copy)
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'DB_PASSWORD',
|
||||
'value' => 'secret123',
|
||||
'is_preview' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
// Delete ALL preview copies — simulates no preview config
|
||||
$this->application->environment_variables_preview()->delete();
|
||||
$this->application->refresh();
|
||||
|
||||
expect($this->application->environment_variables_preview()->count())->toBe(0);
|
||||
|
||||
$envs = simulatePreviewEnvGeneration($this->application);
|
||||
|
||||
$envString = $envs->implode("\n");
|
||||
// Should NOT fall back to production when no preview vars exist
|
||||
expect($envString)->not->toContain('DB_PASSWORD=');
|
||||
});
|
||||
|
||||
test('preview var overrides production var when both exist', function () {
|
||||
// Create production var (auto-creates preview copy)
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'DB_PASSWORD',
|
||||
'value' => 'prod_password',
|
||||
'is_preview' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
// Update the auto-created preview copy with a different value
|
||||
$this->application->environment_variables_preview()
|
||||
->where('key', 'DB_PASSWORD')
|
||||
->update(['value' => encrypt('preview_password')]);
|
||||
|
||||
$this->application->refresh();
|
||||
$envs = simulatePreviewEnvGeneration($this->application);
|
||||
|
||||
// Should contain preview value only, not production
|
||||
$envEntries = $envs->filter(fn ($e) => str_starts_with($e, 'DB_PASSWORD='));
|
||||
expect($envEntries)->toHaveCount(1);
|
||||
expect($envEntries->first())->toContain('preview_password');
|
||||
});
|
||||
|
||||
test('preview-only var works without production counterpart', function () {
|
||||
// Create a preview-only var directly (no production counterpart)
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'PREVIEW_ONLY_VAR',
|
||||
'value' => 'preview_value',
|
||||
'is_preview' => true,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
$this->application->refresh();
|
||||
$envs = simulatePreviewEnvGeneration($this->application);
|
||||
|
||||
$envString = $envs->implode("\n");
|
||||
expect($envString)->toContain('PREVIEW_ONLY_VAR=');
|
||||
});
|
||||
|
||||
test('buildtime-only production vars are not included in preview fallback', function () {
|
||||
// Create a runtime preview var so fallback is active
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'SOME_PREVIEW_VAR',
|
||||
'value' => 'preview_value',
|
||||
'is_preview' => true,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
// Create a buildtime-only production var
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'BUILD_SECRET',
|
||||
'value' => 'build_only',
|
||||
'is_preview' => false,
|
||||
'is_runtime' => false,
|
||||
'is_buildtime' => true,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
// Delete the auto-created preview copy of BUILD_SECRET
|
||||
$this->application->environment_variables_preview()->where('key', 'BUILD_SECRET')->delete();
|
||||
$this->application->refresh();
|
||||
|
||||
$envs = simulatePreviewEnvGeneration($this->application);
|
||||
|
||||
$envString = $envs->implode("\n");
|
||||
expect($envString)->not->toContain('BUILD_SECRET');
|
||||
expect($envString)->toContain('SOME_PREVIEW_VAR=');
|
||||
});
|
||||
|
||||
test('preview env var inherits is_runtime and is_buildtime from production var', function () {
|
||||
// Create production var WITH explicit flags
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'DB_PASSWORD',
|
||||
'value' => 'secret123',
|
||||
'is_preview' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => true,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
$preview = EnvironmentVariable::where('key', 'DB_PASSWORD')
|
||||
->where('is_preview', true)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->first();
|
||||
|
||||
expect($preview)->not->toBeNull();
|
||||
expect($preview->is_runtime)->toBeTrue();
|
||||
expect($preview->is_buildtime)->toBeTrue();
|
||||
});
|
||||
|
||||
test('preview env var gets correct defaults when production var created without explicit flags', function () {
|
||||
// Simulate code paths (docker-compose parser, dev view bulk submit) that create
|
||||
// env vars without explicitly setting is_runtime/is_buildtime
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'DB_PASSWORD',
|
||||
'value' => 'secret123',
|
||||
'is_preview' => false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
]);
|
||||
|
||||
$preview = EnvironmentVariable::where('key', 'DB_PASSWORD')
|
||||
->where('is_preview', true)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->first();
|
||||
|
||||
expect($preview)->not->toBeNull();
|
||||
expect($preview->is_runtime)->toBeTrue();
|
||||
expect($preview->is_buildtime)->toBeTrue();
|
||||
});
|
||||
Loading…
Reference in a new issue