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\Models\PrivateKey;
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 @@ class Change extends Component
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 @@ class Change extends Component
}
}
+ 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 @@ class Change extends Component
// 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 @@ class Change extends Component
$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 @@ class Change extends Component
$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 @@ class GithubApp extends BaseModel
},
);
}
+
+ /**
+ * 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'))