diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 10364885b6..a5f421da3d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -434,8 +434,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // An additional server of a multi-server application does not build: it pulls the image the main server pushed. if ($this->is_this_additional_server && str($this->application->docker_registry_image_name)->isNotEmpty()) { - if (! $this->server->canBuildApplications()) { - // Disabled build cache forces a rebuild, which would skip the registry pull on a server that cannot build. + // The main deployment already built the image (also for a forced rebuild or disabled build cache). + // A forced rebuild would skip the registry pull. Compose applications build on each server that can build. + if ($this->application->build_pack !== 'dockercompose' || ! $this->server->canBuildApplications()) { $this->force_rebuild = false; } $this->application_deployment_queue->addLogEntry('Additional server: pulls the image from the registry, no build server needed.'); @@ -1472,8 +1473,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry('Build configuration changed. Rebuilding image.'); } } else { - if ($this->is_this_additional_server && ! $this->server->canBuildApplications() && $this->application->build_pack !== 'dockercompose') { - throw new DeploymentException("Image ({$this->production_image_name}) not found in the registry; the main server must push it first. The server ({$this->server->name}) is set to deployments only and cannot build it."); + if ($this->is_this_additional_server && $this->application->build_pack !== 'dockercompose') { + $message = "Image ({$this->production_image_name}) not found in the registry; the main server must push it first."; + $message .= $this->server->canBuildApplications() + ? ' Additional servers do not build, so they run the same image as the main server.' + : " The server ({$this->server->name}) is set to deployments only and cannot build it."; + throw new DeploymentException($message); } $this->application_deployment_queue->addLogEntry("Image not found ({$this->production_image_name}). Building new image."); } diff --git a/tests/Feature/BuildServerFallbackPolicyTest.php b/tests/Feature/BuildServerFallbackPolicyTest.php index 779db2f463..a983402447 100644 --- a/tests/Feature/BuildServerFallbackPolicyTest.php +++ b/tests/Feature/BuildServerFallbackPolicyTest.php @@ -381,3 +381,52 @@ test('additional deployments-only servers fail instead of building when the regi 'deploy' => [false], 'restart' => [true], ]); + +test('additional servers that can build still pull the image when the build cache is disabled', function (string $role) { + $team = Team::factory()->create(['is_build_server_fallback_enabled' => false]); + $deploymentServer = makeRoleServer($team, $role); + [$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, ['docker_registry_image_name' => 'ghcr.io/coollabsio/app']); + markAsAdditionalServer($job, forceRebuild: true); + + $deploymentQueue->shouldReceive('addLogEntry')->once()->with('Additional server: pulls the image from the registry, no build server needed.'); + + invokeBuildServerSelection($job); + + expect((new ReflectionProperty(ApplicationDeploymentJob::class, 'force_rebuild'))->getValue($job))->toBeFalse() + ->and(usesRemoteBuildServer($job))->toBeFalse(); +})->with([ + 'deployments and builds' => ['both'], + 'builds only' => ['build'], +]); + +test('additional Compose servers that can build keep a disabled build cache', function () { + $team = Team::factory()->create(); + $deploymentServer = makeRoleServer($team, 'both'); + [$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, ['build_pack' => 'dockercompose', 'docker_registry_image_name' => 'ghcr.io/coollabsio/app']); + markAsAdditionalServer($job, forceRebuild: true); + + $deploymentQueue->shouldReceive('addLogEntry'); + + invokeBuildServerSelection($job); + + expect((new ReflectionProperty(ApplicationDeploymentJob::class, 'force_rebuild'))->getValue($job))->toBeTrue(); +}); + +test('additional servers that can build fail instead of building when the registry image is missing', function (string $role, bool $restartOnly) { + $team = Team::factory()->create(['is_build_server_fallback_enabled' => false]); + $deploymentServer = makeRoleServer($team, $role); + [$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, ['docker_registry_image_name' => 'ghcr.io/coollabsio/app'], restartOnly: $restartOnly); + markAsAdditionalServer($job); + (new ReflectionProperty(ApplicationDeploymentJob::class, 'saved_outputs'))->setValue($job, collect()); + (new ReflectionProperty(ApplicationDeploymentJob::class, 'pull_request_id'))->setValue($job, 0); + (new ReflectionProperty(ApplicationDeploymentJob::class, 'production_image_name'))->setValue($job, 'ghcr.io/coollabsio/app:abc123'); + + $deploymentQueue->shouldNotReceive('addLogEntry'); + + expect(fn () => (new ReflectionMethod(ApplicationDeploymentJob::class, 'should_skip_build'))->invoke($job)) + ->toThrow(DeploymentException::class, 'Image (ghcr.io/coollabsio/app:abc123) not found in the registry; the main server must push it first. Additional servers do not build, so they run the same image as the main server.'); +})->with([ + 'deployments and builds, deploy' => ['both', false], + 'deployments and builds, restart' => ['both', true], + 'builds only, deploy' => ['build', false], +]);