diff --git a/.ai/lessons.md b/.ai/lessons.md
index 8b620a0887..cd37f876f5 100644
--- a/.ai/lessons.md
+++ b/.ai/lessons.md
@@ -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.
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 67f9c6932f..76e36fe873 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -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,
]);
}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index e22dce607c..fb70c9c9cc 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -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;
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index cd435b56b6..a420de5b85 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -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;
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 20952d5598..3cc0ea3bbf 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -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()
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php
index 6f7323ce41..8057209805 100644
--- a/resources/views/livewire/project/application/general.blade.php
+++ b/resources/views/livewire/project/application/general.blade.php
@@ -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())
and($onboarding)
->toContain('after("@if (\$application->build_pack === 'dockerimage')")
+ ->after('@else')
+ ->before('toString();
+
+ expect($requiredImageCondition)
+ ->toContain('$application->settings->is_build_server_enabled')
+ ->toContain('! $application->destination->server->canBuildApplications()');
+});
diff --git a/tests/Feature/BuildServerFallbackPolicyTest.php b/tests/Feature/BuildServerFallbackPolicyTest.php
index 2fd3b2f72a..e698a205b4 100644
--- a/tests/Feature/BuildServerFallbackPolicyTest.php
+++ b/tests/Feature/BuildServerFallbackPolicyTest.php
@@ -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();
});
diff --git a/tests/Feature/ResourceHostingServerTest.php b/tests/Feature/ResourceHostingServerTest.php
index af64673caa..28b6bb2632 100644
--- a/tests/Feature/ResourceHostingServerTest.php
+++ b/tests/Feature/ResourceHostingServerTest.php
@@ -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')),
diff --git a/tests/Feature/ServerUpdatePrivateKeyApiTest.php b/tests/Feature/ServerUpdatePrivateKeyApiTest.php
index 2b67e7b83d..7940c0d9a1 100644
--- a/tests/Feature/ServerUpdatePrivateKeyApiTest.php
+++ b/tests/Feature/ServerUpdatePrivateKeyApiTest.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 () {