mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-26 01:10:30 -04:00
fix(servers): build only on dedicated build servers
The server role migration gives every normal server the combined role, and the build server queries counted combined servers as build servers. "Use a build server" then built on a random production server, the resource picker listed each server twice, and "Deployments only" did not stop builds. - Build server selection and the picker use only "Builds only" servers. A null role falls back to the legacy is_build_server flag. - Without a dedicated build server, builds fall back to the deployment server, never to another combined server. - A "Deployments only" server always builds on a build server and needs a Docker image name. It never builds itself, except for restarts. Docker image and Compose applications are not affected. - Setting "Deployments only" requires a dedicated build server. - The API keeps is_build_server in sync with the role for downgrades. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9e999a2782
commit
60bac941ea
@@ -61,3 +61,6 @@
|
||||
|
||||
## Test the real runtime image
|
||||
- Deployment shell commands run in the Alpine/BusyBox helper image and pass through the non-root sudo parser. Verify new flags and shell syntax in that image and with `parseCommandsByLineForSudo()`; faked command output hides both failures.
|
||||
|
||||
## Format only your own files
|
||||
- `pint --dirty` also rewrites uncommitted files that belong to other work in the tree. When the tree has unrelated changes, pass your changed paths to Pint.
|
||||
|
||||
@@ -540,7 +540,7 @@ class ServersController extends Controller
|
||||
if ($serverRole === ServerRole::DEPLOYMENT && ! ModelsServer::buildServers($teamId)->exists()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['server_role' => ['Add another build-capable server before you set this server to deployments only.']],
|
||||
'errors' => ['server_role' => ['Add a usable build server before you set this server to deployments only.']],
|
||||
], 422);
|
||||
}
|
||||
if (is_null($request->instant_validate)) {
|
||||
@@ -584,6 +584,7 @@ class ServersController extends Controller
|
||||
|
||||
$server->settings()->update([
|
||||
'server_role' => $serverRole,
|
||||
'is_build_server' => $serverRole === ServerRole::BUILD,
|
||||
]);
|
||||
if ($request->instant_validate) {
|
||||
ValidateServer::dispatch($server);
|
||||
@@ -763,7 +764,7 @@ class ServersController extends Controller
|
||||
if ($serverRole === ServerRole::DEPLOYMENT && ! ModelsServer::buildServers($teamId)->whereKeyNot($server->id)->exists()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['server_role' => ['Add another build-capable server before you set this server to deployments only.']],
|
||||
'errors' => ['server_role' => ['Add a usable build server before you set this server to deployments only.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
@@ -771,6 +772,7 @@ class ServersController extends Controller
|
||||
if ($serverRole !== null) {
|
||||
$server->settings()->update([
|
||||
'server_role' => $serverRole,
|
||||
'is_build_server' => $serverRole === ServerRole::BUILD,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -424,33 +424,39 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private function selectBuildServer(): void
|
||||
{
|
||||
if (! data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
$this->build_server = $this->server;
|
||||
$this->build_server = $this->server;
|
||||
|
||||
// A deployments-only server never builds. Docker image and Compose applications are exempt:
|
||||
// the first builds nothing and the second does not support build servers.
|
||||
$mustBuildElsewhere = ! $this->server->canBuildApplications()
|
||||
&& ! in_array($this->application->build_pack, ['dockerimage', 'dockercompose'], true);
|
||||
|
||||
if (! $mustBuildElsewhere && ! data_get($this->application, 'settings.is_build_server_enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mustBuildElsewhere && ! $this->restart_only && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
throw new DeploymentException("The deployment server ({$this->server->name}) is set to deployments only, so this application is built on a build server. Set a Docker image name in the application's General settings so the deployment server can pull the built image.");
|
||||
}
|
||||
|
||||
$team = $this->application->environment->project->team;
|
||||
$buildServers = Server::buildServers($team->id)->get();
|
||||
$buildServers = Server::buildServers($team->id)->whereKeyNot($this->server->id)->get();
|
||||
|
||||
if ($buildServers->isEmpty()) {
|
||||
// A restart only rebuilds when the image is missing, so it may still run on the deployment server.
|
||||
if ($mustBuildElsewhere && ! $this->restart_only) {
|
||||
throw new DeploymentException("The deployment server ({$this->server->name}) is set to deployments only, and no usable build server was found. Add a build server or change the server role.");
|
||||
}
|
||||
if (! $team->is_build_server_fallback_enabled) {
|
||||
throw new DeploymentException('No available dedicated build server was found. Enable a usable build server for this team or allow fallback to the deployment server in the team settings.');
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('No suitable build server found. Using the deployment server.');
|
||||
$this->build_server = $this->server;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->build_server = $buildServers->random();
|
||||
if ($this->build_server->is($this->server)) {
|
||||
$this->application_deployment_queue->addLogEntry("Using deployment server ({$this->server->name}) for the build.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->build_server_id = $this->build_server->id;
|
||||
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
|
||||
$this->use_build_server = true;
|
||||
|
||||
@@ -425,7 +425,7 @@ class Show extends Component
|
||||
|
||||
if ($newRole === ServerRole::DEPLOYMENT && ! Server::buildServers($this->server->team_id)->whereKeyNot($this->server->id)->exists()) {
|
||||
$this->serverRole = $currentRole->value;
|
||||
$this->dispatch('error', 'Add another build-capable server before you set this server to deployments only.');
|
||||
$this->dispatch('error', 'Add a usable build server before you set this server to deployments only.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
+29
-11
@@ -530,12 +530,27 @@ class Server extends BaseModel
|
||||
->whereRelation('settings', 'force_disabled', false);
|
||||
|
||||
return $isBuildServer
|
||||
? $query->whereHas('settings', fn (Builder $settings) => $settings
|
||||
->where('server_role', '!=', ServerRole::DEPLOYMENT->value)
|
||||
->orWhereNull('server_role'))
|
||||
: $query->whereHas('settings', fn (Builder $settings) => $settings
|
||||
->where('server_role', '!=', ServerRole::BUILD->value)
|
||||
->orWhereNull('server_role'));
|
||||
? self::whereServerRole($query, ServerRole::BUILD)
|
||||
: self::whereServerRole($query, ServerRole::DEPLOYMENT, ServerRole::BOTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters by the effective server role. A null role falls back to the legacy
|
||||
* is_build_server flag, like ServerSetting::effectiveServerRole().
|
||||
*/
|
||||
private static function whereServerRole(Builder $query, ServerRole ...$roles): Builder
|
||||
{
|
||||
$legacyBuildServerFlags = collect($roles)
|
||||
->reject(fn (ServerRole $role) => $role === ServerRole::DEPLOYMENT)
|
||||
->map(fn (ServerRole $role) => $role === ServerRole::BUILD)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $query->whereHas('settings', fn (Builder $settings) => $settings
|
||||
->whereIn('server_role', array_map(fn (ServerRole $role) => $role->value, $roles))
|
||||
->orWhere(fn (Builder $legacy) => $legacy
|
||||
->whereNull('server_role')
|
||||
->whereIn('is_build_server', $legacyBuildServerFlags)));
|
||||
}
|
||||
|
||||
public function canHostResources(): bool
|
||||
@@ -929,16 +944,19 @@ $siteAddress {
|
||||
return $this->ip === 'host.docker.internal' || $this->id === 0;
|
||||
}
|
||||
|
||||
public static function buildServers($teamId)
|
||||
/**
|
||||
* Usable dedicated (build-only) servers of a team. Servers with the combined role
|
||||
* host deployments, so they are never picked as build servers.
|
||||
*/
|
||||
public static function buildServers($teamId): Builder
|
||||
{
|
||||
return Server::whereTeamId($teamId)
|
||||
$query = Server::whereTeamId($teamId)
|
||||
->whereRelation('settings', 'is_reachable', true)
|
||||
->whereRelation('settings', 'is_usable', true)
|
||||
->whereRelation('settings', 'is_swarm_worker', false)
|
||||
->whereHas('settings', fn (Builder $settings) => $settings
|
||||
->where('server_role', '!=', ServerRole::DEPLOYMENT->value)
|
||||
->orWhereNull('server_role'))
|
||||
->whereRelation('settings', 'force_disabled', false);
|
||||
|
||||
return self::whereServerRole($query, ServerRole::BUILD);
|
||||
}
|
||||
|
||||
public function isForceDisabled()
|
||||
|
||||
@@ -401,7 +401,8 @@
|
||||
@if (
|
||||
$application->destination->server->isSwarm() ||
|
||||
$application->additional_servers->count() > 0 ||
|
||||
$application->settings->is_build_server_enabled)
|
||||
$application->settings->is_build_server_enabled ||
|
||||
! $application->destination->server->canBuildApplications())
|
||||
<x-forms.input id="dockerRegistryImageName" required label="Image"
|
||||
placeholder="ghcr.io/your-org/your-app" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="dockerRegistryImageTag"
|
||||
|
||||
@@ -61,3 +61,16 @@ test('onboarding uses the reusable advanced settings component', function () {
|
||||
->and($onboarding)
|
||||
->toContain('<x-forms.collapsible title="Advanced Connection Settings"');
|
||||
});
|
||||
|
||||
test('deployments-only servers require a registry image because builds run elsewhere', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
|
||||
$requiredImageCondition = str($view)
|
||||
->after("@if (\$application->build_pack === 'dockerimage')")
|
||||
->after('@else')
|
||||
->before('<x-forms.input id="dockerRegistryImageName" required label="Image"')
|
||||
->toString();
|
||||
|
||||
expect($requiredImageCondition)
|
||||
->toContain('$application->settings->is_build_server_enabled')
|
||||
->toContain('! $application->destination->server->canBuildApplications()');
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ afterEach(function () {
|
||||
Server::flushIdentityMap();
|
||||
});
|
||||
|
||||
function makeBuildServerSelectionJob(Team $team, Server $deploymentServer): array
|
||||
function makeBuildServerSelectionJob(Team $team, Server $deploymentServer, array $applicationAttributes = [], bool $buildServerEnabled = true, bool $restartOnly = false): array
|
||||
{
|
||||
$project = new Project;
|
||||
$project->setRelation('team', $team);
|
||||
@@ -32,9 +32,10 @@ function makeBuildServerSelectionJob(Team $team, Server $deploymentServer): arra
|
||||
$environment->setRelation('project', $project);
|
||||
|
||||
$settings = new ApplicationSetting;
|
||||
$settings->is_build_server_enabled = true;
|
||||
$settings->is_build_server_enabled = $buildServerEnabled;
|
||||
|
||||
$application = new Application;
|
||||
$application->forceFill(array_merge(['build_pack' => 'nixpacks'], $applicationAttributes));
|
||||
$application->setRelation('environment', $environment);
|
||||
$application->setRelation('settings', $settings);
|
||||
|
||||
@@ -45,6 +46,7 @@ function makeBuildServerSelectionJob(Team $team, Server $deploymentServer): arra
|
||||
'application' => $application,
|
||||
'application_deployment_queue' => $deploymentQueue,
|
||||
'server' => $deploymentServer,
|
||||
'restart_only' => $restartOnly,
|
||||
] as $property => $value) {
|
||||
(new ReflectionProperty(ApplicationDeploymentJob::class, $property))->setValue($job, $value);
|
||||
}
|
||||
@@ -62,6 +64,26 @@ function selectedBuildServer(ApplicationDeploymentJob $job): Server
|
||||
return (new ReflectionProperty(ApplicationDeploymentJob::class, 'build_server'))->getValue($job);
|
||||
}
|
||||
|
||||
function usesRemoteBuildServer(ApplicationDeploymentJob $job): bool
|
||||
{
|
||||
return (new ReflectionProperty(ApplicationDeploymentJob::class, 'use_build_server'))->getValue($job);
|
||||
}
|
||||
|
||||
function makeRoleServer(Team $team, string $role): Server
|
||||
{
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'server_role' => $role,
|
||||
'is_build_server' => $role === 'build',
|
||||
'is_swarm_worker' => false,
|
||||
'force_disabled' => false,
|
||||
]);
|
||||
|
||||
return $server->fresh();
|
||||
}
|
||||
|
||||
test('teams allow deployment server fallback by default', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
@@ -136,28 +158,144 @@ test('strict teams use an available dedicated build server', function () {
|
||||
expect(selectedBuildServer($job)->is($buildServer))->toBeTrue();
|
||||
});
|
||||
|
||||
test('a combined deployment server builds locally without remote build handling', function () {
|
||||
test('strict teams do not treat combined servers as dedicated build servers', function () {
|
||||
$team = Team::factory()->create(['is_build_server_fallback_enabled' => false]);
|
||||
$deploymentServer = Server::factory()->create(['team_id' => $team->id]);
|
||||
$deploymentServer->settings()->update([
|
||||
'server_role' => 'both',
|
||||
'is_build_server' => false,
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'is_swarm_worker' => false,
|
||||
'force_disabled' => false,
|
||||
]);
|
||||
$deploymentServer = makeRoleServer($team, 'both');
|
||||
makeRoleServer($team, 'both');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('setAttribute');
|
||||
$deploymentQueue->shouldReceive('addLogEntry')
|
||||
->once()
|
||||
->with("Using deployment server ({$deploymentServer->name}) for the build.");
|
||||
$deploymentQueue->shouldNotReceive('addLogEntry');
|
||||
|
||||
expect(fn () => invokeBuildServerSelection($job))
|
||||
->toThrow(DeploymentException::class, 'No available dedicated build server was found.');
|
||||
});
|
||||
|
||||
test('build server selection only picks dedicated build servers', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = makeRoleServer($team, 'both');
|
||||
makeRoleServer($team, 'both');
|
||||
makeRoleServer($team, 'both');
|
||||
makeRoleServer($team, 'deployment');
|
||||
$buildServer = makeRoleServer($team, 'build');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer);
|
||||
|
||||
$deploymentQueue->shouldReceive('setAttribute')->with('build_server_id', $buildServer->id)->once()->andReturnSelf();
|
||||
$deploymentQueue->shouldReceive('addLogEntry')->once()->with("Found a suitable build server ({$buildServer->name}).");
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
$useBuildServer = (new ReflectionProperty(ApplicationDeploymentJob::class, 'use_build_server'))->getValue($job);
|
||||
expect(selectedBuildServer($job)->is($buildServer))->toBeTrue()
|
||||
->and(usesRemoteBuildServer($job))->toBeTrue();
|
||||
});
|
||||
|
||||
test('fallback builds on the deployment server instead of another combined server', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = makeRoleServer($team, 'both');
|
||||
makeRoleServer($team, 'both');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('setAttribute');
|
||||
$deploymentQueue->shouldReceive('addLogEntry')->once()->with('No suitable build server found. Using the deployment server.');
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect(selectedBuildServer($job)->is($deploymentServer))->toBeTrue()
|
||||
->and($useBuildServer)->toBeFalse();
|
||||
->and(usesRemoteBuildServer($job))->toBeFalse();
|
||||
});
|
||||
|
||||
test('deployments-only servers build on a dedicated build server without the application setting', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = makeRoleServer($team, 'deployment');
|
||||
makeRoleServer($team, 'both');
|
||||
$buildServer = makeRoleServer($team, 'build');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, ['docker_registry_image_name' => 'ghcr.io/coollabsio/app'], buildServerEnabled: false);
|
||||
|
||||
$deploymentQueue->shouldReceive('setAttribute')->with('build_server_id', $buildServer->id)->once()->andReturnSelf();
|
||||
$deploymentQueue->shouldReceive('addLogEntry')->once()->with("Found a suitable build server ({$buildServer->name}).");
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect(selectedBuildServer($job)->is($buildServer))->toBeTrue()
|
||||
->and(usesRemoteBuildServer($job))->toBeTrue();
|
||||
});
|
||||
|
||||
test('deployments-only servers never fall back to building themselves', function (bool $buildServerEnabled) {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = makeRoleServer($team, 'deployment');
|
||||
makeRoleServer($team, 'both');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, ['docker_registry_image_name' => 'ghcr.io/coollabsio/app'], $buildServerEnabled);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('addLogEntry');
|
||||
|
||||
expect(fn () => invokeBuildServerSelection($job))
|
||||
->toThrow(DeploymentException::class, "The deployment server ({$deploymentServer->name}) is set to deployments only");
|
||||
})->with([
|
||||
'application uses a build server' => [true],
|
||||
'application builds on the deployment server' => [false],
|
||||
]);
|
||||
|
||||
test('deployments-only servers need a registry image for remote builds', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = makeRoleServer($team, 'deployment');
|
||||
makeRoleServer($team, 'build');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, buildServerEnabled: false);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('addLogEntry');
|
||||
|
||||
expect(fn () => invokeBuildServerSelection($job))
|
||||
->toThrow(DeploymentException::class, 'Set a Docker image name');
|
||||
});
|
||||
|
||||
test('deployments-only servers restart without a registry image', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = makeRoleServer($team, 'deployment');
|
||||
$buildServer = makeRoleServer($team, 'build');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, buildServerEnabled: false, restartOnly: true);
|
||||
|
||||
$deploymentQueue->shouldReceive('setAttribute')->with('build_server_id', $buildServer->id)->once()->andReturnSelf();
|
||||
$deploymentQueue->shouldReceive('addLogEntry')->once()->with("Found a suitable build server ({$buildServer->name}).");
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect(selectedBuildServer($job)->is($buildServer))->toBeTrue();
|
||||
});
|
||||
|
||||
test('deployments-only servers can restart when no build server is available', function () {
|
||||
$team = Team::factory()->create();
|
||||
$deploymentServer = makeRoleServer($team, 'deployment');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, buildServerEnabled: false, restartOnly: true);
|
||||
|
||||
$deploymentQueue->shouldReceive('addLogEntry')->once()->with('No suitable build server found. Using the deployment server.');
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect(selectedBuildServer($job)->is($deploymentServer))->toBeTrue()
|
||||
->and(usesRemoteBuildServer($job))->toBeFalse();
|
||||
});
|
||||
|
||||
test('deployments-only servers do not need a build server when nothing is built', function (string $buildPack) {
|
||||
$team = Team::factory()->create(['is_build_server_fallback_enabled' => false]);
|
||||
$deploymentServer = makeRoleServer($team, 'deployment');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, ['build_pack' => $buildPack], buildServerEnabled: false);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('addLogEntry');
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect(selectedBuildServer($job)->is($deploymentServer))->toBeTrue()
|
||||
->and(usesRemoteBuildServer($job))->toBeFalse();
|
||||
})->with(['dockerimage', 'dockercompose']);
|
||||
|
||||
test('applications on combined servers build locally unless they use a build server', function () {
|
||||
$team = Team::factory()->create(['is_build_server_fallback_enabled' => false]);
|
||||
$deploymentServer = makeRoleServer($team, 'both');
|
||||
makeRoleServer($team, 'build');
|
||||
[$job, $deploymentQueue] = makeBuildServerSelectionJob($team, $deploymentServer, buildServerEnabled: false);
|
||||
|
||||
$deploymentQueue->shouldNotReceive('addLogEntry');
|
||||
|
||||
invokeBuildServerSelection($job);
|
||||
|
||||
expect(selectedBuildServer($job)->is($deploymentServer))->toBeTrue()
|
||||
->and(usesRemoteBuildServer($job))->toBeFalse();
|
||||
});
|
||||
|
||||
@@ -86,7 +86,34 @@ test('legacy server settings with a null role use the combined role', function (
|
||||
expect($this->server->settings->effectiveServerRole()->value)->toBe('both')
|
||||
->and($this->server->canHostResources())->toBeTrue()
|
||||
->and(Server::isUsable()->pluck('id'))->toContain($this->server->id)
|
||||
->and(Server::isUsableBuildServer()->pluck('id'))->toContain($this->server->id);
|
||||
->and(Server::isUsableBuildServer()->pluck('id'))->not->toContain($this->server->id)
|
||||
->and(Server::buildServers($this->team->id)->pluck('id'))->not->toContain($this->server->id);
|
||||
});
|
||||
|
||||
test('legacy build servers with a null role stay build only', function () {
|
||||
$this->actingAs($this->user);
|
||||
$this->server->settings()->update(['server_role' => null, 'is_build_server' => true]);
|
||||
$this->server->refresh()->load('settings');
|
||||
|
||||
expect($this->server->settings->effectiveServerRole()->value)->toBe('build')
|
||||
->and(Server::isUsable()->pluck('id'))->not->toContain($this->server->id)
|
||||
->and(Server::isUsableBuildServer()->pluck('id'))->toContain($this->server->id)
|
||||
->and(Server::buildServers($this->team->id)->pluck('id'))->toContain($this->server->id);
|
||||
});
|
||||
|
||||
test('deployments only requires a dedicated build server, not a combined server', function () {
|
||||
$combinedServer = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$combinedServer->settings()->update(['server_role' => 'both',
|
||||
'is_build_server' => false, 'is_reachable' => true, 'is_usable' => true]);
|
||||
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Show::class, ['server_uuid' => $this->server->uuid])
|
||||
->set('serverRole', 'deployment')
|
||||
->call('requestServerRoleChange')
|
||||
->assertSet('serverRole', 'both')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect($this->server->settings->fresh()->server_role->value)->toBe('both');
|
||||
});
|
||||
|
||||
test('changing from build only to deployments and builds requires confirmation', function () {
|
||||
@@ -169,11 +196,31 @@ test('resource selection keeps excluded build servers visible for explanation',
|
||||
expect($component->servers->pluck('id'))->toContain($this->server->id)
|
||||
->not->toContain($buildServer->id)
|
||||
->and(Server::isUsableBuildServer()->pluck('id'))->toContain($buildServer->id)
|
||||
->toContain($this->server->id)
|
||||
->not->toContain($this->server->id)
|
||||
->and($component->buildServers->pluck('id'))->toContain($buildServer->id)
|
||||
->and($component->allServers->pluck('id'))->toContain($this->server->id, $buildServer->id);
|
||||
});
|
||||
|
||||
test('resource selection lists each combined server once', function () {
|
||||
$this->actingAs($this->user);
|
||||
$otherServer = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$otherServer->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'server_role' => 'both',
|
||||
'is_build_server' => false,
|
||||
'is_swarm_worker' => false,
|
||||
'force_disabled' => false,
|
||||
]);
|
||||
|
||||
$component = new ResourceSelect;
|
||||
$component->loadServers();
|
||||
|
||||
expect($component->buildServers)->toBeEmpty()
|
||||
->and($component->servers->pluck('id')->all())->toEqualCanonicalizing([$this->server->id, $otherServer->id])
|
||||
->and($component->allServers->pluck('id')->duplicates())->toBeEmpty();
|
||||
});
|
||||
|
||||
test('resource selection does not show the empty server message when only build servers are available', function () {
|
||||
$html = Blade::render(
|
||||
file_get_contents(resource_path('views/livewire/project/new/select.blade.php')),
|
||||
|
||||
@@ -134,7 +134,20 @@ it('rejects the legacy build server API field', function () {
|
||||
->assertJsonValidationErrors('is_build_server');
|
||||
});
|
||||
|
||||
it('requires another build-capable server before selecting deployment only', function () {
|
||||
it('requires a dedicated build server before selecting deployment only', function () {
|
||||
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
|
||||
'server_role' => 'deployment',
|
||||
])->assertUnprocessable()
|
||||
->assertJsonValidationErrors('server_role');
|
||||
|
||||
$combinedServer = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$combinedServer->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'server_role' => 'both',
|
||||
'is_build_server' => false,
|
||||
]);
|
||||
|
||||
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
|
||||
'server_role' => 'deployment',
|
||||
])->assertUnprocessable()
|
||||
@@ -165,7 +178,22 @@ it('creates a server with an API server role', function () {
|
||||
|
||||
$server = Server::whereUuid($response->json('uuid'))->firstOrFail();
|
||||
|
||||
expect($server->settings->server_role->value)->toBe('build');
|
||||
expect($server->settings->server_role->value)->toBe('build')
|
||||
->and($server->settings->is_build_server)->toBeTrue();
|
||||
});
|
||||
|
||||
it('keeps the legacy build server flag in sync when the API changes the role', function () {
|
||||
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
|
||||
'server_role' => 'build',
|
||||
])->assertCreated();
|
||||
|
||||
expect($this->server->settings->fresh()->is_build_server)->toBeTrue();
|
||||
|
||||
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
|
||||
'server_role' => 'both',
|
||||
])->assertCreated();
|
||||
|
||||
expect($this->server->settings->fresh()->is_build_server)->toBeFalse();
|
||||
});
|
||||
|
||||
it('rejects an invalid API server role', function () {
|
||||
|
||||
Reference in New Issue
Block a user