feat(DeploymentException): add custom exception for deployment errors and update handler to exclude from reporting

This commit is contained in:
Andras Bacsai 2025-11-11 15:08:26 +01:00
parent 0d14bc1df7
commit 133d6a0349
4 changed files with 112 additions and 8 deletions

View file

@ -0,0 +1,32 @@
<?php
namespace App\Exceptions;
use Exception;
/**
* Exception for expected deployment failures caused by user/application errors.
* These are not Coolify bugs and should not be logged to laravel.log.
* Examples: Nixpacks detection failures, missing Dockerfiles, invalid configs, etc.
*/
class DeploymentException extends Exception
{
/**
* Create a new deployment exception instance.
*
* @param string $message
* @param int $code
*/
public function __construct($message = '', $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
/**
* Create from another exception, preserving its message and stack trace.
*/
public static function fromException(\Throwable $exception): static
{
return new static($exception->getMessage(), $exception->getCode(), $exception);
}
}

View file

@ -30,6 +30,7 @@ class Handler extends ExceptionHandler
protected $dontReport = [
ProcessException::class,
NonReportableException::class,
DeploymentException::class,
];
/**

View file

@ -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;

View file

@ -0,0 +1,71 @@
<?php
use App\Exceptions\DeploymentException;
use App\Exceptions\Handler;
test('DeploymentException is in the dontReport array', function () {
$handler = new Handler(app());
// Use reflection to access the protected $dontReport property
$reflection = new ReflectionClass($handler);
$property = $reflection->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();
});