mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 17:42:34 +00:00
Revert the app code changes ofd666a0a95,69eeb2f62,1e66c9f9banda32082b03, so login, registration, OAuth, team danger zone, service templates, and the other touched files are back to their earlier state. Keep the removal of unused components and markup-only tests (d666a0a95also contained those deletions by mistake). StartupExecutionCleanupTest now fakes the template CDN with the current file content, so app:init in the test no longer empties templates/service-templates-latest.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
688 lines
33 KiB
PHP
688 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Webhook;
|
|
|
|
use App\Actions\Application\CleanupPreviewDeployment;
|
|
use App\Exceptions\InvalidWebhookPayloadException;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
|
use App\Http\Controllers\Webhook\Concerns\MatchesManualWebhookApplications;
|
|
use App\Http\Controllers\Webhook\Concerns\ReadsWebhookPushPayload;
|
|
use App\Http\Controllers\Webhook\Concerns\ValidatesPreviewDeploymentRepository;
|
|
use App\Livewire\Source\Gitlab\Change as GitlabSource;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationPreview;
|
|
use App\Models\GitlabApp;
|
|
use Exception;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Visus\Cuid2\Cuid2;
|
|
|
|
class Gitlab extends Controller
|
|
{
|
|
use DetectsSkipDeployCommits;
|
|
use MatchesManualWebhookApplications;
|
|
use ReadsWebhookPushPayload;
|
|
use ValidatesPreviewDeploymentRepository;
|
|
|
|
public function redirect(Request $request)
|
|
{
|
|
try {
|
|
$code = $request->query('code');
|
|
$state = $request->query('state');
|
|
|
|
if (! $code || ! $state) {
|
|
return redirect()->route('source.all')->with('error', 'Invalid GitLab OAuth callback. Missing code or state.');
|
|
}
|
|
|
|
// Validate the one-time, team-bound state (not a guessable source UUID) to stop forged callbacks from overwriting a source's tokens.
|
|
$payload = Cache::pull(GitlabSource::oauthStateCacheKey($state));
|
|
$team_id = $request->user()?->currentTeam()?->id;
|
|
if (! is_array($payload) || is_null($team_id) || (int) data_get($payload, 'team_id') !== (int) $team_id) {
|
|
return redirect()->route('source.all')->with('error', 'Invalid or expired GitLab OAuth state. Please start the authorization again.');
|
|
}
|
|
|
|
$gitlabApp = GitlabApp::whereKey(data_get($payload, 'gitlab_app_id'))->firstOrFail();
|
|
|
|
// Only users who may administer the source can complete OAuth and store tokens.
|
|
if (! $request->user()->can('update', $gitlabApp)) {
|
|
return redirect()->route('source.all')->with('error', 'You are not authorized to connect this GitLab App.');
|
|
}
|
|
|
|
$baseUrl = rtrim($gitlabApp->html_url, '/');
|
|
|
|
$response = Http::GitSource($baseUrl)->asForm()->post("{$baseUrl}/oauth/token", [
|
|
'client_id' => $gitlabApp->client_id,
|
|
'client_secret' => $gitlabApp->client_secret,
|
|
'code' => $code,
|
|
'grant_type' => 'authorization_code',
|
|
'redirect_uri' => $gitlabApp->redirect_uri,
|
|
]);
|
|
|
|
if (! $response->successful()) {
|
|
$error = data_get($response->json(), 'error_description', 'Token exchange failed');
|
|
|
|
return redirect()->route('source.gitlab.show', ['gitlab_app_uuid' => $gitlabApp->uuid])
|
|
->with('error', "GitLab OAuth failed: {$error}");
|
|
}
|
|
|
|
$data = $response->json();
|
|
$gitlabApp->update([
|
|
'access_token' => $data['access_token'],
|
|
'refresh_token' => $data['refresh_token'],
|
|
'expires_at' => time() + ($data['expires_in'] ?? 7200),
|
|
]);
|
|
|
|
return redirect()->route('source.gitlab.show', ['gitlab_app_uuid' => $gitlabApp->uuid]);
|
|
} catch (Exception $e) {
|
|
return redirect()->route('source.all')->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function normal(Request $request)
|
|
{
|
|
try {
|
|
$return_payloads = collect([]);
|
|
$payload = $request->collect();
|
|
$x_gitlab_token = $request->header('X-Gitlab-Token');
|
|
$object_kind = $this->webhookString(data_get($payload, 'object_kind'));
|
|
|
|
$allowed_events = ['push', 'merge_request'];
|
|
if (! in_array($object_kind, $allowed_events, true)) {
|
|
return response([
|
|
'status' => 'failed',
|
|
'message' => 'Event not allowed. Only push and merge_request events are allowed.',
|
|
]);
|
|
}
|
|
|
|
if (empty($x_gitlab_token)) {
|
|
auditLogWebhookFailure('gitlab', 'webhook_token_missing', [
|
|
'event' => $object_kind,
|
|
]);
|
|
|
|
return response([
|
|
'status' => 'failed',
|
|
'message' => 'Missing X-Gitlab-Token header.',
|
|
], 401);
|
|
}
|
|
|
|
$gitlab_app = GitlabApp::findByWebhookToken($x_gitlab_token);
|
|
if (! $gitlab_app) {
|
|
auditLogWebhookFailure('gitlab', 'invalid_token', [
|
|
'event' => $object_kind,
|
|
]);
|
|
|
|
return response([
|
|
'status' => 'failed',
|
|
'message' => 'Invalid webhook token.',
|
|
], 401);
|
|
}
|
|
|
|
$project_id = $this->webhookPayloadDatabaseId($payload, 'project.id', required: true);
|
|
$applications = Application::where('source_id', $gitlab_app->id)
|
|
->where('source_type', GitlabApp::class)
|
|
->where('repository_project_id', $project_id);
|
|
|
|
if ($object_kind === 'push') {
|
|
$branch = $this->webhookPushBranch(data_get($payload, 'ref'));
|
|
if (! $branch) {
|
|
return response([
|
|
'status' => 'failed',
|
|
'message' => 'No branch found in the request.',
|
|
]);
|
|
}
|
|
if ($this->isWebhookBranchDeletionPush($payload)) {
|
|
return response([
|
|
'status' => 'skipped',
|
|
'message' => 'Nothing to do. Branch deleted.',
|
|
]);
|
|
}
|
|
$commit = $this->webhookCommitSha($payload, 'after');
|
|
|
|
$applications = $applications->where('git_branch', $branch)->get();
|
|
$changed_files = $this->webhookPushChangedFiles($payload);
|
|
$skip_deploy_commits = self::shouldSkipDeploy($this->webhookPushCommitMessages($payload));
|
|
|
|
foreach ($applications as $application) {
|
|
if (! $application->destination->server->isFunctional()) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'Server is not functional',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! $application->isDeployable()) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'Deployments disabled',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! $this->webhookPushMatchesWatchPaths($application, $changed_files)) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'Changed files do not match watch paths.',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($skip_deploy_commits) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'skipped',
|
|
'message' => 'All commits contain [skip cd] or [skip ci].',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
$deployment_uuid = new Cuid2;
|
|
$result = queue_application_deployment(
|
|
application: $application,
|
|
deployment_uuid: $deployment_uuid,
|
|
commit: $commit ?? 'HEAD',
|
|
force_rebuild: false,
|
|
is_webhook: true,
|
|
);
|
|
|
|
if ($result['status'] === 'queue_full') {
|
|
return response($result['message'], 429)->header('Retry-After', 60);
|
|
}
|
|
|
|
auditLog('webhook.deployment.queued', [
|
|
'provider' => 'gitlab',
|
|
'mode' => 'app',
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
'deployment_uuid' => $deployment_uuid->toString(),
|
|
'commit' => $commit,
|
|
]);
|
|
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => $result['status'] ?? 'success',
|
|
'message' => $result['message'] ?? 'Deployment queued.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
if ($object_kind === 'merge_request') {
|
|
$branch = $this->webhookString(data_get($payload, 'object_attributes.source_branch'));
|
|
$base_branch = $this->webhookString(data_get($payload, 'object_attributes.target_branch'));
|
|
if (! $branch || ! $base_branch) {
|
|
return response([
|
|
'status' => 'failed',
|
|
'message' => 'No branch found in the request.',
|
|
]);
|
|
}
|
|
[
|
|
'action' => $action,
|
|
'pull_request_id' => $pull_request_id,
|
|
'pull_request_html_url' => $pull_request_html_url,
|
|
'commit' => $commit,
|
|
'skip_deploy' => $skip_deploy_pr,
|
|
] = $this->readMergeRequestPayload($payload);
|
|
|
|
$applications = $applications->where('git_branch', $base_branch)->get();
|
|
|
|
foreach ($applications as $application) {
|
|
if (! $application->destination->server->isFunctional()) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'Server is not functional',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (in_array($action, ['open', 'opened', 'synchronize', 'reopened', 'reopen', 'update'], true)) {
|
|
if (! $application->isPRDeployable()) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'Preview deployments disabled',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! $this->isPreviewDeploymentRepositoryTrusted(
|
|
data_get($payload, 'object_attributes.source_project_id'),
|
|
data_get($payload, 'object_attributes.target_project_id'),
|
|
data_get($payload, 'project.id'),
|
|
$application->settings->is_pr_deployments_public_enabled,
|
|
)) {
|
|
continue;
|
|
}
|
|
|
|
if ($skip_deploy_pr) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'skipped',
|
|
'message' => 'PR title or latest commit contains [skip cd] or [skip ci].',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
$deployment_uuid = new Cuid2;
|
|
$found = ApplicationPreview::where('application_id', $application->id)
|
|
->where('pull_request_id', $pull_request_id)
|
|
->first();
|
|
|
|
if (! $found) {
|
|
if ($application->build_pack === 'dockercompose') {
|
|
$pr_app = ApplicationPreview::create([
|
|
'git_type' => 'gitlab',
|
|
'application_id' => $application->id,
|
|
'pull_request_id' => $pull_request_id,
|
|
'pull_request_html_url' => $pull_request_html_url ?? '',
|
|
'docker_compose_domains' => $application->docker_compose_domains,
|
|
]);
|
|
$pr_app->generate_preview_fqdn_compose();
|
|
} else {
|
|
$pr_app = ApplicationPreview::create([
|
|
'git_type' => 'gitlab',
|
|
'application_id' => $application->id,
|
|
'pull_request_id' => $pull_request_id,
|
|
'pull_request_html_url' => $pull_request_html_url ?? '',
|
|
]);
|
|
$pr_app->generate_preview_fqdn();
|
|
}
|
|
}
|
|
|
|
$result = queue_application_deployment(
|
|
application: $application,
|
|
pull_request_id: $pull_request_id,
|
|
deployment_uuid: $deployment_uuid,
|
|
commit: $commit ?? 'HEAD',
|
|
force_rebuild: false,
|
|
is_webhook: true,
|
|
git_type: 'gitlab',
|
|
);
|
|
|
|
if ($result['status'] === 'queue_full') {
|
|
return response($result['message'], 429)->header('Retry-After', 60);
|
|
}
|
|
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => $result['status'] ?? 'success',
|
|
'message' => $result['message'] ?? 'Preview Deployment queued',
|
|
]);
|
|
} elseif (in_array($action, ['closed', 'close', 'merge'], true)) {
|
|
$found = ApplicationPreview::where('application_id', $application->id)
|
|
->where('pull_request_id', $pull_request_id)
|
|
->first();
|
|
|
|
if ($found) {
|
|
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'success',
|
|
'message' => 'Preview deployment closed.',
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return response($return_payloads);
|
|
} catch (InvalidWebhookPayloadException $e) {
|
|
return response([
|
|
'status' => 'failed',
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
} catch (Exception $e) {
|
|
return handleError($e);
|
|
}
|
|
}
|
|
|
|
public function manual(Request $request)
|
|
{
|
|
try {
|
|
$return_payloads = collect([]);
|
|
$payload = $request->collect();
|
|
$headers = $request->headers->all();
|
|
$x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
|
|
$x_gitlab_event = $this->webhookString(data_get($payload, 'object_kind'));
|
|
$allowed_events = ['push', 'merge_request'];
|
|
if (! in_array($x_gitlab_event, $allowed_events, true)) {
|
|
$return_payloads->push([
|
|
'status' => 'failed',
|
|
'message' => 'Event not allowed. Only push and merge_request events are allowed.',
|
|
]);
|
|
|
|
return response($return_payloads);
|
|
}
|
|
|
|
// A delivery without a token does not try a secret, so it is not
|
|
// counted as a failed attempt.
|
|
if (empty($x_gitlab_token)) {
|
|
auditLogWebhookFailure('gitlab', 'webhook_token_missing', [
|
|
'event' => $x_gitlab_event,
|
|
]);
|
|
|
|
return response([$this->unauthenticatedManualWebhookFailurePayload()]);
|
|
}
|
|
|
|
if ($x_gitlab_event === 'push') {
|
|
$branch = $this->webhookPushBranch(data_get($payload, 'ref'));
|
|
$full_name = data_get($payload, 'project.path_with_namespace');
|
|
if (! $branch) {
|
|
$return_payloads->push([
|
|
'status' => 'failed',
|
|
'message' => 'Nothing to do. No branch found in the request.',
|
|
]);
|
|
|
|
return response($return_payloads);
|
|
}
|
|
// A deleted branch has no commit to deploy. No secret is checked here.
|
|
if ($this->isWebhookBranchDeletionPush($payload)) {
|
|
$return_payloads->push([
|
|
'status' => 'skipped',
|
|
'message' => 'Nothing to do. Branch deleted.',
|
|
]);
|
|
|
|
return response($return_payloads);
|
|
}
|
|
$commit = $this->webhookCommitSha($payload, 'after');
|
|
$changed_files = $this->webhookPushChangedFiles($payload);
|
|
$skip_deploy_commits = self::shouldSkipDeploy($this->webhookPushCommitMessages($payload));
|
|
}
|
|
if ($x_gitlab_event === 'merge_request') {
|
|
$branch = $this->webhookString(data_get($payload, 'object_attributes.source_branch'));
|
|
$base_branch = $this->webhookString(data_get($payload, 'object_attributes.target_branch'));
|
|
$full_name = data_get($payload, 'project.path_with_namespace');
|
|
if (! $branch || ! $base_branch) {
|
|
$return_payloads->push([
|
|
'status' => 'failed',
|
|
'message' => 'Nothing to do. No branch found in the request.',
|
|
]);
|
|
|
|
return response($return_payloads);
|
|
}
|
|
[
|
|
'action' => $action,
|
|
'pull_request_id' => $pull_request_id,
|
|
'pull_request_html_url' => $pull_request_html_url,
|
|
'commit' => $commit,
|
|
'skip_deploy' => $skip_deploy_pr,
|
|
] = $this->readMergeRequestPayload($payload);
|
|
}
|
|
$full_name = $this->manualWebhookRepositoryFullName($full_name);
|
|
if ($full_name === null) {
|
|
$return_payloads->push([
|
|
'status' => 'failed',
|
|
'message' => 'Nothing to do. Invalid repository.',
|
|
]);
|
|
|
|
return response($return_payloads);
|
|
}
|
|
$matched_branch = $x_gitlab_event === 'merge_request' ? $base_branch : $branch;
|
|
$failure_key = $this->manualWebhookFailureRateLimitKey($request, 'gitlab', $full_name, $matched_branch);
|
|
if ($this->hasTooManyManualWebhookFailures($failure_key)) {
|
|
return $this->tooManyManualWebhookFailuresResponse($failure_key);
|
|
}
|
|
// GitLab sends a static token. A repeated wrong token is one guess.
|
|
$failure_attempt = $this->manualWebhookTokenAttempt($x_gitlab_token);
|
|
$applications = Application::query();
|
|
if ($x_gitlab_event === 'push') {
|
|
$applications = $this->manualWebhookApplications($applications->where('git_branch', $branch), $full_name);
|
|
if ($applications->isEmpty()) {
|
|
return $this->unauthenticatedManualWebhookResponse($failure_key, $failure_attempt);
|
|
}
|
|
}
|
|
if ($x_gitlab_event === 'merge_request') {
|
|
$applications = $this->manualWebhookApplications($applications->where('git_branch', $base_branch), $full_name);
|
|
if ($applications->isEmpty()) {
|
|
return $this->unauthenticatedManualWebhookResponse($failure_key, $failure_attempt);
|
|
}
|
|
}
|
|
foreach ($applications as $application) {
|
|
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
|
|
if (empty($webhook_secret)) {
|
|
auditLogWebhookFailure('gitlab', 'webhook_secret_missing', [
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
'repository' => $full_name ?? null,
|
|
'event' => $x_gitlab_event,
|
|
]);
|
|
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
|
|
|
continue;
|
|
}
|
|
if (! hash_equals($webhook_secret, $x_gitlab_token ?? '')) {
|
|
auditLogWebhookFailure('gitlab', 'invalid_signature', [
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
'repository' => $full_name ?? null,
|
|
'event' => $x_gitlab_event,
|
|
]);
|
|
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
|
|
|
continue;
|
|
}
|
|
$isFunctional = $application->destination->server->isFunctional();
|
|
if (! $isFunctional) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'Server is not functional',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
if ($x_gitlab_event === 'push') {
|
|
if ($application->isDeployable()) {
|
|
if ($this->webhookPushMatchesWatchPaths($application, $changed_files)) {
|
|
if ($skip_deploy_commits ?? false) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'skipped',
|
|
'message' => 'All commits contain [skip cd] or [skip ci]. Skipping deployment.',
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
$deployment_uuid = new_public_id();
|
|
$result = queue_application_deployment(
|
|
application: $application,
|
|
deployment_uuid: $deployment_uuid,
|
|
commit: $commit ?? 'HEAD',
|
|
force_rebuild: false,
|
|
is_webhook: true,
|
|
);
|
|
if ($result['status'] === 'queue_full') {
|
|
return response($result['message'], 429)->header('Retry-After', 60);
|
|
} elseif ($result['status'] === 'skipped') {
|
|
$return_payloads->push([
|
|
'status' => $result['status'],
|
|
'message' => $result['message'],
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
]);
|
|
} else {
|
|
auditLog('webhook.deployment.queued', [
|
|
'provider' => 'gitlab',
|
|
'mode' => 'manual',
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
'deployment_uuid' => $deployment_uuid,
|
|
'commit' => $commit,
|
|
'repository' => $full_name ?? null,
|
|
]);
|
|
$return_payloads->push([
|
|
'status' => 'success',
|
|
'message' => 'Deployment queued.',
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
]);
|
|
}
|
|
} else {
|
|
$paths = str($application->watch_paths)->explode("\n");
|
|
$return_payloads->push([
|
|
'status' => 'failed',
|
|
'message' => 'Changed files do not match watch paths. Ignoring deployment.',
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
'details' => [
|
|
'changed_files' => $changed_files,
|
|
'watch_paths' => $paths,
|
|
],
|
|
]);
|
|
}
|
|
} else {
|
|
$return_payloads->push([
|
|
'status' => 'failed',
|
|
'message' => 'Deployments disabled',
|
|
'application_uuid' => $application->uuid,
|
|
'application_name' => $application->name,
|
|
]);
|
|
}
|
|
}
|
|
if ($x_gitlab_event === 'merge_request') {
|
|
if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
|
|
if ($application->isPRDeployable()) {
|
|
if (! $this->isPreviewDeploymentRepositoryTrusted(
|
|
data_get($payload, 'object_attributes.source_project_id'),
|
|
data_get($payload, 'object_attributes.target_project_id'),
|
|
data_get($payload, 'project.id'),
|
|
$application->settings->is_pr_deployments_public_enabled,
|
|
)) {
|
|
continue;
|
|
}
|
|
|
|
if ($skip_deploy_pr ?? false) {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'skipped',
|
|
'message' => 'PR title or latest commit contains [skip cd] or [skip ci]. Skipping preview deployment.',
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
$deployment_uuid = new_public_id();
|
|
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
|
if (! $found) {
|
|
if ($application->build_pack === 'dockercompose') {
|
|
$pr_app = ApplicationPreview::create([
|
|
'git_type' => 'gitlab',
|
|
'application_id' => $application->id,
|
|
'pull_request_id' => $pull_request_id,
|
|
'pull_request_html_url' => $pull_request_html_url ?? '',
|
|
'docker_compose_domains' => $application->docker_compose_domains,
|
|
]);
|
|
$pr_app->generate_preview_fqdn_compose();
|
|
} else {
|
|
$pr_app = ApplicationPreview::create([
|
|
'git_type' => 'gitlab',
|
|
'application_id' => $application->id,
|
|
'pull_request_id' => $pull_request_id,
|
|
'pull_request_html_url' => $pull_request_html_url ?? '',
|
|
]);
|
|
$pr_app->generate_preview_fqdn();
|
|
}
|
|
}
|
|
$result = queue_application_deployment(
|
|
application: $application,
|
|
pull_request_id: $pull_request_id,
|
|
deployment_uuid: $deployment_uuid,
|
|
commit: $commit ?? 'HEAD',
|
|
force_rebuild: false,
|
|
is_webhook: true,
|
|
git_type: 'gitlab'
|
|
);
|
|
if ($result['status'] === 'queue_full') {
|
|
return response($result['message'], 429)->header('Retry-After', 60);
|
|
} elseif ($result['status'] === 'skipped') {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'skipped',
|
|
'message' => $result['message'],
|
|
]);
|
|
} else {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'success',
|
|
'message' => 'Preview Deployment queued',
|
|
]);
|
|
}
|
|
} else {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'Preview deployments disabled',
|
|
]);
|
|
}
|
|
} elseif ($action === 'closed' || $action === 'close' || $action === 'merge') {
|
|
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
|
if ($found) {
|
|
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
|
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'success',
|
|
'message' => 'Preview deployment closed.',
|
|
]);
|
|
} else {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'No preview deployment found.',
|
|
]);
|
|
}
|
|
} else {
|
|
$return_payloads->push([
|
|
'application' => $application->name,
|
|
'status' => 'failed',
|
|
'message' => 'No action found. Contact us for debugging.',
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->manualWebhookResponse($return_payloads, $failure_key, $failure_attempt);
|
|
} catch (InvalidWebhookPayloadException $e) {
|
|
return response([[
|
|
'status' => 'failed',
|
|
'message' => $e->getMessage(),
|
|
]]);
|
|
} catch (Exception $e) {
|
|
return handleError($e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read and validate the merge_request payload fields that the handlers use.
|
|
*
|
|
* @return array{action: ?string, pull_request_id: int, pull_request_html_url: ?string, commit: ?string, skip_deploy: bool}
|
|
*
|
|
* @throws InvalidWebhookPayloadException When a field has a wrong type or format.
|
|
*/
|
|
private function readMergeRequestPayload(mixed $payload): array
|
|
{
|
|
$title = $this->webhookPayloadString($payload, 'object_attributes.title');
|
|
|
|
return [
|
|
'action' => $this->webhookPayloadString($payload, 'object_attributes.action'),
|
|
'pull_request_id' => $this->webhookPullRequestId($payload, 'object_attributes.iid'),
|
|
'pull_request_html_url' => $this->webhookPayloadUrl($payload, 'object_attributes.url'),
|
|
'commit' => $this->webhookCommitSha($payload, 'object_attributes.last_commit.id'),
|
|
'skip_deploy' => self::shouldSkipDeployAny([$title, data_get($payload, 'object_attributes.last_commit.message')]),
|
|
];
|
|
}
|
|
}
|