feat(webhook): skip deployment on [skip ci]/[skip cd] commit markers
Add DetectsSkipDeployCommits trait with two strategies: shouldSkipDeploy (all commits must contain the marker) for push events, and shouldSkipDeployAny (any single marker triggers skip) for PR/MR titles and latest-commit signals. Apply trait to Bitbucket, Gitea, GitHub, GitLab webhook controllers and ProcessGithubPullRequestWebhook job. PRs pass pullRequestTitle through to the job constructor for evaluation.
This commit is contained in:
parent
9af0351144
commit
46180dbbf9
8 changed files with 296 additions and 1 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Actions\Application\CleanupPreviewDeployment;
|
use App\Actions\Application\CleanupPreviewDeployment;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
@ -12,6 +13,8 @@
|
||||||
|
|
||||||
class Bitbucket extends Controller
|
class Bitbucket extends Controller
|
||||||
{
|
{
|
||||||
|
use DetectsSkipDeployCommits;
|
||||||
|
|
||||||
public function manual(Request $request)
|
public function manual(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -31,6 +34,16 @@ public function manual(Request $request)
|
||||||
$branch = data_get($payload, 'push.changes.0.new.name');
|
$branch = data_get($payload, 'push.changes.0.new.name');
|
||||||
$full_name = data_get($payload, 'repository.full_name');
|
$full_name = data_get($payload, 'repository.full_name');
|
||||||
$commit = data_get($payload, 'push.changes.0.new.target.hash');
|
$commit = data_get($payload, 'push.changes.0.new.target.hash');
|
||||||
|
// Bitbucket webhooks ship up to 5 commits per change. Larger pushes
|
||||||
|
// are evaluated only on the visible 5.
|
||||||
|
$skip_deploy_commits = self::shouldSkipDeploy(
|
||||||
|
collect(data_get($payload, 'push.changes', []))
|
||||||
|
->flatMap(fn ($change) => data_get($change, 'commits', []))
|
||||||
|
->pluck('message')
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all()
|
||||||
|
);
|
||||||
|
|
||||||
if (! $branch) {
|
if (! $branch) {
|
||||||
return response([
|
return response([
|
||||||
|
|
@ -45,6 +58,8 @@ public function manual(Request $request)
|
||||||
$full_name = data_get($payload, 'repository.full_name');
|
$full_name = data_get($payload, 'repository.full_name');
|
||||||
$pull_request_id = data_get($payload, 'pullrequest.id');
|
$pull_request_id = data_get($payload, 'pullrequest.id');
|
||||||
$pull_request_html_url = data_get($payload, 'pullrequest.links.html.href');
|
$pull_request_html_url = data_get($payload, 'pullrequest.links.html.href');
|
||||||
|
$pull_request_title = data_get($payload, 'pullrequest.title');
|
||||||
|
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title]);
|
||||||
$commit = data_get($payload, 'pullrequest.source.commit.hash');
|
$commit = data_get($payload, 'pullrequest.source.commit.hash');
|
||||||
}
|
}
|
||||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||||
|
|
@ -119,6 +134,17 @@ public function manual(Request $request)
|
||||||
}
|
}
|
||||||
if ($x_bitbucket_event === 'repo:push') {
|
if ($x_bitbucket_event === 'repo:push') {
|
||||||
if ($application->isDeployable()) {
|
if ($application->isDeployable()) {
|
||||||
|
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 Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$result = queue_application_deployment(
|
$result = queue_application_deployment(
|
||||||
application: $application,
|
application: $application,
|
||||||
|
|
@ -161,6 +187,15 @@ public function manual(Request $request)
|
||||||
}
|
}
|
||||||
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:updated') {
|
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:updated') {
|
||||||
if ($application->isPRDeployable()) {
|
if ($application->isPRDeployable()) {
|
||||||
|
if ($skip_deploy_pr ?? false) {
|
||||||
|
$return_payloads->push([
|
||||||
|
'application' => $application->name,
|
||||||
|
'status' => 'skipped',
|
||||||
|
'message' => 'PR title contains [skip cd] or [skip ci]. Skipping preview deployment.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$deployment_uuid = new Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||||
if (! $found) {
|
if (! $found) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Webhook\Concerns;
|
||||||
|
|
||||||
|
trait DetectsSkipDeployCommits
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns true if there is at least one non-empty message and every message
|
||||||
|
* contains [skip cd] or [skip ci] (case-insensitive).
|
||||||
|
*
|
||||||
|
* Accepts commit messages from a push payload. Null/empty entries are
|
||||||
|
* filtered before evaluation.
|
||||||
|
*
|
||||||
|
* @param array<int, string|null> $messages
|
||||||
|
*/
|
||||||
|
public static function shouldSkipDeploy(array $messages): bool
|
||||||
|
{
|
||||||
|
$messages = array_values(array_filter($messages, fn ($m) => filled($m)));
|
||||||
|
|
||||||
|
if (empty($messages)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
$lower = strtolower((string) $message);
|
||||||
|
if (! str_contains($lower, '[skip cd]') && ! str_contains($lower, '[skip ci]')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if at least one non-empty message contains [skip cd] or
|
||||||
|
* [skip ci]. Used for PR/MR title + latest-commit signals where any one
|
||||||
|
* marker should trigger the skip.
|
||||||
|
*
|
||||||
|
* @param array<int, string|null> $messages
|
||||||
|
*/
|
||||||
|
public static function shouldSkipDeployAny(array $messages): bool
|
||||||
|
{
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
if (! filled($message)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$lower = strtolower((string) $message);
|
||||||
|
if (str_contains($lower, '[skip cd]') || str_contains($lower, '[skip ci]')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Actions\Application\CleanupPreviewDeployment;
|
use App\Actions\Application\CleanupPreviewDeployment;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
@ -13,6 +14,8 @@
|
||||||
|
|
||||||
class Gitea extends Controller
|
class Gitea extends Controller
|
||||||
{
|
{
|
||||||
|
use DetectsSkipDeployCommits;
|
||||||
|
|
||||||
public function manual(Request $request)
|
public function manual(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -40,12 +43,15 @@ public function manual(Request $request)
|
||||||
$removed_files = data_get($payload, 'commits.*.removed');
|
$removed_files = data_get($payload, 'commits.*.removed');
|
||||||
$modified_files = data_get($payload, 'commits.*.modified');
|
$modified_files = data_get($payload, 'commits.*.modified');
|
||||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||||
|
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||||
}
|
}
|
||||||
if ($x_gitea_event === 'pull_request') {
|
if ($x_gitea_event === 'pull_request') {
|
||||||
$action = data_get($payload, 'action');
|
$action = data_get($payload, 'action');
|
||||||
$full_name = data_get($payload, 'repository.full_name');
|
$full_name = data_get($payload, 'repository.full_name');
|
||||||
$pull_request_id = data_get($payload, 'number');
|
$pull_request_id = data_get($payload, 'number');
|
||||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||||
|
$pull_request_title = data_get($payload, 'pull_request.title');
|
||||||
|
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title]);
|
||||||
$branch = data_get($payload, 'pull_request.head.ref');
|
$branch = data_get($payload, 'pull_request.head.ref');
|
||||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +118,17 @@ public function manual(Request $request)
|
||||||
if ($application->isDeployable()) {
|
if ($application->isDeployable()) {
|
||||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||||
|
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 Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$result = queue_application_deployment(
|
$result = queue_application_deployment(
|
||||||
application: $application,
|
application: $application,
|
||||||
|
|
@ -170,6 +187,15 @@ public function manual(Request $request)
|
||||||
if ($x_gitea_event === 'pull_request') {
|
if ($x_gitea_event === 'pull_request') {
|
||||||
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
|
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
|
||||||
if ($application->isPRDeployable()) {
|
if ($application->isPRDeployable()) {
|
||||||
|
if ($skip_deploy_pr ?? false) {
|
||||||
|
$return_payloads->push([
|
||||||
|
'application' => $application->name,
|
||||||
|
'status' => 'skipped',
|
||||||
|
'message' => 'PR title contains [skip cd] or [skip ci]. Skipping preview deployment.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$deployment_uuid = new Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||||
if (! $found) {
|
if (! $found) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Http\Controllers\Webhook;
|
namespace App\Http\Controllers\Webhook;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||||
use App\Jobs\GithubAppPermissionJob;
|
use App\Jobs\GithubAppPermissionJob;
|
||||||
use App\Jobs\ProcessGithubPullRequestWebhook;
|
use App\Jobs\ProcessGithubPullRequestWebhook;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
|
|
@ -16,6 +17,8 @@
|
||||||
|
|
||||||
class Github extends Controller
|
class Github extends Controller
|
||||||
{
|
{
|
||||||
|
use DetectsSkipDeployCommits;
|
||||||
|
|
||||||
public function manual(Request $request)
|
public function manual(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -43,12 +46,14 @@ public function manual(Request $request)
|
||||||
$removed_files = data_get($payload, 'commits.*.removed');
|
$removed_files = data_get($payload, 'commits.*.removed');
|
||||||
$modified_files = data_get($payload, 'commits.*.modified');
|
$modified_files = data_get($payload, 'commits.*.modified');
|
||||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||||
|
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||||
}
|
}
|
||||||
if ($x_github_event === 'pull_request') {
|
if ($x_github_event === 'pull_request') {
|
||||||
$action = data_get($payload, 'action');
|
$action = data_get($payload, 'action');
|
||||||
$full_name = data_get($payload, 'repository.full_name');
|
$full_name = data_get($payload, 'repository.full_name');
|
||||||
$pull_request_id = data_get($payload, 'number');
|
$pull_request_id = data_get($payload, 'number');
|
||||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||||
|
$pull_request_title = data_get($payload, 'pull_request.title');
|
||||||
$branch = data_get($payload, 'pull_request.head.ref');
|
$branch = data_get($payload, 'pull_request.head.ref');
|
||||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||||
$before_sha = data_get($payload, 'before');
|
$before_sha = data_get($payload, 'before');
|
||||||
|
|
@ -126,6 +131,17 @@ public function manual(Request $request)
|
||||||
if ($application->isDeployable()) {
|
if ($application->isDeployable()) {
|
||||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||||
|
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 Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$result = queue_application_deployment(
|
$result = queue_application_deployment(
|
||||||
application: $application,
|
application: $application,
|
||||||
|
|
@ -201,6 +217,7 @@ public function manual(Request $request)
|
||||||
action: $action,
|
action: $action,
|
||||||
pullRequestId: $pull_request_id,
|
pullRequestId: $pull_request_id,
|
||||||
pullRequestHtmlUrl: $pull_request_html_url,
|
pullRequestHtmlUrl: $pull_request_html_url,
|
||||||
|
pullRequestTitle: $pull_request_title ?? null,
|
||||||
beforeSha: $before_sha,
|
beforeSha: $before_sha,
|
||||||
afterSha: $after_sha,
|
afterSha: $after_sha,
|
||||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||||
|
|
@ -274,12 +291,14 @@ public function normal(Request $request)
|
||||||
$removed_files = data_get($payload, 'commits.*.removed');
|
$removed_files = data_get($payload, 'commits.*.removed');
|
||||||
$modified_files = data_get($payload, 'commits.*.modified');
|
$modified_files = data_get($payload, 'commits.*.modified');
|
||||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||||
|
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||||
}
|
}
|
||||||
if ($x_github_event === 'pull_request') {
|
if ($x_github_event === 'pull_request') {
|
||||||
$action = data_get($payload, 'action');
|
$action = data_get($payload, 'action');
|
||||||
$id = data_get($payload, 'repository.id');
|
$id = data_get($payload, 'repository.id');
|
||||||
$pull_request_id = data_get($payload, 'number');
|
$pull_request_id = data_get($payload, 'number');
|
||||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||||
|
$pull_request_title = data_get($payload, 'pull_request.title');
|
||||||
$branch = data_get($payload, 'pull_request.head.ref');
|
$branch = data_get($payload, 'pull_request.head.ref');
|
||||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||||
$before_sha = data_get($payload, 'before');
|
$before_sha = data_get($payload, 'before');
|
||||||
|
|
@ -328,6 +347,17 @@ public function normal(Request $request)
|
||||||
if ($application->isDeployable()) {
|
if ($application->isDeployable()) {
|
||||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||||
|
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 Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$result = queue_application_deployment(
|
$result = queue_application_deployment(
|
||||||
application: $application,
|
application: $application,
|
||||||
|
|
@ -399,6 +429,7 @@ public function normal(Request $request)
|
||||||
action: $action,
|
action: $action,
|
||||||
pullRequestId: $pull_request_id,
|
pullRequestId: $pull_request_id,
|
||||||
pullRequestHtmlUrl: $pull_request_html_url,
|
pullRequestHtmlUrl: $pull_request_html_url,
|
||||||
|
pullRequestTitle: $pull_request_title ?? null,
|
||||||
beforeSha: $before_sha,
|
beforeSha: $before_sha,
|
||||||
afterSha: $after_sha,
|
afterSha: $after_sha,
|
||||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Actions\Application\CleanupPreviewDeployment;
|
use App\Actions\Application\CleanupPreviewDeployment;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
@ -13,6 +14,8 @@
|
||||||
|
|
||||||
class Gitlab extends Controller
|
class Gitlab extends Controller
|
||||||
{
|
{
|
||||||
|
use DetectsSkipDeployCommits;
|
||||||
|
|
||||||
public function manual(Request $request)
|
public function manual(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -61,6 +64,7 @@ public function manual(Request $request)
|
||||||
$removed_files = data_get($payload, 'commits.*.removed');
|
$removed_files = data_get($payload, 'commits.*.removed');
|
||||||
$modified_files = data_get($payload, 'commits.*.modified');
|
$modified_files = data_get($payload, 'commits.*.modified');
|
||||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||||
|
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||||
}
|
}
|
||||||
if ($x_gitlab_event === 'merge_request') {
|
if ($x_gitlab_event === 'merge_request') {
|
||||||
$action = data_get($payload, 'object_attributes.action');
|
$action = data_get($payload, 'object_attributes.action');
|
||||||
|
|
@ -69,6 +73,9 @@ public function manual(Request $request)
|
||||||
$full_name = data_get($payload, 'project.path_with_namespace');
|
$full_name = data_get($payload, 'project.path_with_namespace');
|
||||||
$pull_request_id = data_get($payload, 'object_attributes.iid');
|
$pull_request_id = data_get($payload, 'object_attributes.iid');
|
||||||
$pull_request_html_url = data_get($payload, 'object_attributes.url');
|
$pull_request_html_url = data_get($payload, 'object_attributes.url');
|
||||||
|
$pull_request_title = data_get($payload, 'object_attributes.title');
|
||||||
|
$latest_commit_message = data_get($payload, 'object_attributes.last_commit.message');
|
||||||
|
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title, $latest_commit_message]);
|
||||||
if (! $branch) {
|
if (! $branch) {
|
||||||
$return_payloads->push([
|
$return_payloads->push([
|
||||||
'status' => 'failed',
|
'status' => 'failed',
|
||||||
|
|
@ -147,6 +154,17 @@ public function manual(Request $request)
|
||||||
if ($application->isDeployable()) {
|
if ($application->isDeployable()) {
|
||||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||||
|
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 Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$result = queue_application_deployment(
|
$result = queue_application_deployment(
|
||||||
application: $application,
|
application: $application,
|
||||||
|
|
@ -206,6 +224,15 @@ public function manual(Request $request)
|
||||||
if ($x_gitlab_event === 'merge_request') {
|
if ($x_gitlab_event === 'merge_request') {
|
||||||
if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
|
if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
|
||||||
if ($application->isPRDeployable()) {
|
if ($application->isPRDeployable()) {
|
||||||
|
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 Cuid2;
|
$deployment_uuid = new Cuid2;
|
||||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||||
if (! $found) {
|
if (! $found) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Actions\Application\CleanupPreviewDeployment;
|
use App\Actions\Application\CleanupPreviewDeployment;
|
||||||
use App\Enums\ProcessStatus;
|
use App\Enums\ProcessStatus;
|
||||||
|
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
use App\Models\GithubApp;
|
use App\Models\GithubApp;
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
|
|
||||||
class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
|
class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
|
||||||
{
|
{
|
||||||
|
use DetectsSkipDeployCommits;
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
public int $tries = 3;
|
public int $tries = 3;
|
||||||
|
|
@ -31,6 +33,7 @@ public function __construct(
|
||||||
public string $action,
|
public string $action,
|
||||||
public int $pullRequestId,
|
public int $pullRequestId,
|
||||||
public string $pullRequestHtmlUrl,
|
public string $pullRequestHtmlUrl,
|
||||||
|
public ?string $pullRequestTitle,
|
||||||
public ?string $beforeSha,
|
public ?string $beforeSha,
|
||||||
public ?string $afterSha,
|
public ?string $afterSha,
|
||||||
public string $commitSha,
|
public string $commitSha,
|
||||||
|
|
@ -83,6 +86,10 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self::shouldSkipDeployAny([$this->pullRequestTitle])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if PR deployments from public contributors are restricted
|
// Check if PR deployments from public contributors are restricted
|
||||||
if (! $application->settings->is_pr_deployments_public_enabled) {
|
if (! $application->settings->is_pr_deployments_public_enabled) {
|
||||||
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
|
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,6 @@ services:
|
||||||
image: ghcr.io/coollabsio/maxio:latest
|
image: ghcr.io/coollabsio/maxio:latest
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: coolify-minio
|
container_name: coolify-minio
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
ports:
|
ports:
|
||||||
- "${FORWARD_MINIO_PORT:-9000}:9000"
|
- "${FORWARD_MINIO_PORT:-9000}:9000"
|
||||||
- "${FORWARD_MINIO_PORT_CONSOLE:-9001}:9001"
|
- "${FORWARD_MINIO_PORT_CONSOLE:-9001}:9001"
|
||||||
|
|
|
||||||
115
tests/Unit/DetectsSkipDeployCommitsTest.php
Normal file
115
tests/Unit/DetectsSkipDeployCommitsTest.php
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||||
|
|
||||||
|
$harness = new class
|
||||||
|
{
|
||||||
|
use DetectsSkipDeployCommits;
|
||||||
|
};
|
||||||
|
|
||||||
|
$harnessClass = get_class($harness);
|
||||||
|
|
||||||
|
describe('shouldSkipDeploy (all-must-match)', function () use ($harnessClass) {
|
||||||
|
test('returns false when messages array is empty', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeploy([]))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when only nulls or empty strings are provided', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeploy([null, '', null]))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when all messages contain [skip ci]', function () use ($harnessClass) {
|
||||||
|
$messages = [
|
||||||
|
'Update docs [skip ci]',
|
||||||
|
'Fix typo [skip ci]',
|
||||||
|
];
|
||||||
|
expect($harnessClass::shouldSkipDeploy($messages))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when single message contains [skip cd]', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeploy(['Update README [skip cd]']))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true with mixed [skip ci] and [skip cd] (case-insensitive)', function () use ($harnessClass) {
|
||||||
|
$messages = [
|
||||||
|
'Docs [SKIP CI]',
|
||||||
|
'Changelog [Skip Cd]',
|
||||||
|
];
|
||||||
|
expect($harnessClass::shouldSkipDeploy($messages))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when at least one message has no skip marker', function () use ($harnessClass) {
|
||||||
|
$messages = [
|
||||||
|
'Update docs [skip ci]',
|
||||||
|
'Actual feature change',
|
||||||
|
];
|
||||||
|
expect($harnessClass::shouldSkipDeploy($messages))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when single message has no skip marker', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeploy(['Deploy this please']))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('null entries are filtered before evaluation', function () use ($harnessClass) {
|
||||||
|
$messages = [
|
||||||
|
null,
|
||||||
|
'Docs [skip ci]',
|
||||||
|
null,
|
||||||
|
];
|
||||||
|
expect($harnessClass::shouldSkipDeploy($messages))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('matches PR title scenario (single string)', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeploy(['chore: update readme [skip ci]']))->toBeTrue();
|
||||||
|
expect($harnessClass::shouldSkipDeploy(['feat: real change']))->toBeFalse();
|
||||||
|
expect($harnessClass::shouldSkipDeploy([null]))->toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldSkipDeployAny (any-marker)', function () use ($harnessClass) {
|
||||||
|
test('returns false when messages array is empty', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeployAny([]))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when only nulls or empty strings are provided', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeployAny([null, '', null]))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when any one message contains [skip ci]', function () use ($harnessClass) {
|
||||||
|
$messages = [
|
||||||
|
'Real feature change',
|
||||||
|
'docs: update readme [skip ci]',
|
||||||
|
];
|
||||||
|
expect($harnessClass::shouldSkipDeployAny($messages))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when any one message contains [skip cd]', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeployAny(['feature change', 'chore [skip cd]']))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true case-insensitively', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeployAny(['feat: docs [SKIP CI]']))->toBeTrue();
|
||||||
|
expect($harnessClass::shouldSkipDeployAny(['feat: docs [Skip Cd]']))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when no message contains a skip marker', function () use ($harnessClass) {
|
||||||
|
$messages = [
|
||||||
|
'feat: add new endpoint',
|
||||||
|
'fix: handle edge case',
|
||||||
|
];
|
||||||
|
expect($harnessClass::shouldSkipDeployAny($messages))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('null and empty entries are skipped, real markers still match', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeployAny([null, '', 'docs [skip ci]', null]))->toBeTrue();
|
||||||
|
expect($harnessClass::shouldSkipDeployAny([null, '', null]))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PR title alone with skip marker triggers skip', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeployAny(['chore: update readme [skip ci]']))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PR title without skip marker but commit message with skip marker triggers skip', function () use ($harnessClass) {
|
||||||
|
expect($harnessClass::shouldSkipDeployAny(['feat: real change', 'wip [skip cd]']))->toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue