From 3f555e306132637db1470a64bbd0baa4f8aa8880 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 24 Mar 2026 09:25:49 +0530 Subject: [PATCH] feat: implement custom triggers for VCS builds --- app/config/collections/projects.php | 44 +++++++++++ composer.json | 2 +- composer.lock | 29 +++++--- .../Http/GitHub/Authorize/External/Update.php | 5 +- .../Modules/VCS/Http/GitHub/Deployment.php | 74 +++++++++++++++++++ .../Modules/VCS/Http/GitHub/Events/Create.php | 6 +- 6 files changed, 146 insertions(+), 14 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index b41e8f0fd5..80933a46e1 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -841,6 +841,28 @@ return [ 'array' => true, 'filters' => [], ], + [ + '$id' => ID::custom('providerBranches'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerPaths'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1320,6 +1342,28 @@ return [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('providerBranches'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerPaths'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/composer.json b/composer.json index 65838a1615..b37aa0cad5 100644 --- a/composer.json +++ b/composer.json @@ -84,7 +84,7 @@ "utopia-php/storage": "1.0.*", "utopia-php/system": "0.10.*", "utopia-php/telemetry": "0.2.*", - "utopia-php/vcs": "3.*", + "utopia-php/vcs": "dev-ser-401 as 3.0.99", "utopia-php/websocket": "1.0.*", "matomo/device-detector": "6.4.*", "dragonmantank/cron-expression": "3.4.*", diff --git a/composer.lock b/composer.lock index de577e2d78..eb80296f2a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f9225f2b580de0ccb796b2fb8c881384", + "content-hash": "695d5fa15d0688d195c3848af81bb0bf", "packages": [ { "name": "adhocore/jwt", @@ -5216,16 +5216,16 @@ }, { "name": "utopia-php/vcs", - "version": "3.0.1", + "version": "dev-ser-401", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "0efe842d695acb4b184f5306a836169c771fbcea" + "reference": "b37bf9c465db41b25bd741783ae36bf6b898fbdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/0efe842d695acb4b184f5306a836169c771fbcea", - "reference": "0efe842d695acb4b184f5306a836169c771fbcea", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/b37bf9c465db41b25bd741783ae36bf6b898fbdb", + "reference": "b37bf9c465db41b25bd741783ae36bf6b898fbdb", "shasum": "" }, "require": { @@ -5259,9 +5259,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/3.0.1" + "source": "https://github.com/utopia-php/vcs/tree/ser-401" }, - "time": "2026-03-23T15:58:31+00:00" + "time": "2026-03-24T03:31:54+00:00" }, { "name": "utopia-php/websocket", @@ -8433,9 +8433,18 @@ "time": "2024-11-07T12:36:22+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/vcs", + "version": "dev-ser-401", + "alias": "3.0.99", + "alias_normalized": "3.0.99.0" + } + ], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/vcs": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -8456,5 +8465,5 @@ "platform-dev": { "ext-fileinfo": "*" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php index 4a34ffd36a..79657e5f17 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php @@ -133,7 +133,10 @@ class Update extends Action $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? ''; - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); + $providerAffectedFiles = array_column($prFiles, 'filename'); + + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 638ceab59e..abec622fe3 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -40,6 +40,7 @@ trait Deployment string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, + array $providerAffectedFiles = [], bool $external, Database $dbForPlatform, Authorization $authorization, @@ -90,6 +91,11 @@ trait Deployment $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); $resourceInternalId = $resource->getSequence(); + if (!$this->isResourceBuildable($resource, $providerBranch, $providerAffectedFiles, $logBase)) { + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + $deploymentId = ID::unique(); $repositoryId = $repository->getId(); $repositoryInternalId = $repository->getSequence(); @@ -525,4 +531,72 @@ trait Deployment { return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME); } + + private function isResourceBuildable(Document $resource, string $providerBranch, array $providerAffectedFiles, string $logBase): bool + { + $allowedBranches = $resource->getAttribute('providerBranches', []); + if (!$this->matchesPatterns($providerBranch, $allowedBranches)) { + Span::add("{$logBase}.build.skipped.reason", 'branch'); + return false; + } + + $allowedPaths = $resource->getAttribute('providerPaths', []); + if (!empty($allowedPaths) && !empty($providerAffectedFiles)) { + $pathMatched = false; + foreach ($providerAffectedFiles as $file) { + if ($this->matchesPatterns($file, $allowedPaths)) { + $pathMatched = true; + break; + } + } + if (!$pathMatched) { + Span::add("{$logBase}.build.skipped.reason", 'path'); + return false; + } + } + + return true; + } + + private function matchesPatterns(string $subject, array $patterns): bool + { + if (empty($patterns)) { + return true; + } + + $include = array_filter($patterns, fn ($p) => !str_starts_with($p, '!')); + $exclude = array_filter($patterns, fn ($p) => str_starts_with($p, '!')); + + foreach ($include as $pattern) { + if ($this->matchGlob($subject, $pattern)) { + return true; + } + } + + foreach ($exclude as $pattern) { + if ($this->matchGlob($subject, substr($pattern, 1))) { + return false; + } + } + + return empty($include); + } + + private function matchGlob(string $subject, string $pattern): bool + { + $regex = preg_replace_callback( + '/\*\*|\*|\?|[^*?]+/', + static function (array $m): string { + return match ($m[0]) { + '**' => '.*', + '*' => '[^/]*', + '?' => '[^/]', + default => preg_quote($m[0], '/'), + }; + }, + $pattern + ); + + return (bool) preg_match('/^' . $regex . '$/', $subject); + } } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php index c614c80041..df334d259e 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -164,7 +164,8 @@ class Create extends Action // Create new deployment only on push (not committed by us) and not when branch is created or deleted if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) { - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', $providerAffectedFiles, false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); } } @@ -216,7 +217,8 @@ class Create extends Action Query::orderDesc('$createdAt') ])); - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); } elseif ($action == "closed") { // Allowed external contributions cleanup