From af75492c835915ab64e6837fca0c2849c0e82af0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:16:30 +0200 Subject: [PATCH] feat(deployments): add team policy for build server fallback Allow teams to choose whether deployments fall back to the deployment server when no usable dedicated build server is available. --- app/Jobs/ApplicationDeploymentJob.php | 44 ++++-- app/Livewire/Team/Index.php | 5 + app/Models/Server.php | 7 +- app/Models/Team.php | 3 + ...server_fallback_enabled_to_teams_table.php | 28 ++++ .../project/application/general.blade.php | 5 +- resources/views/livewire/team/index.blade.php | 9 ++ .../Authorization/TeamAuthorizationTest.php | 12 ++ .../Feature/BuildServerFallbackPolicyTest.php | 135 ++++++++++++++++++ 9 files changed, 231 insertions(+), 17 deletions(-) create mode 100644 database/migrations/2026_09_18_165916_add_is_build_server_fallback_enabled_to_teams_table.php create mode 100644 tests/Feature/BuildServerFallbackPolicyTest.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 99ff1ac874..7ef4774f8c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -361,21 +361,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // Check custom port ['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository(); - if (data_get($this->application, 'settings.is_build_server_enabled')) { - $teamId = data_get($this->application, 'environment.project.team.id'); - $buildServers = Server::buildServers($teamId)->get(); - if ($buildServers->count() === 0) { - $this->application_deployment_queue->addLogEntry('No suitable build server found. Using the deployment server.'); - $this->build_server = $this->server; - } else { - $this->build_server = $buildServers->random(); - $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; - } - } else { - $this->build_server = $this->server; - } + $this->selectBuildServer(); $this->detectBuildKitCapabilities(); $this->decide_what_to_do(); } catch (Exception $e) { @@ -424,6 +410,34 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } + private function selectBuildServer(): void + { + if (! data_get($this->application, 'settings.is_build_server_enabled')) { + $this->build_server = $this->server; + + return; + } + + $team = $this->application->environment->project->team; + $buildServers = Server::buildServers($team->id)->get(); + + if ($buildServers->isEmpty()) { + 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(); + $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; + } + private function detectBuildKitCapabilities(): void { $this->dockerBuildkitSupported = false; diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index abec26dc36..9ae34b5610 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -23,12 +23,15 @@ class Index extends Component public bool $is_mcp_server_enabled = true; + public bool $is_build_server_fallback_enabled = true; + protected function rules(): array { return [ 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'is_mcp_server_enabled' => 'boolean', + 'is_build_server_fallback_enabled' => 'boolean', ]; } @@ -59,6 +62,7 @@ class Index extends Component $this->team->name = $this->name; $this->team->description = $this->description; $this->team->is_mcp_server_enabled = $this->is_mcp_server_enabled; + $this->team->is_build_server_fallback_enabled = $this->is_build_server_fallback_enabled; } else { // Sync FROM model (on load/refresh) $this->name = $this->team->name; @@ -66,6 +70,7 @@ class Index extends Component // Null can appear after Team::create() when the DB default is not // hydrated onto the in-memory model stored in session. $this->is_mcp_server_enabled = (bool) ($this->team->is_mcp_server_enabled ?? true); + $this->is_build_server_fallback_enabled = (bool) ($this->team->is_build_server_fallback_enabled ?? true); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 6db5368da9..dfca0593e4 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -903,7 +903,12 @@ $siteAddress { public static function buildServers($teamId) { - return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true); + return Server::whereTeamId($teamId) + ->whereRelation('settings', 'is_reachable', true) + ->whereRelation('settings', 'is_usable', true) + ->whereRelation('settings', 'is_swarm_worker', false) + ->whereRelation('settings', 'is_build_server', true) + ->whereRelation('settings', 'force_disabled', false); } public function isForceDisabled() diff --git a/app/Models/Team.php b/app/Models/Team.php index 4cf6391231..2478028798 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -48,15 +48,18 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen 'show_boarding', 'custom_server_limit', 'is_mcp_server_enabled', + 'is_build_server_fallback_enabled', ]; protected $attributes = [ 'is_mcp_server_enabled' => true, + 'is_build_server_fallback_enabled' => true, ]; protected $casts = [ 'personal_team' => 'boolean', 'is_mcp_server_enabled' => 'boolean', + 'is_build_server_fallback_enabled' => 'boolean', ]; protected static function booted() diff --git a/database/migrations/2026_09_18_165916_add_is_build_server_fallback_enabled_to_teams_table.php b/database/migrations/2026_09_18_165916_add_is_build_server_fallback_enabled_to_teams_table.php new file mode 100644 index 0000000000..4b103d4263 --- /dev/null +++ b/database/migrations/2026_09_18_165916_add_is_build_server_fallback_enabled_to_teams_table.php @@ -0,0 +1,28 @@ +boolean('is_build_server_fallback_enabled')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('is_build_server_fallback_enabled'); + }); + } +}; diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 65d806fca9..6f7323ce41 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -287,11 +287,14 @@ ? ['value' => true, 'label' => 'Available build server (auto-select)'] : ['value' => true, 'label' => 'No build servers connected', 'disabled' => true], ]; + $buildServerFallbackPolicy = $application->environment->project->team->is_build_server_fallback_enabled + ? 'If no usable build server is available, Coolify builds on the deployment server.' + : 'If no usable build server is available, the deployment fails.'; @endphp