diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 9d927d10c..2af380a45 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -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()) { diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index b55c324be..041d31bad 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -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(), + ]); + } } } } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index cf60d5ab5..5acd4c1e4 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -32,6 +32,11 @@ )] class EnvironmentVariable extends BaseModel { + protected $attributes = [ + 'is_runtime' => true, + 'is_buildtime' => true, + ]; + protected $fillable = [ // Core identification 'key', diff --git a/resources/views/components/applications/links.blade.php b/resources/views/components/applications/links.blade.php index 26b1cedf5..85e8f7431 100644 --- a/resources/views/components/applications/links.blade.php +++ b/resources/views/components/applications/links.blade.php @@ -4,7 +4,7 @@ @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) diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index e27eda8b6..d743e346e 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -49,7 +49,13 @@ !is_null($parsedServices) && count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled) -