'refreshPreviewDomains']; public Application $application; public bool $isPreviewDeploymentsEnabled = false; public bool $isPrDeploymentsPublicEnabled = false; public string $deployment_uuid; public array $parameters; public Collection $pull_requests; public int $rate_limit_remaining; public array $previewDockerTags = []; public ?int $manualPullRequestId = null; public ?string $manualDockerTag = null; protected $rules = [ 'previewDockerTags.*' => 'string|nullable', 'manualPullRequestId' => 'integer|min:1|nullable', 'manualDockerTag' => 'string|nullable', ]; public function mount() { $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled; $this->isPrDeploymentsPublicEnabled = $this->application->settings->is_pr_deployments_public_enabled ?? false; $this->pull_requests = collect(); $this->parameters = get_route_parameters(); $this->syncDockerTags(); } public function savePreviewSettings(): void { $this->authorize('update', $this->application); $this->validate([ 'isPreviewDeploymentsEnabled' => 'boolean', 'isPrDeploymentsPublicEnabled' => 'boolean', ]); $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled; $this->application->settings->is_pr_deployments_public_enabled = $this->isPrDeploymentsPublicEnabled; $this->application->settings->save(); $this->dispatch('success', 'Settings saved.'); $this->dispatch('configurationChanged'); } public function togglePreviewDeployments(): void { $this->authorize('update', $this->application); $this->isPreviewDeploymentsEnabled = ! $this->isPreviewDeploymentsEnabled; $this->savePreviewSettings(); } private function syncDockerTags(): void { $this->previewDockerTags = []; foreach ($this->application->previews as $key => $preview) { $this->previewDockerTags[$key] = $preview->docker_registry_image_tag; } } public function refreshPreviewDomains(): void { $this->application->refresh(); $this->syncDockerTags(); } public function load_prs() { try { $this->authorize('update', $this->application); ['rate_limit_remaining' => $rate_limit_remaining, 'data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/pulls"); $this->rate_limit_remaining = $rate_limit_remaining; $this->pull_requests = $data->sortBy('number')->values(); } catch (\Throwable $e) { $this->rate_limit_remaining = 0; return handleError($e, $this); } } public function save_preview($preview_id) { try { $this->authorize('update', $this->application); $preview = $this->application->previews->find($preview_id); if (! $preview) { throw new \Exception('Preview not found'); } $previewKey = $this->application->previews->search(function ($item) use ($preview_id) { return $item->id == $preview_id; }); if ($previewKey === false) { throw new \Exception('Preview not found'); } $this->validateOnly("previewDockerTags.{$previewKey}"); $preview->docker_registry_image_tag = $this->previewDockerTags[$previewKey] ?? null; $preview->save(); $this->dispatch('success', 'Preview saved.

Do not forget to redeploy the preview to apply the changes.'); } catch (\Throwable $e) { return handleError($e, $this); } } public function add(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null) { try { $this->authorize('update', $this->application); if ($this->application->build_pack === 'dockercompose') { $this->setDeploymentUuid(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found && ! is_null($pull_request_html_url)) { $found = ApplicationPreview::create([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, 'docker_compose_domains' => $this->application->docker_compose_domains, ]); } $found->generate_preview_fqdn_compose(); $this->application->refresh(); $this->syncDockerTags(); } else { $this->setDeploymentUuid(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) { $found = ApplicationPreview::create([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url ?? '', 'docker_registry_image_tag' => $docker_registry_image_tag, ]); } if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) { $found->docker_registry_image_tag = $docker_registry_image_tag; $found->save(); } $found->generate_preview_fqdn(generateWithoutApplicationDomain: true); $this->application->refresh(); $this->syncDockerTags(); $this->dispatch('update_links'); $this->dispatch('success', 'Preview added.'); } } catch (\Throwable $e) { return handleError($e, $this); } } public function force_deploy_without_cache(int $pull_request_id, ?string $pull_request_html_url = null) { try { $this->authorize('deploy', $this->application); $dockerRegistryImageTag = null; if ($this->application->build_pack === 'dockerimage') { $dockerRegistryImageTag = $this->application->previews() ->where('pull_request_id', $pull_request_id) ->value('docker_registry_image_tag'); } $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag); } catch (\Throwable $e) { return handleError($e, $this); } } public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null) { try { $this->authorize('deploy', $this->application); $this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag); $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag); } catch (\Throwable $e) { return handleError($e, $this); } } public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false, ?string $docker_registry_image_tag = null) { try { $this->authorize('deploy', $this->application); $this->setDeploymentUuid(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) { $found = ApplicationPreview::create([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url ?? '', 'docker_registry_image_tag' => $docker_registry_image_tag, ]); } if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) { $found->docker_registry_image_tag = $docker_registry_image_tag; $found->save(); } $result = queue_application_deployment( application: $this->application, deployment_uuid: $this->deployment_uuid, force_rebuild: $force_rebuild, pull_request_id: $pull_request_id, git_type: $found->git_type ?? null, docker_registry_image_tag: $docker_registry_image_tag, ); 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']); return; } return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], 'deployment_uuid' => $this->deployment_uuid, 'environment_uuid' => $this->parameters['environment_uuid'], ]); } catch (\Throwable $e) { return handleError($e, $this); } } protected function setDeploymentUuid() { $this->deployment_uuid = new_public_id(); $this->parameters['deployment_uuid'] = $this->deployment_uuid; } public function addDockerImagePreview() { $this->authorize('deploy', $this->application); $this->validateOnly('manualPullRequestId'); $this->validateOnly('manualDockerTag'); if ($this->application->build_pack !== 'dockerimage') { $this->dispatch('error', 'Manual Docker Image previews are only available for Docker Image applications.'); return; } if ($this->manualPullRequestId === null || str($this->manualDockerTag)->isEmpty()) { $this->dispatch('error', 'Both pull request id and docker tag are required.'); return; } $dockerTag = str($this->manualDockerTag)->trim()->value(); $this->add_and_deploy($this->manualPullRequestId, null, $dockerTag); $this->manualPullRequestId = null; $this->manualDockerTag = null; } private function stopContainers(array $containers, $server) { $containersToStop = collect($containers)->pluck('Names')->toArray(); $timeout = $this->application->settings->stopGracePeriodSeconds(); foreach ($containersToStop as $containerName) { instant_remote_process(command: [ dockerStopCommand($timeout, $containerName, $server), "docker rm -f $containerName", ], server: $server, throwError: false); } } public function stop(int $pull_request_id) { try { $this->authorize('deploy', $this->application); $server = $this->application->destination->server; if ($this->application->destination->server->isSwarm()) { instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server); } else { $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); $this->stopContainers($containers, $server); } ApplicationPreview::where('application_id', $this->application->id) ->where('pull_request_id', $pull_request_id) ->update(['status' => 'exited']); ServiceStatusChanged::dispatch($this->application->environment->project->team->id); GetContainersStatus::run($server); $this->application->refresh(); $this->dispatch('containerStatusUpdated'); $this->dispatch('success', 'Preview Deployment stopped.'); } catch (\Throwable $e) { return handleError($e, $this); } } public function delete(int $pull_request_id) { try { $this->authorize('delete', $this->application); $preview = ApplicationPreview::where('application_id', $this->application->id) ->where('pull_request_id', $pull_request_id) ->first(); if (! $preview) { $this->dispatch('error', 'Preview not found.'); return; } // Soft delete immediately for instant UI feedback $preview->delete(); // Dispatch the job for async cleanup (container stopping + force delete) DeleteResourceJob::dispatch($preview); // Refresh the application and its previews relationship to reflect the soft delete $this->application->load('previews'); $this->dispatch('update_links'); $this->dispatch('success', 'Preview deletion started. It may take a few moments to complete.'); } catch (\Throwable $e) { return handleError($e, $this); } } }