diff --git a/app/Http/Livewire/Project/Application/Previews.php b/app/Http/Livewire/Project/Application/Previews.php index acb7268c5..197fb74ed 100644 --- a/app/Http/Livewire/Project/Application/Previews.php +++ b/app/Http/Livewire/Project/Application/Previews.php @@ -38,7 +38,7 @@ class Previews extends Component public function load_prs() { try { - ['rate_limit_remaining' => $rate_limit_remaining, 'data' => $data] = get_from_git_api($this->application->source, "/repos/{$this->application->git_repository}/pulls"); + ['rate_limit_remaining' => $rate_limit_remaining, 'data' => $data] = git_api(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) { diff --git a/app/Http/Livewire/Project/New/PublicGitRepository.php b/app/Http/Livewire/Project/New/PublicGitRepository.php index e048b0106..94e5bd258 100644 --- a/app/Http/Livewire/Project/New/PublicGitRepository.php +++ b/app/Http/Livewire/Project/New/PublicGitRepository.php @@ -66,7 +66,7 @@ class PublicGitRepository extends Component $this->get_git_source(); try { - ['data' => $data] = get_from_git_api($this->git_source, "/repos/{$this->git_repository}/branches"); + ['data' => $data] = git_api(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches"); $this->branches = collect($data)->pluck('name')->toArray(); } catch (\Throwable $e) { return general_error_handler(err: $e, that: $this); diff --git a/app/Jobs/ApplicationContainerStatusJob.php b/app/Jobs/ApplicationContainerStatusJob.php index 5790d4cbf..004f1f5a0 100644 --- a/app/Jobs/ApplicationContainerStatusJob.php +++ b/app/Jobs/ApplicationContainerStatusJob.php @@ -34,7 +34,6 @@ class ApplicationContainerStatusJob implements ShouldQueue, ShouldBeUnique { try { $status = get_container_status(server: $this->application->destination->server, container_id: $this->container_name, throwError: false); - ray('ApplicationContainerStatusJob', $status); if ($this->pull_request_id) { $preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); $preview->status = $status; diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index cb22601b6..44388e6cc 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -92,13 +92,29 @@ class ApplicationDeploymentJob implements ShouldQueue public function handle(): void { try { + ray()->clearScreen(); if ($this->application->deploymentType() === 'source') { $this->source = $this->application->source->getMorphClass()::where('id', $this->application->source->id)->first(); } $this->workdir = "/artifacts/{$this->deployment_uuid}"; + if ($this->pull_request_id !== 0) { ray('Deploying pull/' . $this->pull_request_id . '/head for application: ' . $this->application->name); + if ($this->application->fqdn) { + $preview_fqdn = data_get($this->preview, 'fqdn'); + $template = $this->application->preview_url_template; + $url = Url::fromString($this->application->fqdn); + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2(7); + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $this->preview->fqdn = $preview_fqdn; + $this->preview->save(); + } $this->deploy_pull_request(); } else { $this->deploy(); @@ -203,6 +219,12 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory} } private function deploy_pull_request() { + dispatch(new ApplicationPullRequestUpdateJob( + application_id: $this->application->id, + pull_request_id: $this->pull_request_id, + deployment_uuid: $this->deployment_uuid, + status: 'in_progress' + )); $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build"; $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}"; $this->container_name = generate_container_name($this->application->uuid, $this->pull_request_id); @@ -282,12 +304,12 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory} ]); $this->activity->save(); } - dispatch(new ApplicationContainerStatusJob( - application: $this->application, - container_name: $this->container_name, - pull_request_id: $this->pull_request_id + dispatch(new ApplicationPullRequestUpdateJob( + application_id: $this->application->id, + pull_request_id: $this->pull_request_id, + deployment_uuid: $this->deployment_uuid, + status: $status )); - queue_next_deployment($this->application); } private function execute_in_builder(string $command) diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php new file mode 100755 index 000000000..e0f51e0ee --- /dev/null +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -0,0 +1,82 @@ +application = Application::findOrFail($this->application_id); + $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); + + $this->build_logs_url = base_url() . "/project/{$this->application->environment->project->uuid}/{$this->application->environment->name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; + + if ($this->status === ProcessStatus::IN_PROGRESS->value) { + $this->body = "The preview deployment is in progress. 🟡\n\n"; + } + if ($this->status === ProcessStatus::FINISHED->value) { + $this->body = "The preview deployment is ready. 🟢\n\n"; + if ($this->preview->fqdn) { + $this->body .= "[Open Preview]({$this->preview->fqdn}) | "; + } + } + if ($this->status === ProcessStatus::ERROR->value) { + $this->body = "The preview deployment failed. 🔴\n\n"; + } + $this->body .= "[Open Build Logs](" . $this->build_logs_url . ")\n\n\n"; + $this->body .= "Last updated at: " . now()->toDateTimeString() . " CET"; + + ray('Updating comment', $this->body); + if ($this->preview->pull_request_issue_comment_id) { + $this->update_comment(); + } else { + $this->create_comment(); + } + } catch (\Exception $e) { + ray($e); + throw $e; + } + } + private function update_comment() + { + ['data' => $data] = git_api(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/comments/{$this->preview->pull_request_issue_comment_id}", method: 'patch', data: [ + 'body' => $this->body, + ], throwError: false); + if (data_get($data, 'message') === 'Not Found') { + ray('Comment not found. Creating new one.'); + $this->create_comment(); + } + } + private function create_comment() + { + ['data' => $data] = git_api(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/{$this->pull_request_id}/comments", method: 'post', data: [ + 'body' => $this->body, + ]); + $this->preview->pull_request_issue_comment_id = $data['id']; + $this->preview->save(); + } +} diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index f442fe3cd..910e9b73b 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -8,6 +8,7 @@ class ApplicationPreview extends BaseModel 'uuid', 'pull_request_id', 'pull_request_html_url', + 'pull_request_issue_comment_id', 'fqdn', 'status', 'application_id', diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 23061dde1..9fa586116 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -19,10 +19,18 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - Http::macro('github', function (string $api_url) { - return Http::withHeaders([ - 'Accept' => 'application/vnd.github.v3+json' - ])->baseUrl($api_url); + Http::macro('github', function (string $api_url, string|null $github_access_token = null) { + if ($github_access_token) { + return Http::withHeaders([ + 'X-GitHub-Api-Version' => '2022-11-28', + 'Accept' => 'application/vnd.github.v3+json', + 'Authorization' => "Bearer $github_access_token", + ])->baseUrl($api_url); + } else { + return Http::withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + ])->baseUrl($api_url); + } }); } } diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 3a1332e93..bbf24ff00 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -46,6 +46,7 @@ function queue_next_deployment(Application $application) application_id: $next_found->application_id, deployment_uuid: $next_found->deployment_uuid, force_rebuild: $next_found->force_rebuild, + pull_request_id: $next_found->pull_request_id )); } } diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 9ede08f22..7ba4ac076 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -2,6 +2,7 @@ use App\Models\GithubApp; use App\Models\GitlabApp; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Lcobucci\JWT\Encoding\ChainedFormatter; use Lcobucci\JWT\Encoding\JoseEncoder; @@ -47,15 +48,22 @@ function generate_github_jwt_token(GithubApp $source) return $issuedToken; } -function get_from_git_api(GithubApp|GitlabApp $source, $endpoint) +function git_api(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true) { if ($source->getMorphClass() == 'App\Models\GithubApp') { if ($source->is_public) { - $response = Http::github($source->api_url)->get($endpoint); + $response = Http::github($source->api_url)->$method($endpoint); + } else { + $github_access_token = generate_github_installation_token($source); + if ($data && ($method === 'post' || $method === 'patch' || $method === 'put')) { + $response = Http::github($source->api_url, $github_access_token)->$method($endpoint, $data); + } else { + $response = Http::github($source->api_url, $github_access_token)->$method($endpoint); + } } } $json = $response->json(); - if ($response->status() !== 200) { + if ($response->failed() && $throwError) { throw new \Exception("Failed to get data from {$source->name} with error: " . $json['message']); } return [ diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 5968caf8c..a8041da78 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -81,3 +81,16 @@ function set_transanctional_email_settings() "local_domain" => null, ]); } + +function base_url() +{ + $settings = InstanceSettings::get(); + if ($settings->fqdn) { + return $settings->fqdn; + } else { + if (config('app.env') === 'local') { + return url('/') . ':8080'; + } + return url('/'); + } +} diff --git a/database/migrations/2023_03_27_081718_create_application_previews_table.php b/database/migrations/2023_03_27_081718_create_application_previews_table.php index 2900b5791..66211eee4 100644 --- a/database/migrations/2023_03_27_081718_create_application_previews_table.php +++ b/database/migrations/2023_03_27_081718_create_application_previews_table.php @@ -16,6 +16,7 @@ return new class extends Migration $table->string('uuid')->unique(); $table->integer('pull_request_id'); $table->string('pull_request_html_url'); + $table->integer('pull_request_issue_comment_id')->nullable(); $table->string('fqdn')->unique()->nullable(); $table->string('status')->default('exited'); diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php index 8d29b5506..ea2b319c0 100644 --- a/resources/views/livewire/source/github/change.blade.php +++ b/resources/views/livewire/source/github/change.blade.php @@ -115,7 +115,7 @@ default_permissions: { contents: 'read', metadata: 'read', - pull_requests: 'read', + pull_requests: 'write', emails: 'read' }, default_events: ['pull_request', 'push'] diff --git a/routes/webhooks.php b/routes/webhooks.php index e0982d4c7..751e2faf7 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -100,14 +100,13 @@ Route::post('/source/github/events', function () { if (!$id || !$branch) { return response('Nothing to do. No id or branch found.'); } - $applications = Application::where('repository_project_id', $id); + $applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false); if ($x_github_event === 'push') { $applications = $applications->where('git_branch', $branch)->get(); } if ($x_github_event === 'pull_request') { $applications = $applications->where('git_branch', $base_branch)->get(); } - if ($applications->isEmpty()) { return response('Nothing to do. No applications found.'); } diff --git a/thunder-tests/thunderclient.json b/thunder-tests/thunderclient.json index 7c9423af7..517c5d4f8 100644 --- a/thunder-tests/thunderclient.json +++ b/thunder-tests/thunderclient.json @@ -3,7 +3,7 @@ "_id": "b3d379ab-e5e4-4ba4-991d-b6c8c6bbcb98", "colId": "e6458286-eef1-401c-be84-860b111d66f0", "containerId": "b8cfd093-5467-44a2-9221-ad0207717310", - "name": "Push", + "name": "Public Push", "url": "http://localhost:8000/webhooks/source/github/events", "method": "POST", "sortNum": 10000, @@ -51,12 +51,12 @@ "_id": "b5386afc-ad91-428f-88ac-0f449c5c26fd", "colId": "e6458286-eef1-401c-be84-860b111d66f0", "containerId": "b8cfd093-5467-44a2-9221-ad0207717310", - "name": "PR - Opened", + "name": "Public PR - Opened", "url": "http://localhost:8000/webhooks/source/github/events", "method": "POST", "sortNum": 20000, "created": "2023-05-31T08:23:28.904Z", - "modified": "2023-05-31T09:07:17.450Z", + "modified": "2023-06-13T10:01:31.875Z", "headers": [ { "name": "X-GitHub-Delivery", @@ -99,12 +99,12 @@ "_id": "7e7a3abd-dc01-454f-aa80-eaeb2c18aa56", "colId": "e6458286-eef1-401c-be84-860b111d66f0", "containerId": "b8cfd093-5467-44a2-9221-ad0207717310", - "name": "PR - Closed", + "name": "Public PR - Closed", "url": "http://localhost:8000/webhooks/source/github/events", "method": "POST", "sortNum": 30000, "created": "2023-05-31T09:15:15.833Z", - "modified": "2023-05-31T09:15:29.822Z", + "modified": "2023-06-13T08:34:27.203Z", "headers": [ { "name": "X-GitHub-Delivery",