mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
validator pattern
This commit is contained in:
@@ -7,6 +7,7 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
|
||||
use Appwrite\Vcs\Comment;
|
||||
use Appwrite\Vcs\Validator\BuildTrigger;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
@@ -534,22 +535,15 @@ trait Deployment
|
||||
|
||||
private function isResourceBuildable(Document $resource, string $providerBranch, array $providerAffectedFiles, string $logBase): bool
|
||||
{
|
||||
$allowedBranches = $resource->getAttribute('providerBranches', []);
|
||||
if (!$this->matchesPatterns($providerBranch, $allowedBranches)) {
|
||||
$branchTrigger = new BuildTrigger($resource->getAttribute('providerBranches', []));
|
||||
if (!$branchTrigger->isValid($providerBranch)) {
|
||||
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) {
|
||||
$pathTrigger = new BuildTrigger($resource->getAttribute('providerPaths', []));
|
||||
foreach ($providerAffectedFiles as $file) {
|
||||
if (!$pathTrigger->isValid($file)) {
|
||||
Span::add("{$logBase}.build.skipped.reason", 'path');
|
||||
return false;
|
||||
}
|
||||
@@ -557,46 +551,4 @@ trait Deployment
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Vcs\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
class BuildTrigger extends Validator
|
||||
{
|
||||
public function __construct(private readonly array $patterns) {}
|
||||
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->patterns)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$include = array_filter($this->patterns, fn ($p) => !str_starts_with($p, '!'));
|
||||
$exclude = array_filter($this->patterns, fn ($p) => str_starts_with($p, '!'));
|
||||
|
||||
foreach ($include as $pattern) {
|
||||
if ($this->matchGlob($value, $pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($exclude as $pattern) {
|
||||
if ($this->matchGlob($value, substr($pattern, 1))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return empty($include);
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Value must match at least one inclusion pattern and must not match any exclusion pattern.';
|
||||
}
|
||||
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
|
||||
private function matchGlob(string $subject, string $pattern): bool
|
||||
{
|
||||
$regex = '';
|
||||
$len = strlen($pattern);
|
||||
$i = 0;
|
||||
|
||||
while ($i < $len) {
|
||||
$char = $pattern[$i];
|
||||
|
||||
if ($char === '*' && isset($pattern[$i + 1]) && $pattern[$i + 1] === '*') {
|
||||
$prevSlash = $i === 0 || $pattern[$i - 1] === '/';
|
||||
$nextSlash = isset($pattern[$i + 2]) && $pattern[$i + 2] === '/';
|
||||
|
||||
if ($prevSlash && $nextSlash) {
|
||||
// a/**/b → zero or more intermediate dirs (matches a/b, a/x/b, a/x/y/b)
|
||||
// **/foo → zero or more leading dirs (matches foo, a/foo, a/b/foo)
|
||||
$regex .= '(?:.+/)?';
|
||||
$i += 3; // consume ** and the trailing /
|
||||
} else {
|
||||
// foo/** → everything inside (matches foo/a, foo/a/b)
|
||||
$regex .= '.*';
|
||||
$i += 2;
|
||||
}
|
||||
} elseif ($char === '*') {
|
||||
$regex .= '[^/]*'; // anything except a path separator
|
||||
$i++;
|
||||
} elseif ($char === '?') {
|
||||
$regex .= '[^/]'; // any single character except a path separator
|
||||
$i++;
|
||||
} else {
|
||||
$regex .= preg_quote($char, '~');
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
return (bool) preg_match('~^' . $regex . '$~', $subject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Vcs\Validator;
|
||||
|
||||
use Appwrite\Vcs\Validator\BuildTrigger;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BuildTriggerTest extends TestCase
|
||||
{
|
||||
public function testEmptyPatterns(): void
|
||||
{
|
||||
$validator = new BuildTrigger([]);
|
||||
$this->assertTrue($validator->isValid('anything'));
|
||||
$this->assertTrue($validator->isValid('main'));
|
||||
}
|
||||
|
||||
public function testExactMatch(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['main']);
|
||||
$this->assertTrue($validator->isValid('main'));
|
||||
$this->assertFalse($validator->isValid('develop'));
|
||||
}
|
||||
|
||||
public function testSingleWildcard(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['feature/*']);
|
||||
$this->assertTrue($validator->isValid('feature/foo'));
|
||||
$this->assertFalse($validator->isValid('feature/foo/bar')); // * does not cross /
|
||||
$this->assertFalse($validator->isValid('main'));
|
||||
}
|
||||
|
||||
public function testDashInPattern(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['feature/test-*']);
|
||||
$this->assertTrue($validator->isValid('feature/test-1'));
|
||||
$this->assertTrue($validator->isValid('feature/test-abc'));
|
||||
$this->assertFalse($validator->isValid('feature/other'));
|
||||
}
|
||||
|
||||
public function testDoubleWildcardEnd(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['src/**']);
|
||||
$this->assertTrue($validator->isValid('src/foo.js'));
|
||||
$this->assertTrue($validator->isValid('src/a/b/c.js'));
|
||||
}
|
||||
|
||||
public function testDoubleWildcardMiddle(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['a/**/b']);
|
||||
$this->assertTrue($validator->isValid('a/b')); // zero intermediate dirs
|
||||
$this->assertTrue($validator->isValid('a/x/b')); // one
|
||||
$this->assertTrue($validator->isValid('a/x/y/b')); // two
|
||||
$this->assertFalse($validator->isValid('a/b/c'));
|
||||
}
|
||||
|
||||
public function testDoubleWildcardStart(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['**/foo']);
|
||||
$this->assertTrue($validator->isValid('foo'));
|
||||
$this->assertTrue($validator->isValid('a/foo'));
|
||||
$this->assertTrue($validator->isValid('a/b/foo'));
|
||||
$this->assertFalse($validator->isValid('foobar'));
|
||||
}
|
||||
|
||||
public function testOrSemantics(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['main', 'develop']);
|
||||
$this->assertTrue($validator->isValid('main'));
|
||||
$this->assertTrue($validator->isValid('develop'));
|
||||
$this->assertFalse($validator->isValid('feature/x'));
|
||||
}
|
||||
|
||||
public function testInclusionTakesPrecedenceOverExclusion(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['!feature/*', 'feature/abc']);
|
||||
$this->assertTrue($validator->isValid('feature/abc')); // inclusion wins
|
||||
$this->assertFalse($validator->isValid('feature/xyz')); // excluded
|
||||
}
|
||||
|
||||
public function testOnlyExclusions(): void
|
||||
{
|
||||
$validator = new BuildTrigger(['!main']);
|
||||
$this->assertFalse($validator->isValid('main'));
|
||||
$this->assertTrue($validator->isValid('develop')); // not excluded, passes by default
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user