From 28fb694fcb9202f04492b08690c1a2662029d7dd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:46:57 +0200 Subject: [PATCH] fix: preserve custom names and use Livewire redirects Resolve custom container names consistently, clean up legacy container names during deployments, and route Livewire resource redirects through the navigation helper. --- app/Jobs/ApplicationDeploymentJob.php | 40 ++++++++++--- app/Livewire/Project/Database/BackupEdit.php | 4 +- app/Livewire/Project/Service/Index.php | 4 +- .../Project/Shared/ScheduledTask/Show.php | 4 +- app/Livewire/Source/Github/Change.php | 2 +- app/Livewire/Source/Gitlab/Change.php | 2 +- app/Livewire/Storage/Show.php | 2 +- ...plicationDeploymentContainerNamingTest.php | 58 +++++++++++++++++++ ...ationDeploymentCustomDockerOptionsTest.php | 5 +- tests/Unit/LivewireInternalRedirectTest.php | 43 ++++++++++++++ 10 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 tests/Unit/ApplicationDeploymentContainerNamingTest.php create mode 100644 tests/Unit/LivewireInternalRedirectTest.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 327fea25e..7168aea5f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -261,14 +261,7 @@ public function __construct(public int $application_deployment_queue_id) $this->configuration_dir = application_configuration_dir()."/{$this->application->uuid}"; $this->is_debug_enabled = $this->application->settings->is_debug_enabled; - $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); - if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) { - if ($this->pull_request_id === 0) { - $this->container_name = $this->application->settings->custom_internal_name; - } else { - $this->container_name = addPreviewDeploymentSuffix($this->application->settings->custom_internal_name, $this->pull_request_id); - } - } + $this->container_name = $this->resolveContainerName(); $this->saved_outputs = collect(); @@ -1986,6 +1979,19 @@ private function rolling_update() } } + private function resolveContainerName(): string + { + if (str($this->application->settings->custom_internal_name)->isEmpty()) { + return generateApplicationContainerName($this->application, $this->pull_request_id); + } + + if ($this->pull_request_id === 0) { + return $this->application->settings->custom_internal_name; + } + + return addPreviewDeploymentSuffix($this->application->settings->custom_internal_name, $this->pull_request_id); + } + private function health_check() { try { @@ -3428,6 +3434,9 @@ private function generate_compose_file() $custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options); if ((bool) $this->application->settings->is_consistent_container_name_enabled) { $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; + if ($this->container_name !== $this->application->uuid) { + unset($docker_compose['services'][$this->container_name]); + } if (count($custom_compose) > 0) { $ipv4 = data_get($custom_compose, 'ip.0'); $ipv6 = data_get($custom_compose, 'ip6.0'); @@ -4027,7 +4036,10 @@ private function stop_running_container(bool $force = false) $this->application_deployment_queue->addLogEntry('Removing old containers.'); if ($this->newVersionIsHealthy || $force) { if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { - $this->graceful_shutdown_container($this->container_name); + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); + $this->containerNamesToRemove($containers)->each(function (string $containerName) { + $this->graceful_shutdown_container($containerName); + }); } else { $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); if ($this->pull_request_id === 0) { @@ -4066,6 +4078,16 @@ private function stop_running_container(bool $force = false) } } + private function containerNamesToRemove(Collection $containers): Collection + { + return $containers + ->pluck('Names') + ->push($this->container_name) + ->filter() + ->unique() + ->values(); + } + private function start_by_compose_file() { try { diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 2c04f5ba9..2cd0d7673 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -212,14 +212,14 @@ public function delete($password, $selectedActions = []) if ($this->backup->database->getMorphClass() === ServiceDatabase::class) { $serviceDatabase = $this->backup->database; - return redirect()->route('project.service.database.backups', [ + return redirectRoute($this, 'project.service.database.backups', [ 'project_uuid' => $this->parameters['project_uuid'], 'environment_uuid' => $this->parameters['environment_uuid'], 'service_uuid' => $serviceDatabase->service->uuid, 'stack_service_uuid' => $serviceDatabase->uuid, ]); } else { - return redirect()->route('project.database.backup.index', [ + return redirectRoute($this, 'project.database.backup.index', [ 'project_uuid' => $this->parameters['project_uuid'], 'environment_uuid' => $this->parameters['environment_uuid'], 'database_uuid' => $this->parameters['database_uuid'], diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index d93ed7c02..6193f14b5 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -428,7 +428,7 @@ public function deleteApplication($password, $selectedActions = []) $this->serviceApplication->delete(); $this->dispatch('success', 'Application deleted.'); - return redirect()->route('project.service.configuration', $this->parameters); + return redirectRoute($this, 'project.service.configuration', $this->parameters); } catch (\Throwable $e) { return handleError($e, $this); } @@ -462,7 +462,7 @@ public function convertToDatabase() $serviceApplication->delete(); }); - return redirect()->route('project.service.configuration', $redirectParams); + return redirectRoute($this, 'project.service.configuration', $redirectParams); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index 11df00153..4eb5c071c 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -169,9 +169,9 @@ public function delete() $this->task->delete(); if ($this->type === 'application') { - return redirect()->route('project.application.scheduled-tasks.show', $this->parameters); + return redirectRoute($this, 'project.application.scheduled-tasks.show', $this->parameters); } else { - return redirect()->route('project.service.scheduled-tasks.show', $this->parameters); + return redirectRoute($this, 'project.service.scheduled-tasks.show', $this->parameters); } } catch (\Exception $e) { return handleError($e); diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 2570c3a1b..001d31c37 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -484,7 +484,7 @@ public function delete() // @can and canGate checks against a deleted model (null team_id TypeError). $this->github_app = null; - return redirect()->route('source.all'); + return redirectRoute($this, 'source.all'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Source/Gitlab/Change.php b/app/Livewire/Source/Gitlab/Change.php index dd0284582..29374105b 100644 --- a/app/Livewire/Source/Gitlab/Change.php +++ b/app/Livewire/Source/Gitlab/Change.php @@ -338,7 +338,7 @@ public function delete() // @can and canGate checks against a deleted model (null team_id TypeError). $this->gitlab_app = null; - return redirect()->route('source.all'); + return redirectRoute($this, 'source.all'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Storage/Show.php b/app/Livewire/Storage/Show.php index 89782d686..17abd19e5 100644 --- a/app/Livewire/Storage/Show.php +++ b/app/Livewire/Storage/Show.php @@ -43,7 +43,7 @@ public function delete() $this->storage->delete(); - return redirect()->route('storage.index'); + return redirectRoute($this, 'storage.index'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/tests/Unit/ApplicationDeploymentContainerNamingTest.php b/tests/Unit/ApplicationDeploymentContainerNamingTest.php new file mode 100644 index 000000000..b7b63ae54 --- /dev/null +++ b/tests/Unit/ApplicationDeploymentContainerNamingTest.php @@ -0,0 +1,58 @@ +newInstanceWithoutConstructor(); + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); + $reflection->getProperty('application')->setValue($job, $application); + $reflection->getProperty('pull_request_id')->setValue($job, $pullRequestId); + + return [$job, $reflection]; +} + +function applicationWithContainerNaming(string $customName = 'shadowuw'): Application +{ + $application = new Application; + $application->forceFill(['uuid' => 'application-uuid']); + $application->setRelation('settings', new ApplicationSetting([ + 'custom_internal_name' => $customName, + 'is_consistent_container_name_enabled' => true, + ])); + + return $application; +} + +it('uses the custom container name when consistent naming is enabled', function () { + $application = applicationWithContainerNaming(); + + [$job, $reflection] = containerNamingJob($application); + + expect($reflection->getMethod('resolveContainerName')->invoke($job))->toBe('shadowuw'); +}); + +it('adds the pull request suffix to a custom container name', function () { + $application = applicationWithContainerNaming(); + + [$job, $reflection] = containerNamingJob($application, 42); + + expect($reflection->getMethod('resolveContainerName')->invoke($job))->toBe('shadowuw-pr-42'); +}); + +it('includes old generated containers when cleaning up a consistent deployment', function () { + $application = applicationWithContainerNaming(); + [$job, $reflection] = containerNamingJob($application); + $reflection->getProperty('container_name')->setValue($job, 'shadowuw'); + + $containers = new Collection([ + ['Names' => 'application-uuid-192238854305'], + ['Names' => 'shadowuw'], + ]); + + expect($reflection->getMethod('containerNamesToRemove')->invoke($job, $containers)->all()) + ->toBe(['application-uuid-192238854305', 'shadowuw']); +}); diff --git a/tests/Unit/ApplicationDeploymentCustomDockerOptionsTest.php b/tests/Unit/ApplicationDeploymentCustomDockerOptionsTest.php index 9c671156d..234f216bd 100644 --- a/tests/Unit/ApplicationDeploymentCustomDockerOptionsTest.php +++ b/tests/Unit/ApplicationDeploymentCustomDockerOptionsTest.php @@ -64,7 +64,7 @@ function generateComposeServiceWithCustomDockerOptions(string $customDockerOptio 'server' => $server, 'mainServer' => $server, 'pull_request_id' => 0, - 'container_name' => $application->uuid, + 'container_name' => 'custom-internal-name', 'production_image_name' => 'example/app:latest', 'deployment_uuid' => 'deployment-uuid', 'workdir' => '/artifacts/custom-docker-options-app', @@ -82,7 +82,8 @@ function generateComposeServiceWithCustomDockerOptions(string $customDockerOptio it('applies an entrypoint when consistent naming and a custom internal name are configured', function () { expect(generateComposeServiceWithCustomDockerOptions('--entrypoint "/bin/echo hello world"')) - ->toHaveKey('entrypoint', '/bin/echo hello world'); + ->toHaveKey('entrypoint', '/bin/echo hello world') + ->toHaveKey('container_name', 'custom-internal-name'); }); it('preserves custom network aliases when a static IP is configured', function () { diff --git a/tests/Unit/LivewireInternalRedirectTest.php b/tests/Unit/LivewireInternalRedirectTest.php new file mode 100644 index 000000000..7dc19a81b --- /dev/null +++ b/tests/Unit/LivewireInternalRedirectTest.php @@ -0,0 +1,43 @@ +toContain($redirect); + } +})->with([ + 'S3 storage' => [ + 'app/Livewire/Storage/Show.php', + ["redirectRoute(\$this, 'storage.index')"], + ], + 'database backup schedule' => [ + 'app/Livewire/Project/Database/BackupEdit.php', + [ + "redirectRoute(\$this, 'project.service.database.backups'", + "redirectRoute(\$this, 'project.database.backup.index'", + ], + ], + 'scheduled task' => [ + 'app/Livewire/Project/Shared/ScheduledTask/Show.php', + [ + "redirectRoute(\$this, 'project.application.scheduled-tasks.show'", + "redirectRoute(\$this, 'project.service.scheduled-tasks.show'", + ], + ], + 'GitHub source' => [ + 'app/Livewire/Source/Github/Change.php', + ["redirectRoute(\$this, 'source.all')"], + ], + 'GitLab source' => [ + 'app/Livewire/Source/Gitlab/Change.php', + ["redirectRoute(\$this, 'source.all')"], + ], + 'service resources' => [ + 'app/Livewire/Project/Service/Index.php', + [ + "return redirectRoute(\$this, 'project.service.configuration', \$this->parameters);", + "return redirectRoute(\$this, 'project.service.configuration', \$redirectParams);", + ], + ], +]);