diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 933de12290..120c9704ce 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.lock b/composer.lock index a0a687d8ae..07f8594b55 100644 --- a/composer.lock +++ b/composer.lock @@ -5355,16 +5355,16 @@ }, { "name": "utopia-php/validators", - "version": "0.2.3", + "version": "0.2.4", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23" + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/9770269c8ed8e6909934965fa8722103c7434c23", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/b4ee60db4dbae5ffbe53968d01f69b6941251576", + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576", "shasum": "" }, "require": { @@ -5394,9 +5394,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.2.3" + "source": "https://github.com/utopia-php/validators/tree/0.2.4" }, - "time": "2026-05-14T08:05:44+00:00" + "time": "2026-05-21T12:47:43+00:00" }, { "name": "utopia-php/vcs", diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 148f0945ac..3f980275a2 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -95,6 +95,8 @@ class Create extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -147,6 +149,8 @@ class Create extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, string $templateRepository, @@ -248,6 +252,8 @@ class Create extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, ])); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index e8713a179d..aca4ead98e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -87,6 +87,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('providerBranches', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -132,6 +134,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + ?array $providerBranches, + ?array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -276,6 +280,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches ?? $function->getAttribute('providerBranches', []), + 'providerPaths' => $providerPaths ?? $function->getAttribute('providerPaths', []), 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$functionId, $name, $runtime]), diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index d01d0d8ca7..9f3d46ffd0 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -19,6 +19,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Range; use Utopia\Validator\Text; @@ -78,6 +79,8 @@ class Create extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -118,6 +121,8 @@ class Create extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -173,6 +178,8 @@ class Create extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'buildRuntime' => $buildRuntime, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 2aee03265e..bfd8c9f198 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -22,7 +22,9 @@ use Utopia\Http\Adapter\Swoole\Request; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; +use Utopia\Validator\Nullable; use Utopia\Validator\Range; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -81,6 +83,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) + ->param('providerBranches', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -126,6 +130,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + ?array $providerBranches, + ?array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -271,6 +277,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches ?? $site->getAttribute('providerBranches', []), + 'providerPaths' => $providerPaths ?? $site->getAttribute('providerPaths', []), 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$siteId, $name, $framework]), 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 a40d7fc6b9..993740c61a 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 @@ -130,7 +130,14 @@ 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, $publisherForBuilds, $getProjectDB, $platform); + $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); + $providerAffectedFiles = [ + ...array_column($prFiles, 'filename'), + // Only renamed files include previous_filename; skip missing values from other file changes. + ...array_filter(array_column($prFiles, 'previous_filename')) + ]; + + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, true, $dbForPlatform, $authorization, $publisherForBuilds, $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 a6f0e7fd6d..27c4eacba3 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -22,6 +22,7 @@ use Utopia\DSN\DSN; use Utopia\Span\Span; use Utopia\System\System; use Utopia\Validator\Contains; +use Utopia\Validator\Globstar; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; @@ -42,6 +43,7 @@ trait Deployment string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, + array $providerAffectedFiles, bool $external, Database $dbForPlatform, Authorization $authorization, @@ -103,6 +105,32 @@ trait Deployment continue; } + // Skip deployments when the branch or affected files do not match configured build triggers. + $branchTrigger = new Globstar($resource->getAttribute('providerBranches', [])); + if (!$branchTrigger->isValid($providerBranch)) { + Span::add("{$logBase}.build.skipped.reason", 'branch'); + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + + $providerPaths = $resource->getAttribute('providerPaths', []); + if (!empty($providerPaths) && !empty($providerAffectedFiles)) { + $pathTrigger = new Globstar($providerPaths); + $pathMatched = false; + foreach ($providerAffectedFiles as $file) { + if ($pathTrigger->isValid($file)) { + $pathMatched = true; + break; + } + } + + if (!$pathMatched) { + Span::add("{$logBase}.build.skipped.reason", 'path'); + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + } + $deploymentId = ID::unique(); $repositoryId = $repository->getId(); $repositoryInternalId = $repository->getSequence(); 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 0b81504309..c79df05f8a 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -133,7 +133,6 @@ class Create extends Action callable $getProjectDB, array $platform, ) { - $providerBranchCreated = $parsedPayload["branchCreated"] ?? false; $providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false; $providerBranch = $parsedPayload["branch"] ?? ''; $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; @@ -164,7 +163,8 @@ class Create extends Action // Create new deployment only on push (not committed by us) and not when branch is deleted if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchDeleted) { - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); + $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', $providerAffectedFiles, false, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); } } @@ -211,12 +211,19 @@ class Create extends Action $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; + $prFiles = $github->getPullRequestFiles($providerRepositoryOwner, $providerRepositoryName, $providerPullRequestId); + $providerAffectedFiles = [ + ...array_column($prFiles, 'filename'), + // Only renamed files include previous_filename; skip missing values from other file changes. + ...array_filter(array_column($prFiles, 'previous_filename')) + ]; + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, $external, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); } elseif ($action == "closed") { // Allowed external contributions cleanup