diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 28e92dcd4..c4fdc5fd5 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -83,7 +83,10 @@ public function manual(Request $request) } } if ($x_github_event === 'pull_request') { - $applications = $this->manualWebhookApplications($applications->where('git_branch', $base_branch), $full_name); + if ($action !== 'closed') { + $applications->where('git_branch', $base_branch); + } + $applications = $this->manualWebhookApplications($applications, $full_name); if ($applications->isEmpty()) { return response("Nothing to do. No applications found for repo $full_name and branch '$base_branch'."); } @@ -334,7 +337,10 @@ public function normal(Request $request) } } if ($x_github_event === 'pull_request') { - $applications = $applications->where('git_branch', $base_branch)->get(); + if ($action !== 'closed') { + $applications->where('git_branch', $base_branch); + } + $applications = $applications->get(); if ($applications->isEmpty()) { return response("Nothing to do. No applications found with branch '$base_branch'."); } diff --git a/tests/Feature/GithubPullRequestWebhookRoutingTest.php b/tests/Feature/GithubPullRequestWebhookRoutingTest.php new file mode 100644 index 000000000..7768dbd90 --- /dev/null +++ b/tests/Feature/GithubPullRequestWebhookRoutingTest.php @@ -0,0 +1,156 @@ +create(); + $project = Project::factory()->create(['team_id' => $team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $server = Server::factory()->create(['team_id' => $team->id]); + $server->settings->update([ + 'is_reachable' => true, + 'is_usable' => true, + 'force_disabled' => false, + ]); + $destination = $server->standaloneDockers()->firstOrFail(); + + return Application::create(array_merge([ + 'name' => 'github-pr-webhook-app', + 'git_repository' => 'https://github.com/test-org/test-repo', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ], $overrides)); +} + +function githubPullRequestPayload(string $action, string $baseBranch): array +{ + return [ + 'action' => $action, + 'number' => 42, + 'repository' => [ + 'id' => 987654321, + 'full_name' => 'test-org/test-repo', + ], + 'pull_request' => [ + 'html_url' => 'https://github.com/test-org/test-repo/pull/42', + 'title' => 'Stacked change', + 'author_association' => 'OWNER', + 'head' => [ + 'ref' => 'feature/child', + 'sha' => 'head-sha', + ], + 'base' => [ + 'ref' => $baseBranch, + ], + ], + ]; +} + +it('routes a closed GitHub App pull request to its application after the base branch changes', function () { + Queue::fake(); + + $team = Team::factory()->create(); + $githubApp = GithubApp::create([ + 'name' => 'github-app-webhook-test', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 1234567890, + 'webhook_secret' => 'test-secret', + 'is_public' => false, + 'team_id' => $team->id, + ]); + $application = createGithubPullRequestWebhookApplication([ + 'repository_project_id' => 987654321, + 'source_id' => $githubApp->id, + 'source_type' => GithubApp::class, + ]); + + $body = json_encode(githubPullRequestPayload('closed', 'feature/parent'), JSON_THROW_ON_ERROR); + $response = $this->call('POST', '/webhooks/source/github/events', [], [], [], [ + 'HTTP_X-GitHub-Event' => 'pull_request', + 'HTTP_X-GitHub-Hook-Installation-Target-Id' => '1234567890', + 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $body, 'test-secret'), + 'CONTENT_TYPE' => 'application/json', + ], $body); + + $response->assertOk(); + expect($response->getContent())->toContain('PR webhook received'); + Queue::assertPushed(ProcessGithubPullRequestWebhook::class, fn (ProcessGithubPullRequestWebhook $job): bool => $job->applicationId === $application->id + && $job->action === 'closed' + && $job->pullRequestId === 42); +}); + +it('routes a closed manual pull request to its application after the base branch changes', function () { + Queue::fake(); + + $application = createGithubPullRequestWebhookApplication(); + $payload = githubPullRequestPayload('closed', 'feature/parent'); + $body = json_encode($payload, JSON_THROW_ON_ERROR); + + $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ + 'HTTP_X-GitHub-Event' => 'pull_request', + 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $body, $application->manual_webhook_secret_github), + 'CONTENT_TYPE' => 'application/json', + ], $body); + + $response->assertOk(); + Queue::assertPushed(ProcessGithubPullRequestWebhook::class, fn (ProcessGithubPullRequestWebhook $job): bool => $job->applicationId === $application->id + && $job->action === 'closed' + && $job->pullRequestId === 42); +}); + +it('continues to filter non-closed pull requests by base branch', function (string $endpoint) { + Queue::fake(); + + if ($endpoint === 'app') { + $team = Team::factory()->create(); + $githubApp = GithubApp::create([ + 'name' => 'github-app-webhook-test', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 1234567890, + 'webhook_secret' => 'test-secret', + 'is_public' => false, + 'team_id' => $team->id, + ]); + createGithubPullRequestWebhookApplication([ + 'repository_project_id' => 987654321, + 'source_id' => $githubApp->id, + 'source_type' => GithubApp::class, + ]); + + $body = json_encode(githubPullRequestPayload('opened', 'feature/parent'), JSON_THROW_ON_ERROR); + $response = $this->call('POST', '/webhooks/source/github/events', [], [], [], [ + 'HTTP_X-GitHub-Event' => 'pull_request', + 'HTTP_X-GitHub-Hook-Installation-Target-Id' => '1234567890', + 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $body, 'test-secret'), + 'CONTENT_TYPE' => 'application/json', + ], $body); + } else { + $application = createGithubPullRequestWebhookApplication(); + $body = json_encode(githubPullRequestPayload('opened', 'feature/parent'), JSON_THROW_ON_ERROR); + $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ + 'HTTP_X-GitHub-Event' => 'pull_request', + 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $body, $application->manual_webhook_secret_github), + 'CONTENT_TYPE' => 'application/json', + ], $body); + } + + $response->assertOk(); + Queue::assertNotPushed(ProcessGithubPullRequestWebhook::class); +})->with(['app', 'manual']);