From 89d7672253bd2d085ed9b48c3d11ddfb580ba2dd Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 13 May 2026 13:09:18 +0200 Subject: [PATCH 1/2] feat(api): add is_preview_deployments_enabled on applications --- .../Api/ApplicationsController.php | 38 +++++- .../ApplicationPreviewDeploymentsApiTest.php | 108 ++++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/ApplicationPreviewDeploymentsApiTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index eb2e7fc53..a59e71efa 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -168,6 +168,7 @@ public function applications(Request $request) 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], + 'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'], 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], @@ -335,6 +336,7 @@ public function create_public_application(Request $request) 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], + 'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'], 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], @@ -501,6 +503,7 @@ public function create_private_gh_app_application(Request $request) 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], + 'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'], 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], @@ -694,6 +697,7 @@ public function create_private_deploy_key_application(Request $request) 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], + 'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], @@ -828,6 +832,7 @@ public function create_dockerfile_application(Request $request) 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], + 'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], @@ -1011,7 +1016,7 @@ private function create_application(Request $request, $type) if ($return instanceof JsonResponse) { return $return; } - $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled']; + $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled']; $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -1057,6 +1062,7 @@ private function create_application(Request $request, $type) $isSpa = $request->is_spa; $isAutoDeployEnabled = $request->is_auto_deploy_enabled; $isForceHttpsEnabled = $request->is_force_https_enabled; + $isPreviewDeploymentsEnabled = $request->is_preview_deployments_enabled; $connectToDockerNetwork = $request->connect_to_docker_network; $customNginxConfiguration = $request->custom_nginx_configuration; $isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true); @@ -1261,6 +1267,10 @@ private function create_application(Request $request, $type) $application->settings->is_force_https_enabled = $isForceHttpsEnabled; $application->settings->save(); } + if (isset($isPreviewDeploymentsEnabled)) { + $application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled; + $application->settings->save(); + } if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); @@ -1497,6 +1507,10 @@ private function create_application(Request $request, $type) $application->settings->is_force_https_enabled = $isForceHttpsEnabled; $application->settings->save(); } + if (isset($isPreviewDeploymentsEnabled)) { + $application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled; + $application->settings->save(); + } if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); @@ -1697,6 +1711,10 @@ private function create_application(Request $request, $type) $application->settings->is_force_https_enabled = $isForceHttpsEnabled; $application->settings->save(); } + if (isset($isPreviewDeploymentsEnabled)) { + $application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled; + $application->settings->save(); + } if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); @@ -1812,6 +1830,10 @@ private function create_application(Request $request, $type) $application->settings->is_force_https_enabled = $isForceHttpsEnabled; $application->settings->save(); } + if (isset($isPreviewDeploymentsEnabled)) { + $application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled; + $application->settings->save(); + } if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); @@ -1922,6 +1944,10 @@ private function create_application(Request $request, $type) $application->settings->is_force_https_enabled = $isForceHttpsEnabled; $application->settings->save(); } + if (isset($isPreviewDeploymentsEnabled)) { + $application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled; + $application->settings->save(); + } if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); @@ -2350,6 +2376,7 @@ public function delete_by_uuid(Request $request) 'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'], 'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'], 'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'], + 'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -2494,7 +2521,7 @@ public function update_by_uuid(Request $request) $this->authorize('update', $application); $server = $application->destination->server; - $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled']; + $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled']; $validationRules = [ 'name' => 'string|max:255', @@ -2507,6 +2534,7 @@ public function update_by_uuid(Request $request) 'docker_compose_domains.*.domain' => 'string|nullable', 'custom_nginx_configuration' => 'string|nullable', 'is_http_basic_auth_enabled' => 'boolean|nullable', + 'is_preview_deployments_enabled' => 'boolean|nullable', 'http_basic_auth_username' => 'string', 'http_basic_auth_password' => 'string', ]; @@ -2738,6 +2766,7 @@ public function update_by_uuid(Request $request) $isSpa = $request->is_spa; $isAutoDeployEnabled = $request->is_auto_deploy_enabled; $isForceHttpsEnabled = $request->is_force_https_enabled; + $isPreviewDeploymentsEnabled = $request->is_preview_deployments_enabled; $connectToDockerNetwork = $request->connect_to_docker_network; $useBuildServer = $request->use_build_server; $isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled'); @@ -2767,6 +2796,11 @@ public function update_by_uuid(Request $request) $application->settings->save(); } + if (isset($isPreviewDeploymentsEnabled)) { + $application->settings->is_preview_deployments_enabled = $isPreviewDeploymentsEnabled; + $application->settings->save(); + } + if (isset($connectToDockerNetwork)) { $application->settings->connect_to_docker_network = $connectToDockerNetwork; $application->settings->save(); diff --git a/tests/Feature/ApplicationPreviewDeploymentsApiTest.php b/tests/Feature/ApplicationPreviewDeploymentsApiTest.php new file mode 100644 index 000000000..8752d81ef --- /dev/null +++ b/tests/Feature/ApplicationPreviewDeploymentsApiTest.php @@ -0,0 +1,108 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + session(['currentTeam' => $this->team]); + + $this->token = $this->user->createToken('test-token', ['*']); + $this->bearerToken = $this->token->plainTextToken; + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + + StandaloneDocker::withoutEvents(function () { + $this->destination = $this->server->standaloneDockers()->firstOrCreate( + ['network' => 'coolify'], + ['uuid' => (string) new Cuid2, 'name' => 'test-docker'] + ); + }); + + $this->project = Project::create([ + 'uuid' => (string) new Cuid2, + 'name' => 'test-project', + 'team_id' => $this->team->id, + ]); + + $this->environment = $this->project->environments()->first(); + + $this->application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); +}); + +function previewDeploymentsAuthHeaders($bearerToken): array +{ + return [ + 'Authorization' => 'Bearer '.$bearerToken, + 'Content-Type' => 'application/json', + ]; +} + +describe('PATCH /api/v1/applications/{uuid} is_preview_deployments_enabled', function () { + test('can enable preview deployments', function () { + $this->application->settings->update(['is_preview_deployments_enabled' => false]); + + $response = $this->withHeaders(previewDeploymentsAuthHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$this->application->uuid}", [ + 'is_preview_deployments_enabled' => true, + ]); + + $response->assertOk(); + + $this->application->refresh(); + expect($this->application->settings->is_preview_deployments_enabled)->toBeTrue(); + }); + + test('can disable preview deployments', function () { + $this->application->settings->update(['is_preview_deployments_enabled' => true]); + + $response = $this->withHeaders(previewDeploymentsAuthHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$this->application->uuid}", [ + 'is_preview_deployments_enabled' => false, + ]); + + $response->assertOk(); + + $this->application->refresh(); + expect($this->application->settings->is_preview_deployments_enabled)->toBeFalse(); + }); + + test('is_preview_deployments_enabled is not changed when not in request', function () { + $this->application->settings->update(['is_preview_deployments_enabled' => true]); + + $response = $this->withHeaders(previewDeploymentsAuthHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$this->application->uuid}", [ + 'name' => 'updated-name', + ]); + + $response->assertOk(); + + $this->application->refresh(); + expect($this->application->settings->is_preview_deployments_enabled)->toBeTrue(); + }); + + test('rejects non-boolean value', function () { + $response = $this->withHeaders(previewDeploymentsAuthHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$this->application->uuid}", [ + 'is_preview_deployments_enabled' => 'yes', + ]); + + $response->assertStatus(422); + }); +}); From cf12e1d7ef4f7cb11dddc2652c7a7f66d84785d9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:36:35 +0200 Subject: [PATCH 2/2] feat(api): allow preview deployments on app create Expose is_preview_deployments_enabled in the application create API schema and validation, and skip deployment configuration column casts on sqlite migrations. --- bootstrap/helpers/api.php | 2 + ...ation_deployment_configuration_columns.php | 8 ++++ openapi.json | 24 ++++++++++ openapi.yaml | 18 ++++++++ .../ApplicationPreviewDeploymentsApiTest.php | 45 ++++++++++++++++++- 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 6a288a064..757343f4d 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -97,6 +97,7 @@ function sharedDataApplications() 'is_spa' => 'boolean', 'is_auto_deploy_enabled' => 'boolean', 'is_force_https_enabled' => 'boolean', + 'is_preview_deployments_enabled' => 'boolean', 'static_image' => Rule::enum(StaticImageTypes::class), 'domains' => 'string|nullable', 'redirect' => Rule::enum(RedirectTypes::class), @@ -198,6 +199,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('is_spa'); $request->offsetUnset('is_auto_deploy_enabled'); $request->offsetUnset('is_force_https_enabled'); + $request->offsetUnset('is_preview_deployments_enabled'); $request->offsetUnset('connect_to_docker_network'); $request->offsetUnset('force_domain_override'); $request->offsetUnset('autogenerate_domain'); diff --git a/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php b/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php index 123fd226d..95500cfef 100644 --- a/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php +++ b/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php @@ -11,12 +11,20 @@ */ public function up(): void { + if (DB::getDriverName() === 'sqlite') { + return; + } + DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_snapshot TYPE text USING configuration_snapshot::text'); DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_diff TYPE text USING configuration_diff::text'); } public function down(): void { + if (DB::getDriverName() === 'sqlite') { + return; + } + DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_snapshot TYPE json USING configuration_snapshot::json'); DB::statement('ALTER TABLE application_deployment_queues ALTER COLUMN configuration_diff TYPE json USING configuration_diff::json'); } diff --git a/openapi.json b/openapi.json index ca445ade0..71a7f9c1f 100644 --- a/openapi.json +++ b/openapi.json @@ -165,6 +165,10 @@ "type": "boolean", "description": "The flag to indicate if HTTPS is forced. Defaults to true." }, + "is_preview_deployments_enabled": { + "type": "boolean", + "description": "Enable preview deployments for pull requests." + }, "static_image": { "type": "string", "enum": [ @@ -615,6 +619,10 @@ "type": "boolean", "description": "The flag to indicate if HTTPS is forced. Defaults to true." }, + "is_preview_deployments_enabled": { + "type": "boolean", + "description": "Enable preview deployments for pull requests." + }, "static_image": { "type": "string", "enum": [ @@ -1065,6 +1073,10 @@ "type": "boolean", "description": "The flag to indicate if HTTPS is forced. Defaults to true." }, + "is_preview_deployments_enabled": { + "type": "boolean", + "description": "Enable preview deployments for pull requests." + }, "static_image": { "type": "string", "enum": [ @@ -1626,6 +1638,10 @@ "type": "boolean", "description": "The flag to indicate if HTTPS is forced. Defaults to true." }, + "is_preview_deployments_enabled": { + "type": "boolean", + "description": "Enable preview deployments for pull requests." + }, "use_build_server": { "type": "boolean", "nullable": true, @@ -1961,6 +1977,10 @@ "type": "boolean", "description": "The flag to indicate if HTTPS is forced. Defaults to true." }, + "is_preview_deployments_enabled": { + "type": "boolean", + "description": "Enable preview deployments for pull requests." + }, "use_build_server": { "type": "boolean", "nullable": true, @@ -2333,6 +2353,10 @@ "type": "boolean", "description": "The flag to indicate if HTTPS is forced. Defaults to true." }, + "is_preview_deployments_enabled": { + "type": "boolean", + "description": "Enable preview deployments for pull requests." + }, "install_command": { "type": "string", "description": "The install command." diff --git a/openapi.yaml b/openapi.yaml index 6182cacd3..e8dba0a54 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -118,6 +118,9 @@ paths: is_force_https_enabled: type: boolean description: 'The flag to indicate if HTTPS is forced. Defaults to true.' + is_preview_deployments_enabled: + type: boolean + description: 'Enable preview deployments for pull requests.' static_image: type: string enum: ['nginx:alpine'] @@ -405,6 +408,9 @@ paths: is_force_https_enabled: type: boolean description: 'The flag to indicate if HTTPS is forced. Defaults to true.' + is_preview_deployments_enabled: + type: boolean + description: 'Enable preview deployments for pull requests.' static_image: type: string enum: ['nginx:alpine'] @@ -692,6 +698,9 @@ paths: is_force_https_enabled: type: boolean description: 'The flag to indicate if HTTPS is forced. Defaults to true.' + is_preview_deployments_enabled: + type: boolean + description: 'Enable preview deployments for pull requests.' static_image: type: string enum: ['nginx:alpine'] @@ -1063,6 +1072,9 @@ paths: is_force_https_enabled: type: boolean description: 'The flag to indicate if HTTPS is forced. Defaults to true.' + is_preview_deployments_enabled: + type: boolean + description: 'Enable preview deployments for pull requests.' use_build_server: type: boolean nullable: true @@ -1277,6 +1289,9 @@ paths: is_force_https_enabled: type: boolean description: 'The flag to indicate if HTTPS is forced. Defaults to true.' + is_preview_deployments_enabled: + type: boolean + description: 'Enable preview deployments for pull requests.' use_build_server: type: boolean nullable: true @@ -1507,6 +1522,9 @@ paths: is_force_https_enabled: type: boolean description: 'The flag to indicate if HTTPS is forced. Defaults to true.' + is_preview_deployments_enabled: + type: boolean + description: 'Enable preview deployments for pull requests.' install_command: type: string description: 'The install command.' diff --git a/tests/Feature/ApplicationPreviewDeploymentsApiTest.php b/tests/Feature/ApplicationPreviewDeploymentsApiTest.php index 8752d81ef..34b670690 100644 --- a/tests/Feature/ApplicationPreviewDeploymentsApiTest.php +++ b/tests/Feature/ApplicationPreviewDeploymentsApiTest.php @@ -1,7 +1,7 @@ 'file']); + + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0])); + $this->team = Team::factory()->create(); $this->user = User::factory()->create(); $this->team->members()->attach($this->user->id, ['role' => 'owner']); @@ -106,3 +110,42 @@ function previewDeploymentsAuthHeaders($bearerToken): array $response->assertStatus(422); }); }); + +describe('POST /api/v1/applications/public is_preview_deployments_enabled', function () { + test('can enable preview deployments on create', function () { + $response = $this->withHeaders(previewDeploymentsAuthHeaders($this->bearerToken)) + ->postJson('/api/v1/applications/public', [ + 'project_uuid' => $this->project->uuid, + 'environment_uuid' => $this->environment->uuid, + 'server_uuid' => $this->server->uuid, + 'git_repository' => 'https://gitlab.com/coolify/test-preview-app', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'is_preview_deployments_enabled' => true, + 'autogenerate_domain' => false, + ]); + + $response->assertCreated(); + + $application = Application::where('uuid', $response->json('uuid'))->firstOrFail(); + expect($application->settings->is_preview_deployments_enabled)->toBeTrue(); + }); + + test('rejects non-boolean value on create', function () { + $response = $this->withHeaders(previewDeploymentsAuthHeaders($this->bearerToken)) + ->postJson('/api/v1/applications/public', [ + 'project_uuid' => $this->project->uuid, + 'environment_uuid' => $this->environment->uuid, + 'server_uuid' => $this->server->uuid, + 'git_repository' => 'https://gitlab.com/coolify/test-preview-app', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'is_preview_deployments_enabled' => 'yes', + 'autogenerate_domain' => false, + ]); + + $response->assertUnprocessable(); + }); +});