From 4114d9dcb15770a598ffba389abe36ea871e0055 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 4 Feb 2026 17:36:59 +0530 Subject: [PATCH] Move to transformation adapter --- app/controllers/api/vcs.php | 13 +- .../Platform/Modules/Compute/Base.php | 13 +- .../Modules/Functions/Workers/Builds.php | 14 +- .../Adapter/BranchDomain.php} | 99 +++++---- .../Transformation/TransformationTest.php | 119 ++++++++++ tests/unit/Vcs/DomainTest.php | 207 ------------------ 6 files changed, 207 insertions(+), 258 deletions(-) rename src/Appwrite/{Vcs/Domain.php => Transformation/Adapter/BranchDomain.php} (63%) delete mode 100644 tests/unit/Vcs/DomainTest.php diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index c3dd8cfaa6..072f0d69e2 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -9,11 +9,12 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\MethodType; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Transformation\Transformation; use Appwrite\Utopia\Database\Validator\Queries\Installations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Vcs\Comment; -use Appwrite\Vcs\Domain; +use Appwrite\Transformation\Adapter\BranchDomain; use Swoole\Coroutine\WaitGroup; use Utopia\CLI\Console; use Utopia\Config\Adapters\Dotenv as ConfigDotenv; @@ -370,7 +371,15 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId // VCS branch preview if (!empty($providerBranch)) { - $domain = Domain::generateBranchDomain($providerBranch, $resource->getId(), $project->getId(), $sitesDomain); + $transformation = new Transformation([new BranchDomain()]); + $transformation->setInput([ + 'branch' => $providerBranch, + 'resourceId' => $resource->getId(), + 'projectId' => $project->getId(), + 'sitesDomain' => $sitesDomain, + ]); + $transformation->transform(); + $domain = $transformation->getOutput(); $ruleId = md5($domain); try { $authorization->skip( diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 1f42ece7e7..aad9f07460 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -6,7 +6,8 @@ use Appwrite\Event\Build; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Compute\Validator\Specification as SpecificationValidator; -use Appwrite\Vcs\Domain; +use Appwrite\Transformation\Adapter\BranchDomain; +use Appwrite\Transformation\Transformation; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -297,7 +298,15 @@ class Base extends Action // VCS branch preview if (!empty($providerBranch)) { - $domain = Domain::generateBranchDomain($providerBranch, $site->getId(), $project->getId(), $sitesDomain); + $transformation = new Transformation([new BranchDomain()]); + $transformation->setInput([ + 'branch' => $providerBranch, + 'resourceId' => $site->getId(), + 'projectId' => $project->getId(), + 'sitesDomain' => $sitesDomain, + ]); + $transformation->transform(); + $domain = $transformation->getOutput(); $ruleId = md5($domain); try { $authorization->skip( diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 2f58181de1..37b677a24f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -9,9 +9,10 @@ use Appwrite\Event\Realtime; use Appwrite\Event\Screenshot; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; +use Appwrite\Transformation\Transformation; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; -use Appwrite\Vcs\Domain; +use Appwrite\Transformation\Adapter\BranchDomain; use Exception; use Executor\Executor; use Swoole\Coroutine as Co; @@ -1038,8 +1039,15 @@ class Builds extends Action // VCS branch $branchName = $deployment->getAttribute('providerBranch'); if (!empty($branchName)) { - $sitesDomain = $platform['sitesDomain']; - $domain = Domain::generateBranchDomain($branchName, $resource->getId(), $project->getId(), $sitesDomain); + $transformation = new Transformation([new BranchDomain()]); + $transformation->setInput([ + 'branch' => $branchName, + 'resourceId' => $resource->getId(), + 'projectId' => $project->getId(), + 'sitesDomain' => $platform['sitesDomain'], + ]); + $transformation->transform(); + $domain = $transformation->getOutput(); $ruleId = md5($domain); try { diff --git a/src/Appwrite/Vcs/Domain.php b/src/Appwrite/Transformation/Adapter/BranchDomain.php similarity index 63% rename from src/Appwrite/Vcs/Domain.php rename to src/Appwrite/Transformation/Adapter/BranchDomain.php index 93ef881c8f..d74b2ecaa6 100644 --- a/src/Appwrite/Vcs/Domain.php +++ b/src/Appwrite/Transformation/Adapter/BranchDomain.php @@ -1,10 +1,11 @@ $traits + */ + public function isValid(array $traits): bool + { + return true; + } + + /** + * Transform branch name into a valid domain name. + * + * Input should be an array with: + * - 'branch' (string): The branch name + * - 'resourceId' (string): The resource ID (site or function) + * - 'projectId' (string): The project ID + * - 'sitesDomain' (string): The base sites domain + */ + public function transform(): void + { + $branch = $this->input['branch'] ?? ''; + $resourceId = $this->input['resourceId'] ?? ''; + $projectId = $this->input['projectId'] ?? ''; + $sitesDomain = $this->input['sitesDomain'] ?? ''; + + $branchPrefix = $this->generateBranchPrefix($branch); + $resourceProjectHash = substr(hash('sha256', $resourceId . $projectId), 0, self::HASH_SUFFIX_LENGTH); + $this->output = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}"; + } + + /** + * Generate a branch prefix for domain name from a branch name. + * Takes up to 16 characters, sanitizes them for domain use, + * and appends a hash suffix if the branch name is longer than 16 characters. + * + * @param string $branch The branch name + * @return string The branch prefix for domain name + */ + private function generateBranchPrefix(string $branch): string + { + $branchPrefix = substr($branch, 0, self::BRANCH_PREFIX_MAX_LENGTH); + $branchPrefix = $this->sanitizeBranchName($branchPrefix); + + if (strlen($branch) > self::BRANCH_PREFIX_MAX_LENGTH) { + $remainingChars = substr($branch, self::BRANCH_PREFIX_MAX_LENGTH); + $branchPrefix .= '-' . substr(hash('sha256', $remainingChars), 0, self::HASH_SUFFIX_LENGTH); + } + + return $branchPrefix; + } + /** * Sanitize a branch name for use in a domain name. * Replaces any characters that are not alphanumeric or hyphens with hyphens, @@ -24,16 +75,15 @@ class Domain * @param string $branch The branch name to sanitize * @return string The sanitized branch name */ - public static function sanitizeBranchName(string $branch): string + private function sanitizeBranchName(string $branch): string { - // Replace any sequence of invalid characters with a single hyphen $allowedChars = array_merge( Text::NUMBERS, Text::ALPHABET_UPPER, Text::ALPHABET_LOWER, ['-'] ); - $allowedCharsFlip = array_flip($allowedChars); // Flip solves issues with named numeric indexes + $allowedCharsFlip = array_flip($allowedChars); $sanitized = ''; for ($i = 0; $i < \strlen($branch); $i++) { @@ -49,45 +99,6 @@ class Domain } } - // Remove leading and trailing hyphens return trim($sanitized, '-'); } - - /** - * Generate a branch prefix for domain name from a branch name. - * Takes up to 16 characters, sanitizes them for domain use, - * and appends a hash suffix if the branch name is longer than 16 characters. - * - * @param string $branch The branch name - * @return string The branch prefix for domain name - */ - public static function generateBranchPrefix(string $branch): string - { - $branchPrefix = substr($branch, 0, self::BRANCH_PREFIX_MAX_LENGTH); - $branchPrefix = self::sanitizeBranchName($branchPrefix); - - if (strlen($branch) > self::BRANCH_PREFIX_MAX_LENGTH) { - $remainingChars = substr($branch, self::BRANCH_PREFIX_MAX_LENGTH); - $branchPrefix .= '-' . substr(hash('sha256', $remainingChars), 0, self::HASH_SUFFIX_LENGTH); - } - - return $branchPrefix; - } - - /** - * Generate a full branch preview domain name. - * - * @param string $branch The branch name - * @param string $resourceId The resource ID (site or function) - * @param string $projectId The project ID - * @param string $sitesDomain The base sites domain - * @return string The full domain name - */ - public static function generateBranchDomain(string $branch, string $resourceId, string $projectId, string $sitesDomain): string - { - $branchPrefix = self::generateBranchPrefix($branch); - $resourceProjectHash = substr(hash('sha256', $resourceId . $projectId), 0, self::HASH_SUFFIX_LENGTH); - - return "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}"; - } } diff --git a/tests/unit/Transformation/TransformationTest.php b/tests/unit/Transformation/TransformationTest.php index a3169026a3..aa8540f12d 100644 --- a/tests/unit/Transformation/TransformationTest.php +++ b/tests/unit/Transformation/TransformationTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Transformation; +use Appwrite\Transformation\Adapter\BranchDomain; use Appwrite\Transformation\Adapter\Mock; use Appwrite\Transformation\Adapter\Preview; use Appwrite\Transformation\Transformation; @@ -40,4 +41,122 @@ class TransformationTest extends TestCase $this->assertStringContainsString("Preview by", $transformer->getOutput()); $this->assertStringContainsString("Mock:", $transformer->getOutput()); } + + public function testBranchDomain(): void + { + $transformer = new Transformation([new BranchDomain()]); + + // Branch name with slash + $transformer->setInput([ + 'branch' => 'feature/test', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain = $transformer->getOutput(); + $this->assertStringNotContainsString('/', $domain); + $this->assertStringStartsWith('branch-feature-test-', $domain); + $this->assertStringEndsWith('.appwrite.network', $domain); + + // Branch domain consistency + $transformer->setInput([ + 'branch' => 'feature/test', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain2 = $transformer->getOutput(); + $this->assertEquals($domain, $domain2); + + // Different resources should produce different domains + $transformer->setInput([ + 'branch' => 'feature/test', + 'resourceId' => 'site789', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain2 = $transformer->getOutput(); + $this->assertNotEquals($domain, $domain2); + + // Different projects should produce different domains + $transformer->setInput([ + 'branch' => 'feature/test', + 'resourceId' => 'site123', + 'projectId' => 'proj789', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain2 = $transformer->getOutput(); + $this->assertNotEquals($domain, $domain2); + + // Some real-world branch names + $transformer->setInput([ + 'branch' => 'feature/SER-1234', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain = $transformer->getOutput(); + $this->assertStringStartsWith('branch-feature-ser-1234-', $domain); + $this->assertStringEndsWith('.appwrite.network', $domain); + + $transformer->setInput([ + 'branch' => 'bugfix/fix-login', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain = $transformer->getOutput(); + $this->assertStringStartsWith('branch-bugfix-fix-login-', $domain); + $this->assertStringEndsWith('.appwrite.network', $domain); + + $transformer->setInput([ + 'branch' => 'hotfix/v1.2.3', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain = $transformer->getOutput(); + $this->assertStringStartsWith('branch-hotfix-v1-2-3-', $domain); + $this->assertStringEndsWith('.appwrite.network', $domain); + + $transformer->setInput([ + 'branch' => 'release/2024.01', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain = $transformer->getOutput(); + $this->assertStringStartsWith('branch-release-2024-01-', $domain); + $this->assertStringEndsWith('.appwrite.network', $domain); + + $transformer->setInput([ + 'branch' => 'user/john/experiment', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain = $transformer->getOutput(); + $this->assertStringStartsWith('branch-user-john-experiment-', $domain); + $this->assertStringEndsWith('.appwrite.network', $domain); + + $transformer->setInput([ + 'branch' => 'dependabot/npm_and_yarn/lodash-4.17.21', + 'resourceId' => 'site123', + 'projectId' => 'proj456', + 'sitesDomain' => 'appwrite.network' + ]); + $transformer->transform(); + $domain = $transformer->getOutput(); + $this->assertStringStartsWith('branch-dependabot-npm-and-yarn-lodash-4-17-21-', $domain); + $this->assertStringEndsWith('.appwrite.network', $domain); + } } diff --git a/tests/unit/Vcs/DomainTest.php b/tests/unit/Vcs/DomainTest.php deleted file mode 100644 index c1f7e63c7b..0000000000 --- a/tests/unit/Vcs/DomainTest.php +++ /dev/null @@ -1,207 +0,0 @@ -assertEquals('feature-test', Domain::sanitizeBranchName('feature/test')); - $this->assertEquals('user-john-fix', Domain::sanitizeBranchName('user/john/fix')); - $this->assertEquals('abc-test-235', Domain::sanitizeBranchName('abc/test-235')); - } - - public function testSanitizeBranchNameWithUnderscore(): void - { - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature_test')); - $this->assertEquals('my-branch-name', Domain::sanitizeBranchName('my_branch_name')); - } - - public function testSanitizeBranchNameWithMultipleInvalidChars(): void - { - // Multiple consecutive invalid characters should become a single hyphen - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature//test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature__test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature/_test')); - } - - public function testSanitizeBranchNameWithSpecialChars(): void - { - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature@test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature#test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature$test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature%test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature&test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature*test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature+test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature=test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature!test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature~test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature`test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature^test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature:test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature;test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature,test')); - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature.test')); - } - - public function testSanitizeBranchNameWithSpaces(): void - { - $this->assertEquals('feature-test', Domain::sanitizeBranchName('feature test')); - $this->assertEquals('my-branch-name', Domain::sanitizeBranchName('my branch name')); - } - - public function testSanitizeBranchNameTrimsHyphens(): void - { - // Leading and trailing invalid chars should be removed - $this->assertEquals('feature', Domain::sanitizeBranchName('/feature')); - $this->assertEquals('feature', Domain::sanitizeBranchName('feature/')); - $this->assertEquals('feature', Domain::sanitizeBranchName('/feature/')); - $this->assertEquals('feature', Domain::sanitizeBranchName('//feature//')); - } - - public function testSanitizeBranchNamePreservesValidChars(): void - { - // Valid branch names should remain unchanged - $this->assertEquals('main', Domain::sanitizeBranchName('main')); - $this->assertEquals('develop', Domain::sanitizeBranchName('develop')); - $this->assertEquals('feature-123', Domain::sanitizeBranchName('feature-123')); - $this->assertEquals('v1-2-3', Domain::sanitizeBranchName('v1-2-3')); - $this->assertEquals('UPPERCASE', Domain::sanitizeBranchName('UPPERCASE')); - $this->assertEquals('MixedCase123', Domain::sanitizeBranchName('MixedCase123')); - } - - public function testSanitizeBranchNameWithEmptyString(): void - { - $this->assertEquals('', Domain::sanitizeBranchName('')); - } - - public function testSanitizeBranchNameWithOnlyInvalidChars(): void - { - $this->assertEquals('', Domain::sanitizeBranchName('///')); - $this->assertEquals('', Domain::sanitizeBranchName('___')); - $this->assertEquals('', Domain::sanitizeBranchName('@#$')); - } - - /** - * Test generating branch prefix for domain names - */ - public function testGenerateBranchPrefixShortBranch(): void - { - // Branch names <= 16 characters should not have hash suffix - $prefix = Domain::generateBranchPrefix('main'); - $this->assertEquals('main', $prefix); - - $prefix = Domain::generateBranchPrefix('feature-test'); - $this->assertEquals('feature-test', $prefix); - - $prefix = Domain::generateBranchPrefix('exactly16chars12'); - $this->assertEquals('exactly16chars12', $prefix); - } - - public function testGenerateBranchPrefixLongBranch(): void - { - // Branch names > 16 characters should have hash suffix - $prefix = Domain::generateBranchPrefix('this-is-a-very-long-branch-name'); - // First 16 chars: "this-is-a-very-l" + hash of remaining chars - $this->assertStringStartsWith('this-is-a-very-l-', $prefix); - $this->assertEquals(24, strlen($prefix)); // 16 + 1 (hyphen) + 7 (hash) - } - - public function testGenerateBranchPrefixWithInvalidChars(): void - { - // Branch with slash should be sanitized - $prefix = Domain::generateBranchPrefix('feature/test'); - $this->assertEquals('feature-test', $prefix); - - // Long branch with slash - $prefix = Domain::generateBranchPrefix('feature/very/long/branch/name'); - $this->assertStringStartsWith('feature-very-lon-', $prefix); - $this->assertEquals(24, strlen($prefix)); - } - - public function testGenerateBranchPrefixConsistency(): void - { - // Same input should produce same output - $prefix1 = Domain::generateBranchPrefix('feature/my-long-branch-name'); - $prefix2 = Domain::generateBranchPrefix('feature/my-long-branch-name'); - $this->assertEquals($prefix1, $prefix2); - } - - public function testGenerateBranchPrefixDifferentHashes(): void - { - // Different branch names with same first 16 chars should have different hashes - $prefix1 = Domain::generateBranchPrefix('feature-branch-01234567890'); - $prefix2 = Domain::generateBranchPrefix('feature-branch-0abcdefghij'); - - // Both start with sanitized first 16 chars - $this->assertStringStartsWith('feature-branch-0-', $prefix1); - $this->assertStringStartsWith('feature-branch-0-', $prefix2); - - // But have different hash suffixes - $this->assertNotEquals($prefix1, $prefix2); - } - - /** - * Test generating full branch domain names - */ - public function testGenerateBranchDomain(): void - { - $domain = Domain::generateBranchDomain('main', 'site123', 'proj456', 'appwrite.network'); - $this->assertStringStartsWith('branch-main-', $domain); - $this->assertStringEndsWith('.appwrite.network', $domain); - } - - public function testGenerateBranchDomainWithSlash(): void - { - $domain = Domain::generateBranchDomain('feature/test', 'site123', 'proj456', 'appwrite.network'); - // Should NOT contain slash - $this->assertStringNotContainsString('/', $domain); - $this->assertStringStartsWith('branch-feature-test-', $domain); - $this->assertStringEndsWith('.appwrite.network', $domain); - } - - public function testGenerateBranchDomainConsistency(): void - { - // Same inputs should produce same domain - $domain1 = Domain::generateBranchDomain('feature/test', 'site123', 'proj456', 'appwrite.network'); - $domain2 = Domain::generateBranchDomain('feature/test', 'site123', 'proj456', 'appwrite.network'); - $this->assertEquals($domain1, $domain2); - } - - public function testGenerateBranchDomainDifferentResources(): void - { - // Different resources should produce different domains - $domain1 = Domain::generateBranchDomain('main', 'site123', 'proj456', 'appwrite.network'); - $domain2 = Domain::generateBranchDomain('main', 'site789', 'proj456', 'appwrite.network'); - $this->assertNotEquals($domain1, $domain2); - } - - public function testGenerateBranchDomainDifferentProjects(): void - { - // Different projects should produce different domains - $domain1 = Domain::generateBranchDomain('main', 'site123', 'proj456', 'appwrite.network'); - $domain2 = Domain::generateBranchDomain('main', 'site123', 'proj789', 'appwrite.network'); - $this->assertNotEquals($domain1, $domain2); - } - - /** - * Test real-world branch name scenarios - */ - public function testRealWorldBranchNames(): void - { - // Common Git branch naming conventions - $this->assertEquals('feature-SER-1234', Domain::sanitizeBranchName('feature/SER-1234')); - $this->assertEquals('bugfix-fix-login', Domain::sanitizeBranchName('bugfix/fix-login')); - $this->assertEquals('hotfix-v1-2-3', Domain::sanitizeBranchName('hotfix/v1.2.3')); - $this->assertEquals('release-2024-01', Domain::sanitizeBranchName('release/2024.01')); - $this->assertEquals('user-john-experiment', Domain::sanitizeBranchName('user/john/experiment')); - $this->assertEquals('dependabot-npm-and-yarn-lodash-4-17-21', Domain::sanitizeBranchName('dependabot/npm_and_yarn/lodash-4.17.21')); - } -}