From aeeb2665cd950dbb74e78701b973217dd5a4ca58 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 21 Jul 2026 21:54:15 +0200
Subject: [PATCH] feat(github): add GitHub App connection testing
---
app/Livewire/Source/Github/Change.php | 56 +++++++
app/Models/GithubApp.php | 13 ++
.../livewire/source/github/change.blade.php | 9 +-
resources/views/source/all.blade.php | 8 +-
.../Application/GithubSourceChangeTest.php | 157 ++++++++++++++++++
5 files changed, 237 insertions(+), 6 deletions(-)
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index a24ed9ce3..1876b8c49 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -8,6 +8,7 @@
use App\Rules\SafeExternalUrl;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
@@ -79,6 +80,8 @@ class Change extends Component
public string $activeTab = 'general';
+ public bool $isConnected = false;
+
private bool $shouldDeriveApiUrlAfterHtmlUrlUpdate = false;
protected function rules(): array
@@ -230,6 +233,7 @@ public function checkPermissions()
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->syncData(false);
+ $this->isConnected = $this->github_app->isConnected();
$this->name = str($this->github_app->name)->kebab();
$this->dispatch('success', 'Github App permissions updated.');
@@ -247,6 +251,55 @@ public function checkPermissions()
}
}
+ public function testConnection()
+ {
+ try {
+ $this->authorize('view', $this->github_app);
+
+ if (! $this->github_app->isConnected()) {
+ $this->dispatch('error', 'GitHub App is not fully set up. Please complete installation first.');
+
+ return;
+ }
+
+ if (! $this->github_app->private_key_id || ! $this->github_app->privateKey) {
+ $this->dispatch('error', 'Private Key not found. Please select a valid private key.');
+
+ return;
+ }
+
+ $jwt = generateGithubJwt($this->github_app);
+ $appResponse = Http::withHeaders([
+ 'Authorization' => "Bearer $jwt",
+ 'Accept' => 'application/vnd.github+json',
+ ])->timeout(10)->get("{$this->github_app->api_url}/app");
+
+ if (! $appResponse->successful()) {
+ $error = data_get($appResponse->json(), 'message', 'Unknown error');
+ $this->dispatch('error', "Connection failed: {$error}");
+
+ return;
+ }
+
+ // Confirm installation credentials can mint an installation access token.
+ generateGithubInstallationToken($this->github_app);
+
+ $appName = data_get($appResponse->json(), 'name')
+ ?? data_get($appResponse->json(), 'slug', 'unknown');
+ $this->dispatch('success', "Connection successful! Authenticated as GitHub App: {$appName}");
+ } catch (\Throwable $e) {
+ $errorMessage = $e->getMessage();
+ if (str_contains($errorMessage, 'DECODER routines::unsupported') ||
+ str_contains($errorMessage, 'parse your key')) {
+ $this->dispatch('error', 'The selected private key format is not supported for GitHub Apps.
Please use an RSA private key in PEM format (BEGIN RSA PRIVATE KEY).
OpenSSH format keys (BEGIN OPENSSH PRIVATE KEY) are not supported.');
+
+ return;
+ }
+
+ return handleError($e, $this);
+ }
+ }
+
public function mount()
{
try {
@@ -260,6 +313,7 @@ public function mount()
// Sync data from model to properties
$this->syncData(false);
+ $this->isConnected = $this->github_app->isConnected();
// Override name with kebab case for display
$this->name = str($this->github_app->name)->kebab();
@@ -373,6 +427,7 @@ public function submit()
$this->syncData(true);
$this->github_app->save();
+ $this->isConnected = $this->github_app->isConnected();
$this->dispatch('success', 'Github App updated.');
} catch (ValidationException $e) {
throw $e;
@@ -404,6 +459,7 @@ public function instantSave()
$this->syncData(true);
$this->github_app->save();
+ $this->isConnected = $this->github_app->isConnected();
$this->dispatch('success', 'Github App updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php
index e5032d2d0..7c2f8c062 100644
--- a/app/Models/GithubApp.php
+++ b/app/Models/GithubApp.php
@@ -98,4 +98,17 @@ public function type(): Attribute
},
);
}
+
+ /**
+ * A private GitHub App is connected once it has been registered and installed.
+ * Public sources do not require installation credentials.
+ */
+ public function isConnected(): bool
+ {
+ if ($this->is_public) {
+ return true;
+ }
+
+ return filled($this->app_id) && filled($this->installation_id);
+ }
}
diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php
index 051f9809f..3b0ac9388 100644
--- a/resources/views/livewire/source/github/change.blade.php
+++ b/resources/views/livewire/source/github/change.blade.php
@@ -2,11 +2,18 @@
@if (data_get($github_app, 'app_id'))