Merge remote-tracking branch 'origin/main'

This commit is contained in:
Andras Bacsai
2026-10-15 23:00:46 +02:00
6 changed files with 383 additions and 4 deletions
+5 -1
View File
@@ -84,7 +84,11 @@ class Bitbucket extends Controller
$failure_key = $this->manualWebhookFailureRateLimitKey($request, 'bitbucket', $full_name, $branch);
// A redelivery of the same signed payload is one guess.
$failure_attempt = $this->manualWebhookSignedPayloadAttempt($request, $x_bitbucket_token);
$applications = $this->manualWebhookApplications(Application::query()->where('git_branch', $branch), $full_name);
$applications = Application::query();
if ($x_bitbucket_event !== 'pullrequest:rejected' && $x_bitbucket_event !== 'pullrequest:fulfilled') {
$applications->where('git_branch', $branch);
}
$applications = $this->manualWebhookApplications($applications, $full_name);
if ($applications->isEmpty()) {
return $this->unauthenticatedManualWebhookResponse($failure_key, $failure_attempt);
}
+4 -1
View File
@@ -84,7 +84,10 @@ class Gitea extends Controller
}
}
if ($x_gitea_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 $this->unauthenticatedManualWebhookResponse($failure_key, $failure_attempt);
}
+8 -2
View File
@@ -231,7 +231,10 @@ class Gitlab extends Controller
'skip_deploy' => $skip_deploy_pr,
] = $this->readMergeRequestPayload($payload);
$applications = $applications->where('git_branch', $base_branch)->get();
if (! in_array($action, ['closed', 'close', 'merge'])) {
$applications->where('git_branch', $base_branch);
}
$applications = $applications->get();
foreach ($applications as $application) {
if (! $application->destination->server->isFunctional()) {
@@ -440,7 +443,10 @@ class Gitlab extends Controller
}
}
if ($x_gitlab_event === 'merge_request') {
$applications = $this->manualWebhookApplications($applications->where('git_branch', $base_branch), $full_name);
if (! in_array($action, ['closed', 'close', 'merge'])) {
$applications->where('git_branch', $base_branch);
}
$applications = $this->manualWebhookApplications($applications, $full_name);
if ($applications->isEmpty()) {
return $this->unauthenticatedManualWebhookResponse($failure_key, $failure_attempt);
}
@@ -0,0 +1,105 @@
<?php
use App\Actions\Application\CleanupPreviewDeployment;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
function createBitbucketPullRequestWebhookApplication(): Application
{
$team = Team::factory()->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([
'name' => 'bitbucket-pr-webhook-app',
'git_repository' => 'https://bitbucket.org/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(),
]);
}
function sendBitbucketPullRequestWebhook(Application $application, string $event, string $destinationBranch)
{
$body = json_encode([
'repository' => [
'full_name' => 'test-org/test-repo',
'uuid' => '{repository-uuid}',
],
'pullrequest' => [
'id' => 42,
'title' => 'Stacked change',
'links' => ['html' => ['href' => 'https://bitbucket.org/test-org/test-repo/pull-requests/42']],
'source' => [
'branch' => ['name' => 'feature/child'],
'commit' => ['hash' => 'abc1234def5678'],
'repository' => ['uuid' => '{repository-uuid}'],
],
'destination' => [
'branch' => ['name' => $destinationBranch],
'repository' => ['uuid' => '{repository-uuid}'],
],
],
], JSON_THROW_ON_ERROR);
return test()->call('POST', '/webhooks/source/bitbucket/events/manual', [], [], [], [
'HTTP_X-Event-Key' => $event,
'HTTP_X-Hub-Signature' => 'sha256='.hash_hmac('sha256', $body, $application->manual_webhook_secret_bitbucket),
'CONTENT_TYPE' => 'application/json',
], $body);
}
it('cleans up the preview of a closed pull request after the destination branch changes', function (string $event) {
$application = createBitbucketPullRequestWebhookApplication();
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 42,
'pull_request_html_url' => 'https://bitbucket.org/test-org/test-repo/pull-requests/42',
'git_type' => 'bitbucket',
]);
CleanupPreviewDeployment::shouldRun()
->once()
->withArgs(fn (Application $cleanedApplication, int $pullRequestId, ApplicationPreview $cleanedPreview): bool => $cleanedApplication->is($application)
&& $pullRequestId === 42
&& $cleanedPreview->is($preview))
->andReturn([
'cancelled_deployments' => 0,
'killed_containers' => 0,
'status' => 'success',
]);
$response = sendBitbucketPullRequestWebhook($application, $event, 'feature/parent');
$response->assertOk();
expect($response->getContent())->toContain('Preview deployment closed.');
})->with(['pullrequest:fulfilled', 'pullrequest:rejected']);
it('continues to filter opened pull requests by destination branch', function () {
Queue::fake();
$application = createBitbucketPullRequestWebhookApplication();
$application->settings->update(['is_preview_deployments_enabled' => true]);
$response = sendBitbucketPullRequestWebhook($application, 'pullrequest:created', 'feature/parent');
$response->assertOk();
expect(ApplicationPreview::where('application_id', $application->id)->exists())->toBeFalse();
});
@@ -0,0 +1,106 @@
<?php
use App\Actions\Application\CleanupPreviewDeployment;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
function createGiteaPullRequestWebhookApplication(): Application
{
$team = Team::factory()->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([
'name' => 'gitea-pr-webhook-app',
'git_repository' => 'https://gitea.example.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(),
]);
}
function sendGiteaPullRequestWebhook(Application $application, string $action, string $baseBranch)
{
$body = json_encode([
'action' => $action,
'number' => 42,
'repository' => [
'id' => 987654321,
'full_name' => 'test-org/test-repo',
],
'pull_request' => [
'html_url' => 'https://gitea.example.com/test-org/test-repo/pulls/42',
'title' => 'Stacked change',
'head' => [
'ref' => 'feature/child',
'sha' => 'abc1234def5678',
'repo' => ['id' => 987654321],
],
'base' => [
'ref' => $baseBranch,
'repo' => ['id' => 987654321],
],
],
], JSON_THROW_ON_ERROR);
return test()->call('POST', '/webhooks/source/gitea/events/manual', [], [], [], [
'HTTP_X-Gitea-Event' => 'pull_request',
'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $body, $application->manual_webhook_secret_gitea),
'CONTENT_TYPE' => 'application/json',
], $body);
}
it('cleans up the preview of a closed pull request after the base branch changes', function () {
$application = createGiteaPullRequestWebhookApplication();
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 42,
'pull_request_html_url' => 'https://gitea.example.com/test-org/test-repo/pulls/42',
'git_type' => 'gitea',
]);
CleanupPreviewDeployment::shouldRun()
->once()
->withArgs(fn (Application $cleanedApplication, int $pullRequestId, ApplicationPreview $cleanedPreview): bool => $cleanedApplication->is($application)
&& $pullRequestId === 42
&& $cleanedPreview->is($preview))
->andReturn([
'cancelled_deployments' => 0,
'killed_containers' => 0,
'status' => 'success',
]);
$response = sendGiteaPullRequestWebhook($application, 'closed', 'feature/parent');
$response->assertOk();
expect($response->getContent())->toContain('Preview deployment closed.');
});
it('continues to filter opened pull requests by base branch', function () {
Queue::fake();
$application = createGiteaPullRequestWebhookApplication();
$application->settings->update(['is_preview_deployments_enabled' => true]);
$response = sendGiteaPullRequestWebhook($application, 'opened', 'feature/parent');
$response->assertOk();
expect(ApplicationPreview::where('application_id', $application->id)->exists())->toBeFalse();
});
@@ -0,0 +1,155 @@
<?php
use App\Actions\Application\CleanupPreviewDeployment;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Environment;
use App\Models\GitlabApp;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
function createGitlabMergeRequestWebhookApplication(array $overrides = []): Application
{
$team = Team::factory()->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' => 'gitlab-mr-webhook-app',
'git_repository' => 'https://gitlab.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 createGitlabMergeRequestWebhookSource(): GitlabApp
{
return GitlabApp::create([
'name' => 'gitlab-app-webhook-test',
'api_url' => 'https://gitlab.com/api/v4',
'html_url' => 'https://gitlab.com',
'webhook_token' => 'test-webhook-token',
'team_id' => Team::factory()->create()->id,
]);
}
function gitlabMergeRequestPayload(string $action, string $targetBranch): array
{
return [
'object_kind' => 'merge_request',
'project' => [
'id' => 987654321,
'path_with_namespace' => 'test-org/test-repo',
],
'object_attributes' => [
'action' => $action,
'iid' => 42,
'url' => 'https://gitlab.com/test-org/test-repo/-/merge_requests/42',
'title' => 'Stacked change',
'source_branch' => 'feature/child',
'target_branch' => $targetBranch,
'source_project_id' => 987654321,
'target_project_id' => 987654321,
'last_commit' => [
'id' => 'abc1234def5678',
'message' => 'Stacked change',
],
],
];
}
function expectGitlabPreviewCleanup(Application $application): void
{
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 42,
'pull_request_html_url' => 'https://gitlab.com/test-org/test-repo/-/merge_requests/42',
'git_type' => 'gitlab',
]);
CleanupPreviewDeployment::shouldRun()
->once()
->withArgs(fn (Application $cleanedApplication, int $pullRequestId, ApplicationPreview $cleanedPreview): bool => $cleanedApplication->is($application)
&& $pullRequestId === 42
&& $cleanedPreview->is($preview))
->andReturn([
'cancelled_deployments' => 0,
'killed_containers' => 0,
'status' => 'success',
]);
}
it('cleans up the preview of a closed GitLab App merge request after the target branch changes', function (string $action) {
$gitlabApp = createGitlabMergeRequestWebhookSource();
$application = createGitlabMergeRequestWebhookApplication([
'repository_project_id' => 987654321,
'source_id' => $gitlabApp->id,
'source_type' => GitlabApp::class,
]);
expectGitlabPreviewCleanup($application);
$response = $this->postJson('/webhooks/source/gitlab/events', gitlabMergeRequestPayload($action, 'feature/parent'), [
'X-Gitlab-Token' => 'test-webhook-token',
]);
$response->assertOk();
expect($response->getContent())->toContain('Preview deployment closed.');
})->with(['close', 'merge']);
it('cleans up the preview of a closed manual merge request after the target branch changes', function (string $action) {
$application = createGitlabMergeRequestWebhookApplication();
expectGitlabPreviewCleanup($application);
$response = $this->postJson('/webhooks/source/gitlab/events/manual', gitlabMergeRequestPayload($action, 'feature/parent'), [
'X-Gitlab-Event' => 'Merge Request Hook',
'X-Gitlab-Token' => $application->manual_webhook_secret_gitlab,
]);
$response->assertOk();
expect($response->getContent())->toContain('Preview deployment closed.');
})->with(['close', 'merge']);
it('continues to filter opened merge requests by target branch', function (string $endpoint) {
Queue::fake();
if ($endpoint === 'app') {
$gitlabApp = createGitlabMergeRequestWebhookSource();
$application = createGitlabMergeRequestWebhookApplication([
'repository_project_id' => 987654321,
'source_id' => $gitlabApp->id,
'source_type' => GitlabApp::class,
]);
$application->settings->update(['is_preview_deployments_enabled' => true]);
$response = $this->postJson('/webhooks/source/gitlab/events', gitlabMergeRequestPayload('open', 'feature/parent'), [
'X-Gitlab-Token' => 'test-webhook-token',
]);
} else {
$application = createGitlabMergeRequestWebhookApplication();
$application->settings->update(['is_preview_deployments_enabled' => true]);
$response = $this->postJson('/webhooks/source/gitlab/events/manual', gitlabMergeRequestPayload('open', 'feature/parent'), [
'X-Gitlab-Event' => 'Merge Request Hook',
'X-Gitlab-Token' => $application->manual_webhook_secret_gitlab,
]);
}
$response->assertOk();
expect(ApplicationPreview::where('application_id', $application->id)->exists())->toBeFalse();
})->with(['app', 'manual']);