From d8972e97c976c0111c3399bf41e38da9c19ca562 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 13 May 2026 09:31:28 +0200 Subject: [PATCH 1/9] fix(previews): clean up closed PR previews after update failures Catch and report failures while updating closed pull request status so preview deployment cleanup still runs for closed GitHub pull request webhooks. Add coverage for cleanup continuing when GitHub comment cleanup fails. --- app/Jobs/ProcessGithubPullRequestWebhook.php | 24 +++++-- .../ProcessGithubPullRequestWebhookTest.php | 72 +++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/ProcessGithubPullRequestWebhookTest.php diff --git a/app/Jobs/ProcessGithubPullRequestWebhook.php b/app/Jobs/ProcessGithubPullRequestWebhook.php index 54e386676..5a390e8ed 100644 --- a/app/Jobs/ProcessGithubPullRequestWebhook.php +++ b/app/Jobs/ProcessGithubPullRequestWebhook.php @@ -14,6 +14,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Throwable; use Visus\Cuid2\Cuid2; class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue @@ -70,16 +71,25 @@ private function handleClosedAction(Application $application): void ->first(); if ($found) { - ApplicationPullRequestUpdateJob::dispatchSync( - application: $application, - preview: $found, - status: ProcessStatus::CLOSED - ); - - CleanupPreviewDeployment::run($application, $this->pullRequestId, $found); + try { + $this->dispatchPullRequestClosedUpdate($application, $found); + } catch (Throwable $e) { + report($e); + } finally { + CleanupPreviewDeployment::run($application, $this->pullRequestId, $found); + } } } + protected function dispatchPullRequestClosedUpdate(Application $application, ApplicationPreview $preview): void + { + ApplicationPullRequestUpdateJob::dispatchSync( + application: $application, + preview: $preview, + status: ProcessStatus::CLOSED + ); + } + private function handleOpenAction(Application $application, ?GithubApp $githubApp): void { if (! $application->isPRDeployable()) { diff --git a/tests/Feature/ProcessGithubPullRequestWebhookTest.php b/tests/Feature/ProcessGithubPullRequestWebhookTest.php new file mode 100644 index 000000000..6e0e9241e --- /dev/null +++ b/tests/Feature/ProcessGithubPullRequestWebhookTest.php @@ -0,0 +1,72 @@ + InstanceSettings::firstOrCreate(['id' => 0])); + + $this->team = Team::factory()->create(); + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + + $this->application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); +}); + +it('cleans up a closed pull request preview when pull request comment cleanup fails', function () { + $preview = ApplicationPreview::create([ + 'application_id' => $this->application->id, + 'pull_request_id' => 42, + 'pull_request_html_url' => 'https://github.com/example/repo/pull/42', + ]); + + CleanupPreviewDeployment::shouldRun() + ->once() + ->withArgs(fn (Application $application, int $pullRequestId, ApplicationPreview $applicationPreview): bool => $application->is($this->application) + && $pullRequestId === 42 + && $applicationPreview->is($preview)) + ->andReturn([ + 'cancelled_deployments' => 0, + 'killed_containers' => 0, + 'status' => 'success', + ]); + + $job = new class( + applicationId: $this->application->id, + githubAppId: null, + action: 'closed', + pullRequestId: 42, + pullRequestHtmlUrl: 'https://github.com/example/repo/pull/42', + pullRequestTitle: null, + beforeSha: null, + afterSha: null, + commitSha: 'HEAD', + authorAssociation: 'OWNER', + fullName: 'example/repo', + ) extends ProcessGithubPullRequestWebhook + { + protected function dispatchPullRequestClosedUpdate(Application $application, ApplicationPreview $preview): void + { + throw new RuntimeException('GitHub comment cleanup failed.'); + } + }; + + $job->handle(); +}); From 4c77504b5ef80ca8f955ceffb006031d94f3310f Mon Sep 17 00:00:00 2001 From: vuguul <88252044+vuguul@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:29:29 -0600 Subject: [PATCH 2/9] fix(service): limit Grafana extra fields to Grafana images --- app/Models/Service.php | 11 +++++- tests/Feature/ServiceExtraFieldsTest.php | 47 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/ServiceExtraFieldsTest.php diff --git a/app/Models/Service.php b/app/Models/Service.php index cc8074b74..aba4c6e56 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -626,7 +626,7 @@ public function extraFields() } $fields->put('Unleash', $data->toArray()); break; - case $image->contains('grafana'): + case $this->isGrafanaImage($image->toString()): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first(); $data = $data->merge([ @@ -1380,6 +1380,15 @@ public function extraFields() return $fields; } + private function isGrafanaImage(string $image): bool + { + return in_array($image, [ + 'grafana/grafana', + 'grafana/grafana-oss', + 'grafana/grafana-enterprise', + ], true); + } + public function saveExtraFields($fields) { foreach ($fields as $field) { diff --git a/tests/Feature/ServiceExtraFieldsTest.php b/tests/Feature/ServiceExtraFieldsTest.php new file mode 100644 index 000000000..c90a76bc6 --- /dev/null +++ b/tests/Feature/ServiceExtraFieldsTest.php @@ -0,0 +1,47 @@ +create(); + $project = Project::factory()->create(['team_id' => $team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $server = Server::factory()->create(); + $destination = StandaloneDocker::factory()->create(['server_id' => $server->id]); + + $service = Service::factory()->create([ + 'environment_id' => $environment->id, + 'server_id' => $server->id, + 'destination_id' => $destination->id, + 'destination_type' => StandaloneDocker::class, + ]); + + $service->applications()->create([ + 'name' => 'app', + 'image' => $image, + ]); + + return $service; +} + +it('only adds Grafana extra fields for Grafana server images', function (string $image, bool $shouldHaveGrafanaFields) { + $fields = serviceExtraFieldsTestServiceWithApplicationImage($image)->extraFields(); + + expect($fields->has('Grafana'))->toBe($shouldHaveGrafanaFields); +})->with([ + 'grafana oss' => ['grafana/grafana-oss:latest', true], + 'grafana enterprise' => ['grafana/grafana-enterprise:latest', true], + 'grafana default' => ['grafana/grafana:latest', true], + 'loki' => ['grafana/loki:latest', false], + 'promtail' => ['grafana/promtail:latest', false], + 'tempo' => ['grafana/tempo:latest', false], +]); From ee74889f190aee43e66c9def76a475b9ba073f64 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:45:06 +0200 Subject: [PATCH 3/9] feat(resources): show mobile action buttons --- .../project/application/heading.blade.php | 73 ++++++++++------ .../project/database/heading.blade.php | 29 ++++--- .../project/service/heading.blade.php | 83 ++++++++++++++----- tests/Feature/MobileResourceMenuTest.php | 17 +++- 4 files changed, 141 insertions(+), 61 deletions(-) diff --git a/resources/views/livewire/project/application/heading.blade.php b/resources/views/livewire/project/application/heading.blade.php index 17f077c57..fd37d869a 100644 --- a/resources/views/livewire/project/application/heading.blade.php +++ b/resources/views/livewire/project/application/heading.blade.php @@ -282,33 +282,54 @@ @endif - @if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))) - - @if (!str($application->status)->startsWith('exited')) - @if (!$application->destination->server->isSwarm()) - - @endif - @if ($application->build_pack !== 'dockercompose') - @if ($application->destination->server->isSwarm()) - - @else - - @endif - @endif - - @else - - @endif - @if (!$application->destination->server->isSwarm()) - @if ($application->status === 'running') - - @else - - @endif - @endif - - @endif + @if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))) +
+ @if (!str($application->status)->startsWith('exited')) + @if (!$application->destination->server->isSwarm()) + + @endif + @if ($application->build_pack !== 'dockercompose') + @if ($application->destination->server->isSwarm()) + + @else + + @endif + @endif + + @else + + @endif + @if (!$application->destination->server->isSwarm()) + @if ($application->status === 'running') + + @else + + @endif + @endif +
+ @endif - @if (!str($database->status)->startsWith('exited')) - - - @else - - @endif - - @endif + @if ($database->destination->server->isFunctional()) +
+ @if (!str($database->status)->startsWith('exited')) + + + @else + + @endif +
+ @endif