diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 16a7b6f71..26378c3bd 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -388,7 +388,11 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false, int $p continue; } } - ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force, $pr); + $result = $this->deploy_resource($resource, $force, $pr); + if (isset($result['status']) && $result['status'] === 429) { + return response()->json(['message' => $result['message']], 429); + } + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $result; if ($deployment_uuid) { $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); } else { @@ -430,7 +434,11 @@ public function by_tags(string $tags, int $team_id, bool $force = false) continue; } foreach ($applications as $resource) { - ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); + $result = $this->deploy_resource($resource, $force); + if (isset($result['status']) && $result['status'] === 429) { + return response()->json(['message' => $result['message']], 429); + } + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $result; if ($deployment_uuid) { $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]); } @@ -474,8 +482,11 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar deployment_uuid: $deployment_uuid, force_rebuild: $force, pull_request_id: $pr, + is_api: true, ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return ['message' => $result['message'], 'deployment_uuid' => null, 'status' => 429]; + } elseif ($result['status'] === 'skipped') { $message = $result['message']; } else { $message = "Application {$resource->name} deployment queued."; diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 078494f82..5410564c8 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -107,7 +107,9 @@ public function manual(Request $request) force_rebuild: false, is_webhook: true ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', @@ -161,7 +163,9 @@ public function manual(Request $request) is_webhook: true, git_type: 'bitbucket' ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 3e0c5a0b6..8f9cdba0c 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -123,7 +123,9 @@ public function manual(Request $request) commit: data_get($payload, 'after', 'HEAD'), is_webhook: true, ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', @@ -193,7 +195,9 @@ public function manual(Request $request) is_webhook: true, git_type: 'gitea' ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index a1fcaa7f5..e0ccf0850 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -136,7 +136,9 @@ public function manual(Request $request) commit: data_get($payload, 'after', 'HEAD'), is_webhook: true, ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', @@ -222,7 +224,9 @@ public function manual(Request $request) is_webhook: true, git_type: 'github' ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', @@ -427,12 +431,15 @@ public function normal(Request $request) force_rebuild: false, is_webhook: true, ); + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } $return_payloads->push([ 'status' => $result['status'], 'message' => $result['message'], 'application_uuid' => $application->uuid, 'application_name' => $application->name, - 'deployment_uuid' => $result['deployment_uuid'], + 'deployment_uuid' => $result['deployment_uuid'] ?? null, ]); } else { $paths = str($application->watch_paths)->explode("\n"); @@ -491,7 +498,9 @@ public function normal(Request $request) is_webhook: true, git_type: 'github' ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index 3187663d4..004ab0e59 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -149,7 +149,9 @@ public function manual(Request $request) force_rebuild: false, is_webhook: true, ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'status' => $result['status'], 'message' => $result['message'], @@ -220,7 +222,9 @@ public function manual(Request $request) is_webhook: true, git_type: 'gitlab' ); - if ($result['status'] === 'skipped') { + if ($result['status'] === 'queue_full') { + return response($result['message'], 429); + } elseif ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, 'status' => 'skipped', diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index bcd7a729d..b6facba22 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1813,7 +1813,7 @@ private function health_check() $this->application->update(['status' => 'running']); $this->application_deployment_queue->addLogEntry('New container is healthy.'); break; - } elseif (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { + } elseif (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { $this->newVersionIsHealthy = false; $this->application_deployment_queue->addLogEntry('New container is unhealthy.', type: 'error'); $this->query_logs(); diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index fc63c7f4b..523383e2b 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -100,6 +100,11 @@ public function deploy(bool $force_rebuild = false) deployment_uuid: $this->deploymentUuid, force_rebuild: $force_rebuild, ); + if ($result['status'] === 'queue_full') { + $this->dispatch('error', 'Deployment queue full', $result['message']); + + return; + } if ($result['status'] === 'skipped') { $this->dispatch('error', 'Deployment skipped', $result['message']); @@ -151,6 +156,11 @@ public function restart() deployment_uuid: $this->deploymentUuid, restart_only: true, ); + if ($result['status'] === 'queue_full') { + $this->dispatch('error', 'Deployment queue full', $result['message']); + + return; + } if ($result['status'] === 'skipped') { $this->dispatch('success', 'Deployment skipped', $result['message']); diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 45371678b..41f352c14 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -249,6 +249,11 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu pull_request_id: $pull_request_id, git_type: $found->git_type ?? null, ); + if ($result['status'] === 'queue_full') { + $this->dispatch('error', 'Deployment queue full', $result['message']); + + return; + } if ($result['status'] === 'skipped') { $this->dispatch('success', 'Deployment skipped', $result['message']); diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php index da67a5707..c915ef212 100644 --- a/app/Livewire/Project/Application/Rollback.php +++ b/app/Livewire/Project/Application/Rollback.php @@ -30,7 +30,7 @@ public function rollbackImage($commit) $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $this->application, deployment_uuid: $deployment_uuid, commit: $commit, @@ -38,6 +38,12 @@ public function rollbackImage($commit) force_rebuild: false, ); + if ($result['status'] === 'queue_full') { + $this->dispatch('error', 'Deployment queue full', $result['message']); + + return; + } + return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 40291d2b0..28e3f23e7 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -89,6 +89,11 @@ public function redeploy(int $network_id, int $server_id) only_this_server: true, no_questions_asked: true, ); + if ($result['status'] === 'queue_full') { + $this->dispatch('error', 'Deployment queue full', $result['message']); + + return; + } if ($result['status'] === 'skipped') { $this->dispatch('success', 'Deployment skipped', $result['message']); diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index 8d17bb557..dba1b4903 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -24,6 +24,9 @@ class Advanced extends Component #[Validate(['integer', 'min:1'])] public int $dynamicTimeout = 1; + #[Validate(['integer', 'min:1'])] + public int $deploymentQueueLimit = 25; + public function mount(string $server_uuid) { try { @@ -43,12 +46,14 @@ public function syncData(bool $toModel = false) $this->validate(); $this->server->settings->concurrent_builds = $this->concurrentBuilds; $this->server->settings->dynamic_timeout = $this->dynamicTimeout; + $this->server->settings->deployment_queue_limit = $this->deploymentQueueLimit; $this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold; $this->server->settings->server_disk_usage_check_frequency = $this->serverDiskUsageCheckFrequency; $this->server->settings->save(); } else { $this->concurrentBuilds = $this->server->settings->concurrent_builds; $this->dynamicTimeout = $this->server->settings->dynamic_timeout; + $this->deploymentQueueLimit = $this->server->settings->deployment_queue_limit; $this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold; $this->serverDiskUsageCheckFrequency = $this->server->settings->server_disk_usage_check_frequency; } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 6da4dd4c6..af301d891 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -13,6 +13,7 @@ properties: [ 'id' => ['type' => 'integer'], 'concurrent_builds' => ['type' => 'integer'], + 'deployment_queue_limit' => ['type' => 'integer'], 'dynamic_timeout' => ['type' => 'integer'], 'force_disabled' => ['type' => 'boolean'], 'force_server_cleanup' => ['type' => 'boolean'], diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 7a36c4b63..03c53989c 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -28,6 +28,20 @@ function queue_application_deployment(Application $application, string $deployme $destination_id = $destination->id; } + // Check if the deployment queue is full for this server + $serverForQueueCheck = $server ?? Server::find($server_id); + $queue_limit = $serverForQueueCheck->settings->deployment_queue_limit ?? 25; + $queued_count = ApplicationDeploymentQueue::where('server_id', $server_id) + ->where('status', ApplicationDeploymentStatus::QUEUED->value) + ->count(); + + if ($queued_count >= $queue_limit) { + return [ + 'status' => 'queue_full', + 'message' => 'Deployment queue is full. Please wait for existing deployments to complete.', + ]; + } + // Check if there's already a deployment in progress or queued for this application and commit $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) ->where('commit', $commit) diff --git a/database/migrations/2025_12_04_134435_add_deployment_queue_limit_to_server_settings.php b/database/migrations/2025_12_04_134435_add_deployment_queue_limit_to_server_settings.php new file mode 100644 index 000000000..a1bcab5bb --- /dev/null +++ b/database/migrations/2025_12_04_134435_add_deployment_queue_limit_to_server_settings.php @@ -0,0 +1,28 @@ +integer('deployment_queue_limit')->default(25)->after('concurrent_builds'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('deployment_queue_limit'); + }); + } +}; diff --git a/openapi.json b/openapi.json index dd3c6783a..1c2a1e61e 100644 --- a/openapi.json +++ b/openapi.json @@ -9816,6 +9816,9 @@ "concurrent_builds": { "type": "integer" }, + "deployment_queue_limit": { + "type": "integer" + }, "dynamic_timeout": { "type": "integer" }, diff --git a/openapi.yaml b/openapi.yaml index 754b7ec6f..bbd9294c1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -6312,6 +6312,8 @@ components: type: integer concurrent_builds: type: integer + deployment_queue_limit: + type: integer dynamic_timeout: type: integer force_disabled: diff --git a/resources/views/livewire/server/advanced.blade.php b/resources/views/livewire/server/advanced.blade.php index 6622961c5..33086aea1 100644 --- a/resources/views/livewire/server/advanced.blade.php +++ b/resources/views/livewire/server/advanced.blade.php @@ -36,6 +36,9 @@ +