fix(deploy): never build the image on additional servers

An additional server of a multi-server application must run the image
that the main server built and pushed. On servers with the build or
both role, a disabled build cache still forced a local rebuild, and a
failed registry pull fell back to a local build. That ignored the build
server and the build-server fallback setting, and the servers could run
different images.

Additional servers now always pull the main server's image (a forced
rebuild is reset, except for Docker Compose applications, which build
on each server by design). When the image is not in the registry, the
deployment fails with a clear message instead of building it.
This commit is contained in:
Andras Bacsai
2026-10-01 07:16:14 +02:00
parent 766fe56448
commit cd8d299955
2 changed files with 58 additions and 4 deletions
@@ -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],
]);