Move to transformation adapter

This commit is contained in:
Hemachandar
2026-02-04 17:36:59 +05:30
parent 90c96e9318
commit 4114d9dcb1
6 changed files with 207 additions and 258 deletions
+11 -2
View File
@@ -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(
+11 -2
View File
@@ -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(
@@ -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 {
@@ -1,10 +1,11 @@
<?php
namespace Appwrite\Vcs;
namespace Appwrite\Transformation\Adapter;
use Appwrite\Transformation\Adapter;
use Utopia\Validator\Text;
class Domain
class BranchDomain extends Adapter
{
/**
* Maximum length for branch prefix in domain name
@@ -16,6 +17,56 @@ class Domain
*/
public const HASH_SUFFIX_LENGTH = 7;
/**
* @param array<mixed> $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}";
}
}
@@ -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);
}
}
-207
View File
@@ -1,207 +0,0 @@
<?php
namespace Tests\Unit\Vcs;
use Appwrite\Vcs\Domain;
use PHPUnit\Framework\TestCase;
class DomainTest extends TestCase
{
/**
* Test sanitizing branch names with various invalid characters
*/
public function testSanitizeBranchNameWithSlash(): void
{
$this->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'));
}
}