diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 59dce3cf59..43c3732464 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -115,17 +115,22 @@ class GithubPrivateRepository extends Component ->where('is_public', false) ->whereNotNull('app_id') ->findOrFail($github_app_id); - $token = generateGithubInstallationToken($this->github_app); - $repositories = loadRepositoryByPage($this->github_app, $token, $this->page); - $this->total_repositories_count = $repositories['total_count']; - $this->repositories = $this->repositories->concat(collect($repositories['repositories'])); - if ($this->repositories->count() < $this->total_repositories_count) { + try { + $token = generateGithubInstallationToken($this->github_app); + $repositories = loadRepositoryByPage($this->github_app, $token, $this->page); + $this->total_repositories_count = $repositories['total_count']; + $this->repositories = $this->repositories->concat(collect($repositories['repositories'])); while ($this->repositories->count() < $this->total_repositories_count) { $this->page++; $repositories = loadRepositoryByPage($this->github_app, $token, $this->page); $this->total_repositories_count = $repositories['total_count']; $this->repositories = $this->repositories->concat(collect($repositories['repositories'])); } + } catch (\Throwable $e) { + $this->repositories = collect(); + handleError($e, $this); + + return; } $this->repositories = $this->repositories->sortBy('name'); if ($this->repositories->count() > 0) { @@ -141,12 +146,17 @@ class GithubPrivateRepository extends Component $this->selected_repository_repo = data_get($repository, 'name'); $this->branches = collect(); $this->page = 1; - $this->loadBranchByPage(); - if ($this->total_branches_count === 100) { - while ($this->total_branches_count === 100) { - $this->page++; + try { + do { $this->loadBranchByPage(); - } + $this->page++; + } while ($this->total_branches_count === 100); + } catch (\Throwable $e) { + $this->branches = collect(); + $this->total_branches_count = 0; + handleError($e, $this); + + return; } $this->branches = sortBranchesByPriority($this->branches); $defaultBranch = data_get($repository, 'default_branch', 'main'); @@ -157,7 +167,7 @@ class GithubPrivateRepository extends Component $this->detectRepository(); } - protected function loadBranchByPage() + protected function loadBranchByPage(): void { $token = generateGithubInstallationToken($this->github_app); @@ -170,7 +180,9 @@ class GithubPrivateRepository extends Component ]); $json = $response->json(); if ($response->status() !== 200) { - return $this->dispatch('error', $json['message']); + $error = data_get($json, 'message', "HTTP {$response->status()}"); + + throw new \RuntimeException("Failed to load branches of {$this->selected_repository_owner}/{$this->selected_repository_repo} with error: ".$error); } $this->total_branches_count = count($json); diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index edcbca88f6..04eebdb505 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -212,7 +212,7 @@ function generateGithubToken(GithubApp $source, string $type) if (! $response->successful()) { $error = data_get($response->json(), 'message', "HTTP {$response->status()}"); if ($error === 'Not Found') { - $error = 'Repository not found. Is it moved or deleted?'; + $error = "GitHub App installation {$source->installation_id} was not found. Install the GitHub App again or update its installation ID."; } throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error); } @@ -388,10 +388,9 @@ function loadRepositoryByPage(GithubApp $source, string $token, int $page) ]); $json = $response->json(); if ($response->status() !== 200) { - return [ - 'total_count' => 0, - 'repositories' => [], - ]; + $error = data_get($json, 'message', "HTTP {$response->status()}"); + + throw new RuntimeException("Failed to load repositories for {$source->name} with error: ".$error); } if ($json['total_count'] === 0) { diff --git a/tests/Feature/GithubPrivateRepositoryTest.php b/tests/Feature/GithubPrivateRepositoryTest.php index 6de13f0c79..de455553a1 100644 --- a/tests/Feature/GithubPrivateRepositoryTest.php +++ b/tests/Feature/GithubPrivateRepositoryTest.php @@ -344,6 +344,52 @@ describe('GitHub Private Repository Component', function () { ->assertSee('Refresh'); }); + test('loadRepositories shows an error when the installation is not found', function () { + Http::fake([ + 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ + 'Date' => now()->toRfc7231String(), + ]), + 'https://api.github.com/app/installations/67890/access_tokens' => Http::response(['message' => 'Not Found'], 404), + ]); + + Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->call('loadRepositories', $this->githubApp->id) + ->assertDispatched('error', fn (string $event, array $params) => str_contains($params[0], 'installation 67890 was not found')) + ->assertSet('current_step', 'github_apps'); + }); + + test('loadRepositories shows the GitHub error when listing repositories fails', function () { + Http::fake([ + 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ + 'Date' => now()->toRfc7231String(), + ]), + 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ + 'token' => 'fake-installation-token', + ], 201), + 'https://api.github.com/installation/repositories*' => Http::response(['message' => 'Resource not accessible by integration'], 403), + ]); + + Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->call('loadRepositories', $this->githubApp->id) + ->assertDispatched('error', fn (string $event, array $params) => str_contains($params[0], 'Resource not accessible by integration')) + ->assertSet('current_step', 'github_apps'); + }); + + test('loadBranches stops and shows the GitHub error when a later branch page fails', function () { + $repository = ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']]; + fakeGithubHttp([$repository]); + Http::fake([ + 'https://api.github.com/repos/testuser/alpha-repo/branches*' => Http::sequence() + ->push(collect(range(1, 100))->map(fn (int $i) => ['name' => "branch-{$i}"])->all(), 200) + ->whenEmpty(Http::response(['message' => 'API rate limit exceeded'], 403)), + ]); + + Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->call('loadRepositories', $this->githubApp->id) + ->call('loadBranches') + ->assertDispatched('error', fn (string $event, array $params) => str_contains($params[0], 'API rate limit exceeded')); + }); + test('refresh button is not visible before repositories are loaded', function () { Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) ->assertDontSee('Refresh');