diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 2b67107484..f54e5d3a3f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -34,6 +34,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\TimeoutExceededException; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Sleep; @@ -5953,7 +5954,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $errorClass = get_class($exception); $this->application_deployment_queue->addLogEntry('========================================', 'stderr'); - if ($exception instanceof DeploymentException && $exception->isMessageAlreadyLogged()) { + if ($exception instanceof TimeoutExceededException) { + $this->application_deployment_queue->addLogEntry("Deployment failed: Timeout of {$this->timeout} seconds exceeded. Increase the deployment timeout in the Server settings (Advanced > Builds).", 'stderr'); + } elseif ($exception instanceof DeploymentException && $exception->isMessageAlreadyLogged()) { $this->application_deployment_queue->addLogEntry('Deployment failed.', 'stderr'); } else { $this->application_deployment_queue->addLogEntry("Deployment failed: {$errorMessage}", 'stderr'); diff --git a/tests/Unit/ApplicationDeploymentErrorLoggingTest.php b/tests/Unit/ApplicationDeploymentErrorLoggingTest.php index c6210639ac..adc194ca0b 100644 --- a/tests/Unit/ApplicationDeploymentErrorLoggingTest.php +++ b/tests/Unit/ApplicationDeploymentErrorLoggingTest.php @@ -4,6 +4,8 @@ use App\Exceptions\DeploymentException; use App\Jobs\ApplicationDeploymentJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use Illuminate\Contracts\Queue\Job; +use Illuminate\Queue\TimeoutExceededException; /** * Test to verify that deployment errors are properly logged with comprehensive details. @@ -19,7 +21,7 @@ use App\Models\ApplicationDeploymentQueue; */ it('logs comprehensive error details when failed() is called', function () { // Create a mock exception with all properties - $innerException = new \RuntimeException('Connection refused', 111); + $innerException = new RuntimeException('Connection refused', 111); $exception = new DeploymentException( 'Failed to start container', 500, @@ -71,7 +73,7 @@ it('logs comprehensive error details when failed() is called', function () { $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); - $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); $queueProperty = $reflection->getProperty('application_deployment_queue'); $queueProperty->setAccessible(true); @@ -137,7 +139,7 @@ it('logs comprehensive error details when failed() is called', function () { }); it('handles exceptions with no message gracefully', function () { - $exception = new \Exception; + $exception = new Exception; $mockQueue = Mockery::mock(ApplicationDeploymentQueue::class); $logEntries = []; @@ -180,7 +182,7 @@ it('handles exceptions with no message gracefully', function () { $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); - $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); $queueProperty = $reflection->getProperty('application_deployment_queue'); $queueProperty->setAccessible(true); @@ -215,7 +217,7 @@ it('handles exceptions with no message gracefully', function () { it('wraps exceptions in deployment methods with DeploymentException', function () { // Verify that our deployment methods wrap exceptions properly - $originalException = new \RuntimeException('Container not found'); + $originalException = new RuntimeException('Container not found'); try { throw new DeploymentException('Failed to start container', 0, $originalException); @@ -228,7 +230,7 @@ it('wraps exceptions in deployment methods with DeploymentException', function ( it('logs error code 0 correctly', function () { // Verify that error code 0 is logged (previously skipped due to falsy check) - $exception = new \Exception('Test error', 0); + $exception = new Exception('Test error', 0); $mockQueue = Mockery::mock(ApplicationDeploymentQueue::class); $logEntries = []; @@ -271,7 +273,7 @@ it('logs error code 0 correctly', function () { $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); - $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); $queueProperty = $reflection->getProperty('application_deployment_queue'); $queueProperty->setAccessible(true); @@ -305,7 +307,7 @@ it('logs error code 0 correctly', function () { it('preserves original exception type in wrapped DeploymentException messages', function () { // Verify that when wrapping exceptions, the original exception type is included in the message - $originalException = new \RuntimeException('Connection timeout'); + $originalException = new RuntimeException('Connection timeout'); // Test rolling update scenario $wrappedException = new DeploymentException( @@ -319,7 +321,7 @@ it('preserves original exception type in wrapped DeploymentException messages', expect($wrappedException->getPrevious())->toBe($originalException); // Test health check scenario - $healthCheckException = new \InvalidArgumentException('Invalid health check URL'); + $healthCheckException = new InvalidArgumentException('Invalid health check URL'); $wrappedHealthCheck = new DeploymentException( 'Health check failed ('.get_class($healthCheckException).'): '.$healthCheckException->getMessage(), $healthCheckException->getCode(), @@ -331,7 +333,7 @@ it('preserves original exception type in wrapped DeploymentException messages', expect($wrappedHealthCheck->getPrevious())->toBe($healthCheckException); // Test docker registry push scenario - $registryException = new \RuntimeException('Failed to authenticate'); + $registryException = new RuntimeException('Failed to authenticate'); $wrappedRegistry = new DeploymentException( get_class($registryException).': '.$registryException->getMessage(), $registryException->getCode(), @@ -342,3 +344,68 @@ it('preserves original exception type in wrapped DeploymentException messages', expect($wrappedRegistry->getMessage())->toContain('Failed to authenticate'); expect($wrappedRegistry->getPrevious())->toBe($registryException); }); + +it('logs the configured timeout when the deployment times out', function () { + $queueJob = Mockery::mock(Job::class); + $queueJob->shouldReceive('resolveName')->andReturn(ApplicationDeploymentJob::class); + $exception = TimeoutExceededException::forJob($queueJob); + + $mockQueue = Mockery::mock(ApplicationDeploymentQueue::class); + $logEntries = []; + + $mockQueue->shouldReceive('addLogEntry') + ->withArgs(function ($message, $type = 'stdout', $hidden = false) use (&$logEntries) { + $logEntries[] = ['message' => $message, 'type' => $type, 'hidden' => $hidden]; + + return true; + }) + ->atLeast()->once(); + + $mockQueue->shouldReceive('update')->andReturn(true); + + $mockApplication = Mockery::mock(Application::class); + $mockApplication->shouldReceive('getAttribute') + ->with('build_pack') + ->andReturn('dockerfile'); + + $mockSettings = Mockery::mock(); + $mockSettings->shouldReceive('getAttribute') + ->with('is_consistent_container_name_enabled') + ->andReturn(false); + $mockSettings->is_consistent_container_name_enabled = false; + + $mockApplication->shouldReceive('getAttribute') + ->with('settings') + ->andReturn($mockSettings); + + $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); + $job->shouldAllowMockingProtectedMethods(); + $job->timeout = 900; + + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); + + $queueProperty = $reflection->getProperty('application_deployment_queue'); + $queueProperty->setValue($job, $mockQueue); + + $applicationProperty = $reflection->getProperty('application'); + $applicationProperty->setValue($job, $mockApplication); + + $pullRequestProperty = $reflection->getProperty('pull_request_id'); + $pullRequestProperty->setValue($job, 0); + + $containerNameProperty = $reflection->getProperty('container_name'); + $containerNameProperty->setValue($job, 'test-container'); + + $job->shouldReceive('failDeployment')->andReturn(); + $job->shouldReceive('execute_remote_command')->andReturn(); + + $job->failed($exception); + + $timeoutEntry = collect($logEntries)->first(fn ($entry) => str_contains($entry['message'], 'Timeout of 900 seconds exceeded')); + + expect($timeoutEntry)->not->toBeNull() + ->and($timeoutEntry['message'])->toContain('Increase the deployment timeout in the Server settings') + ->and($timeoutEntry['type'])->toBe('stderr') + ->and($timeoutEntry['hidden'])->toBeFalse() + ->and(collect($logEntries)->pluck('message')->implode("\n"))->not->toContain('has timed out.'); +});