diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php new file mode 100644 index 000000000..e0fcf8a6c --- /dev/null +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -0,0 +1,228 @@ +header('X-Gitea-Delivery'); + if (app()->isDownForMaintenance()) { + ray('Maintenance mode is on'); + $epoch = now()->valueOf(); + $files = Storage::disk('webhooks-during-maintenance')->files(); + $gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) { + return Str::contains($file, $x_gitea_delivery); + })->first(); + if ($gitea_delivery_found) { + ray('Webhook already found'); + return; + } + $data = [ + 'attributes' => $request->attributes->all(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), + ]; + $json = json_encode($data); + Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitea::manual_{$x_gitea_delivery}", $json); + return; + } + $x_gitea_event = Str::lower($request->header('X-Gitea-Event')); + $x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256='); + $content_type = $request->header('Content-Type'); + $payload = $request->collect(); + if ($x_gitea_event === 'ping') { + // Just pong + return response('pong'); + } + + if ($content_type !== 'application/json') { + $payload = json_decode(data_get($payload, 'payload'), true); + } + if ($x_gitea_event === 'push') { + $branch = data_get($payload, 'ref'); + $full_name = data_get($payload, 'repository.full_name'); + if (Str::isMatch('/refs\/heads\/*/', $branch)) { + $branch = Str::after($branch, 'refs/heads/'); + } + $added_files = data_get($payload, 'commits.*.added'); + $removed_files = data_get($payload, 'commits.*.removed'); + $modified_files = data_get($payload, 'commits.*.modified'); + $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); + ray($changed_files); + ray('Manual Webhook Gitea Push Event with branch: ' . $branch); + } + if ($x_gitea_event === 'pull_request') { + $action = data_get($payload, 'action'); + $full_name = data_get($payload, 'repository.full_name'); + $pull_request_id = data_get($payload, 'number'); + $pull_request_html_url = data_get($payload, 'pull_request.html_url'); + $branch = data_get($payload, 'pull_request.head.ref'); + $base_branch = data_get($payload, 'pull_request.base.ref'); + ray('Webhook Gitea Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + } + if (!$branch) { + return response('Nothing to do. No branch found in the request.'); + } + $applications = Application::where('git_repository', 'like', "%$full_name%"); + if ($x_gitea_event === 'push') { + $applications = $applications->where('git_branch', $branch)->get(); + if ($applications->isEmpty()) { + return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name."); + } + } + if ($x_gitea_event === 'pull_request') { + $applications = $applications->where('git_branch', $base_branch)->get(); + if ($applications->isEmpty()) { + return response("Nothing to do. No applications found with branch '$base_branch'."); + } + } + foreach ($applications as $application) { + $webhook_secret = data_get($application, 'manual_webhook_secret_gitea'); + $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); + if (!hash_equals($x_hub_signature_256, $hmac) && !isDev()) { + ray('Invalid signature'); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Invalid signature.', + ]); + continue; + } + $isFunctional = $application->destination->server->isFunctional(); + if (!$isFunctional) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Server is not functional.', + ]); + continue; + } + if ($x_gitea_event === 'push') { + if ($application->isDeployable()) { + $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); + if ($is_watch_path_triggered || is_null($application->watch_paths)) { + ray('Deploying ' . $application->name . ' with branch ' . $branch); + $deployment_uuid = new Cuid2(7); + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + commit: data_get($payload, 'after', 'HEAD'), + is_webhook: true, + ); + $return_payloads->push([ + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } else { + $paths = str($application->watch_paths)->explode("\n"); + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Changed files do not match watch paths. Ignoring deployment.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + 'details' => [ + 'changed_files' => $changed_files, + 'watch_paths' => $paths, + ], + ]); + } + } else { + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Deployments disabled.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } + } + if ($x_gitea_event === 'pull_request') { + if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { + if ($application->isPRDeployable()) { + $deployment_uuid = new Cuid2(7); + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if (!$found) { + ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + queue_application_deployment( + application: $application, + pull_request_id: $pull_request_id, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + commit: data_get($payload, 'head.sha', 'HEAD'), + is_webhook: true, + git_type: 'gitea' + ); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Preview deployments disabled.', + ]); + } + } + if ($action === 'closed') { + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if ($found) { + $found->delete(); + $container_name = generateApplicationContainerName($application, $pull_request_id); + // ray('Stopping container: ' . $container_name); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment closed.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'No preview deployment found.', + ]); + } + } + } + } + ray($return_payloads); + return response($return_payloads); + } catch (Exception $e) { + ray($e->getMessage()); + return handleError($e); + } + } +} diff --git a/app/Livewire/Project/Shared/Webhooks.php b/app/Livewire/Project/Shared/Webhooks.php index 6bb9428d5..35a383ece 100644 --- a/app/Livewire/Project/Shared/Webhooks.php +++ b/app/Livewire/Project/Shared/Webhooks.php @@ -11,10 +11,12 @@ class Webhooks extends Component public ?string $githubManualWebhook = null; public ?string $gitlabManualWebhook = null; public ?string $bitbucketManualWebhook = null; + public ?string $giteaManualWebhook = null; protected $rules = [ 'resource.manual_webhook_secret_github' => 'nullable|string', 'resource.manual_webhook_secret_gitlab' => 'nullable|string', 'resource.manual_webhook_secret_bitbucket' => 'nullable|string', + 'resource.manual_webhook_secret_gitea' => 'nullable|string', ]; public function saveSecret() { @@ -32,6 +34,7 @@ public function mount() $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github'); $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab'); $this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket'); + $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea'); } public function render() { diff --git a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php new file mode 100644 index 000000000..716f1f44c --- /dev/null +++ b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php @@ -0,0 +1,29 @@ +string('manual_webhook_secret_gitea')->nullable(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('manual_webhook_secret_gitea'); + }); + } +}; diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php index 0fe999232..9390082c9 100644 --- a/resources/views/livewire/project/shared/webhooks.blade.php +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -40,6 +40,13 @@ label="Bitbucket Webhook Secret" id="resource.manual_webhook_secret_bitbucket"> +