refactor(github-webhook): restructure application processing by grouping applications by server for improved deployment handling

This commit is contained in:
Andras Bacsai
2025-09-10 09:30:43 +02:00
parent 40f2471c5a
commit 52312e9de6
2 changed files with 266 additions and 254 deletions

View File

@@ -97,162 +97,168 @@ class Github extends Controller
return response("Nothing to do. No applications found with branch '$base_branch'."); return response("Nothing to do. No applications found with branch '$base_branch'.");
} }
} }
foreach ($applications as $application) { $applicationsByServer = $applications->groupBy(function ($app) {
$webhook_secret = data_get($application, 'manual_webhook_secret_github'); return $app->destination->server_id;
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); });
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Invalid signature.',
]);
continue; foreach ($applicationsByServer as $serverId => $serverApplications) {
} foreach ($serverApplications as $application) {
$isFunctional = $application->destination->server->isFunctional(); $webhook_secret = data_get($application, 'manual_webhook_secret_github');
if (! $isFunctional) { $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
$return_payloads->push([ if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional.',
]);
continue;
}
if ($x_github_event === 'push') {
if ($application->isDeployable()) {
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true,
);
if ($result['status'] === 'skipped') {
$return_payloads->push([
'application' => $application->name,
'status' => 'skipped',
'message' => $result['message'],
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Deployment queued.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
'deployment_uuid' => $result['deployment_uuid'],
]);
}
} 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([ $return_payloads->push([
'application' => $application->name,
'status' => 'failed', 'status' => 'failed',
'message' => 'Deployments disabled.', 'message' => 'Invalid signature.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]); ]);
continue;
} }
} $isFunctional = $application->destination->server->isFunctional();
if ($x_github_event === 'pull_request') { if (! $isFunctional) {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { $return_payloads->push([
if ($application->isPRDeployable()) { 'application' => $application->name,
// Check if PR deployments from public contributors are restricted 'status' => 'failed',
if (! $application->settings->is_pr_deployments_public_enabled) { 'message' => 'Server is not functional.',
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; ]);
if (! in_array($author_association, $trustedAssociations)) {
continue;
}
if ($x_github_event === 'push') {
if ($application->isDeployable()) {
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true,
);
if ($result['status'] === 'skipped') {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
'status' => 'failed', 'status' => 'skipped',
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, 'message' => $result['message'],
]); ]);
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' => 'github',
'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 { } else {
$pr_app = ApplicationPreview::create([ $return_payloads->push([
'git_type' => 'github', 'application' => $application->name,
'application_id' => $application->id, 'status' => 'success',
'pull_request_id' => $pull_request_id, 'message' => 'Deployment queued.',
'pull_request_html_url' => $pull_request_html_url, 'application_uuid' => $application->uuid,
'application_name' => $application->name,
'deployment_uuid' => $result['deployment_uuid'],
]); ]);
$pr_app->generate_preview_fqdn();
} }
} 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_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
// Check if PR deployments from public contributors are restricted
if (! $application->settings->is_pr_deployments_public_enabled) {
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
if (! in_array($author_association, $trustedAssociations)) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
]);
$result = queue_application_deployment( continue;
application: $application, }
pull_request_id: $pull_request_id, }
deployment_uuid: $deployment_uuid, $deployment_uuid = new Cuid2;
force_rebuild: false, $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
commit: data_get($payload, 'head.sha', 'HEAD'), if (! $found) {
is_webhook: true, if ($application->build_pack === 'dockercompose') {
git_type: 'github' $pr_app = ApplicationPreview::create([
); 'git_type' => 'github',
if ($result['status'] === 'skipped') { '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' => 'github',
'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,
force_rebuild: false,
commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true,
git_type: 'github'
);
if ($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([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
'status' => 'skipped', 'status' => 'failed',
'message' => $result['message'], 'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
DeleteResourceJob::dispatch($found);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]); ]);
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
'status' => 'success', 'status' => 'failed',
'message' => 'Preview deployment queued.', 'message' => 'No preview deployment found.',
]); ]);
} }
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
DeleteResourceJob::dispatch($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.',
]);
} }
} }
} }
@@ -358,141 +364,147 @@ class Github extends Controller
return response("Nothing to do. No applications found with branch '$base_branch'."); return response("Nothing to do. No applications found with branch '$base_branch'.");
} }
} }
foreach ($applications as $application) { $applicationsByServer = $applications->groupBy(function ($app) {
$isFunctional = $application->destination->server->isFunctional(); return $app->destination->server_id;
if (! $isFunctional) { });
$return_payloads->push([
'status' => 'failed',
'message' => 'Server is not functional.',
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
continue; foreach ($applicationsByServer as $serverId => $serverApplications) {
} foreach ($serverApplications as $application) {
if ($x_github_event === 'push') { $isFunctional = $application->destination->server->isFunctional();
if ($application->isDeployable()) { if (! $isFunctional) {
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
$deployment_uuid = new Cuid2;
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false,
is_webhook: true,
);
$return_payloads->push([
'status' => $result['status'],
'message' => $result['message'],
'application_uuid' => $application->uuid,
'application_name' => $application->name,
'deployment_uuid' => $result['deployment_uuid'],
]);
} 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([ $return_payloads->push([
'status' => 'failed', 'status' => 'failed',
'message' => 'Deployments disabled.', 'message' => 'Server is not functional.',
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'application_name' => $application->name, 'application_name' => $application->name,
]); ]);
}
}
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
// Check if PR deployments from public contributors are restricted
if (! $application->settings->is_pr_deployments_public_enabled) {
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
if (! in_array($author_association, $trustedAssociations)) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
]);
continue; continue;
} }
} if ($x_github_event === 'push') {
$deployment_uuid = new Cuid2; if ($application->isDeployable()) {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if (! $found) { if ($is_watch_path_triggered || is_null($application->watch_paths)) {
ApplicationPreview::create([ $deployment_uuid = new Cuid2;
'git_type' => 'github', $result = queue_application_deployment(
'application_id' => $application->id, application: $application,
'pull_request_id' => $pull_request_id, deployment_uuid: $deployment_uuid,
'pull_request_html_url' => $pull_request_html_url, commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false,
is_webhook: true,
);
$return_payloads->push([
'status' => $result['status'],
'message' => $result['message'],
'application_uuid' => $application->uuid,
'application_name' => $application->name,
'deployment_uuid' => $result['deployment_uuid'],
]);
} 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,
],
]); ]);
} }
$result = queue_application_deployment( } else {
application: $application, $return_payloads->push([
pull_request_id: $pull_request_id, 'status' => 'failed',
deployment_uuid: $deployment_uuid, 'message' => 'Deployments disabled.',
force_rebuild: false, 'application_uuid' => $application->uuid,
commit: data_get($payload, 'head.sha', 'HEAD'), 'application_name' => $application->name,
is_webhook: true, ]);
git_type: 'github' }
); }
if ($result['status'] === 'skipped') { if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
// Check if PR deployments from public contributors are restricted
if (! $application->settings->is_pr_deployments_public_enabled) {
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
if (! in_array($author_association, $trustedAssociations)) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
]);
continue;
}
}
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
$result = queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true,
git_type: 'github'
);
if ($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([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
'status' => 'skipped', 'status' => 'failed',
'message' => $result['message'], 'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id);
if ($containers->isNotEmpty()) {
$containers->each(function ($container) use ($application) {
$container_name = data_get($container, 'Names');
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
});
}
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
DeleteResourceJob::dispatch($found);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]); ]);
} else { } else {
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
'status' => 'success', 'status' => 'failed',
'message' => 'Preview deployment queued.', 'message' => 'No preview deployment found.',
]); ]);
} }
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id);
if ($containers->isNotEmpty()) {
$containers->each(function ($container) use ($application) {
$container_name = data_get($container, 'Names');
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
});
}
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
DeleteResourceJob::dispatch($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.',
]);
} }
} }
} }

View File

@@ -147,7 +147,7 @@ function next_after_cancel(?Server $server = null)
foreach ($next_found as $next) { foreach ($next_found as $next) {
$server = Server::find($next->server_id); $server = Server::find($next->server_id);
$concurrent_builds = $server->settings->concurrent_builds; $concurrent_builds = $server->settings->concurrent_builds;
$inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS])->get()->sortByDesc('created_at');
if ($inprogress_deployments->count() < $concurrent_builds) { if ($inprogress_deployments->count() < $concurrent_builds) {
$next->update([ $next->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,