From 133d6a0349bfbc7367522b66d68484604f0f2157 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:08:26 +0100 Subject: [PATCH] feat(DeploymentException): add custom exception for deployment errors and update handler to exclude from reporting --- app/Exceptions/DeploymentException.php | 32 ++++++++++++ app/Exceptions/Handler.php | 1 + app/Jobs/ApplicationDeploymentJob.php | 16 +++--- tests/Unit/DeploymentExceptionTest.php | 71 ++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 app/Exceptions/DeploymentException.php create mode 100644 tests/Unit/DeploymentExceptionTest.php diff --git a/app/Exceptions/DeploymentException.php b/app/Exceptions/DeploymentException.php new file mode 100644 index 000000000..01e0a8235 --- /dev/null +++ b/app/Exceptions/DeploymentException.php @@ -0,0 +1,32 @@ +getMessage(), $exception->getCode(), $exception); + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 3d731223d..71de48bcd 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -30,6 +30,7 @@ class Handler extends ExceptionHandler protected $dontReport = [ ProcessException::class, NonReportableException::class, + DeploymentException::class, ]; /** diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 7553ec987..349fccb50 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -7,6 +7,7 @@ use App\Enums\ProcessStatus; use App\Events\ApplicationConfigurationChanged; use App\Events\ServiceStatusChanged; +use App\Exceptions\DeploymentException; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; @@ -31,7 +32,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Sleep; use Illuminate\Support\Str; -use RuntimeException; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Throwable; @@ -976,7 +976,7 @@ private function push_to_docker_registry() } catch (Exception $e) { $this->application_deployment_queue->addLogEntry('Failed to push image to docker registry. Please check debug logs for more information.'); if ($forceFail) { - throw new RuntimeException($e->getMessage(), 69420); + throw new DeploymentException($e->getMessage(), 69420); } } } @@ -1823,7 +1823,7 @@ private function prepare_builder_image(bool $firstTry = true) $env_flags = $this->generate_docker_env_flags_for_secrets(); if ($this->use_build_server) { if ($this->dockerConfigFileExists === 'NOK') { - throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.'); + throw new DeploymentException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.'); } $runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { @@ -2089,7 +2089,7 @@ private function generate_nixpacks_confs() if ($this->saved_outputs->get('nixpacks_type')) { $this->nixpacks_type = $this->saved_outputs->get('nixpacks_type'); if (str($this->nixpacks_type)->isEmpty()) { - throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers'); + throw new DeploymentException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers'); } } @@ -3676,7 +3676,7 @@ private function run_pre_deployment_command() return; } } - throw new RuntimeException('Pre-deployment command: Could not find a valid container. Is the container name correct?'); + throw new DeploymentException('Pre-deployment command: Could not find a valid container. Is the container name correct?'); } private function run_post_deployment_command() @@ -3712,7 +3712,7 @@ private function run_post_deployment_command() return; } } - throw new RuntimeException('Post-deployment command: Could not find a valid container. Is the container name correct?'); + throw new DeploymentException('Post-deployment command: Could not find a valid container. Is the container name correct?'); } /** @@ -3723,7 +3723,7 @@ private function checkForCancellation(): void $this->application_deployment_queue->refresh(); if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { $this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.'); - throw new \RuntimeException('Deployment cancelled by user', 69420); + throw new DeploymentException('Deployment cancelled by user', 69420); } } @@ -3756,7 +3756,7 @@ private function isInTerminalState(): bool if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { $this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.'); - throw new \RuntimeException('Deployment cancelled by user', 69420); + throw new DeploymentException('Deployment cancelled by user', 69420); } return false; diff --git a/tests/Unit/DeploymentExceptionTest.php b/tests/Unit/DeploymentExceptionTest.php new file mode 100644 index 000000000..5dd448df4 --- /dev/null +++ b/tests/Unit/DeploymentExceptionTest.php @@ -0,0 +1,71 @@ +getProperty('dontReport'); + $property->setAccessible(true); + $dontReport = $property->getValue($handler); + + expect($dontReport)->toContain(DeploymentException::class); +}); + +test('DeploymentException can be created with a message', function () { + $exception = new DeploymentException('Test deployment error'); + + expect($exception->getMessage())->toBe('Test deployment error'); + expect($exception)->toBeInstanceOf(Exception::class); +}); + +test('DeploymentException can be created with a message and code', function () { + $exception = new DeploymentException('Test error', 69420); + + expect($exception->getMessage())->toBe('Test error'); + expect($exception->getCode())->toBe(69420); +}); + +test('DeploymentException can be created from another exception', function () { + $originalException = new RuntimeException('Original error', 500); + $deploymentException = DeploymentException::fromException($originalException); + + expect($deploymentException->getMessage())->toBe('Original error'); + expect($deploymentException->getCode())->toBe(500); + expect($deploymentException->getPrevious())->toBe($originalException); +}); + +test('DeploymentException is not reported when thrown', function () { + $handler = new Handler(app()); + + // DeploymentException should not be reported (logged) + $exception = new DeploymentException('Test deployment failure'); + + // Check that the exception should not be reported + $reflection = new ReflectionClass($handler); + $method = $reflection->getMethod('shouldReport'); + $method->setAccessible(true); + + $shouldReport = $method->invoke($handler, $exception); + + expect($shouldReport)->toBeFalse(); +}); + +test('RuntimeException is still reported when thrown', function () { + $handler = new Handler(app()); + + // RuntimeException should still be reported (this is for Coolify bugs) + $exception = new RuntimeException('Unexpected error in Coolify code'); + + // Check that the exception should be reported + $reflection = new ReflectionClass($handler); + $method = $reflection->getMethod('shouldReport'); + $method->setAccessible(true); + + $shouldReport = $method->invoke($handler, $exception); + + expect($shouldReport)->toBeTrue(); +});