diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 287c33e5ec..917321aaa5 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -14363,10 +14363,22 @@ "description": "Path to function code in the template repo.", "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" }, "activate": { "type": "boolean", @@ -14378,7 +14390,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } @@ -32184,10 +32197,22 @@ "description": "Path to site code in the template repo.", "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" }, "activate": { "type": "boolean", @@ -32199,7 +32224,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index ff4af79c91..d28d98f07a 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -13152,10 +13152,22 @@ "description": "Path to function code in the template repo.", "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" }, "activate": { "type": "boolean", @@ -13167,7 +13179,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } @@ -22711,10 +22724,22 @@ "description": "Path to site code in the template repo.", "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" }, "activate": { "type": "boolean", @@ -22726,7 +22751,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 4562120363..0e0504aca2 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -14306,11 +14306,24 @@ "default": null, "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the function template.", + "description": "Type for the reference provided. Can be commit, branch, or tag", "default": null, - "x-example": "" + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": null, + "x-example": "" }, "activate": { "type": "boolean", @@ -14323,7 +14336,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } @@ -32292,11 +32306,24 @@ "default": null, "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", + "description": "Type for the reference provided. Can be commit, branch, or tag", "default": null, - "x-example": "" + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": null, + "x-example": "" }, "activate": { "type": "boolean", @@ -32309,7 +32336,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 9603ed313f..5e70a734af 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -13122,11 +13122,24 @@ "default": null, "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the function template.", + "description": "Type for the reference provided. Can be commit, branch, or tag", "default": null, - "x-example": "" + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": null, + "x-example": "" }, "activate": { "type": "boolean", @@ -13139,7 +13152,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } @@ -22867,11 +22881,24 @@ "default": null, "x-example": "" }, - "version": { + "type": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", + "description": "Type for the reference provided. Can be commit, branch, or tag", "default": null, - "x-example": "" + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": null, + "x-example": "" }, "activate": { "type": "boolean", @@ -22884,7 +22911,8 @@ "repository", "owner", "rootDirectory", - "version" + "type", + "reference" ] } } diff --git a/app/controllers/general.php b/app/controllers/general.php index 07de95a38f..e0435cd499 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -23,6 +23,7 @@ use Appwrite\Utopia\Request\Filters\V17 as RequestV17; use Appwrite\Utopia\Request\Filters\V18 as RequestV18; use Appwrite\Utopia\Request\Filters\V19 as RequestV19; use Appwrite\Utopia\Request\Filters\V20 as RequestV20; +use Appwrite\Utopia\Request\Filters\V21 as RequestV21; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\V16 as ResponseV16; use Appwrite\Utopia\Response\Filters\V17 as ResponseV17; @@ -906,6 +907,9 @@ App::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } + if (version_compare($requestFormat, '1.9.0', '<')) { + $request->addFilter(new RequestV21()); + } } $domain = $request->getHostname(); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index bbe84c56ee..00c29d6bba 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP; use Utopia\Swoole\Request; use Utopia\Validator\Boolean; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base @@ -65,7 +66,8 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.') + ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -83,7 +85,8 @@ class Create extends Base string $repository, string $owner, string $rootDirectory, - string $version, + string $type, + string $reference, bool $activate, Request $request, Response $response, @@ -100,11 +103,16 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } + $branchUrl = "https://github.com/$owner/$repository/blob/$reference"; + + $repositoryUrl = "https://github.com/$owner/$repository"; + $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'version' => $version + 'referenceType' => $type, + 'referenceValue' => $reference, ]); if (!empty($function->getAttribute('providerRepositoryId'))) { @@ -146,7 +154,12 @@ class Create extends Base 'resourceType' => 'functions', 'entrypoint' => $function->getAttribute('entrypoint', ''), 'buildCommands' => $function->getAttribute('commands', ''), - 'type' => 'manual', + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '', + 'type' => 'vcs', 'activate' => $activate, ])); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index f9aa60db5f..22b302f26e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -310,20 +310,23 @@ class Builds extends Action // Non-VCS + Template $templateRepositoryName = $template->getAttribute('repositoryName', ''); $templateOwnerName = $template->getAttribute('ownerName', ''); - $templateVersion = $template->getAttribute('version', ''); + $templateReferenceType = $template->getAttribute('referenceType', ''); + $templateReferenceValue = $template->getAttribute('referenceValue', ''); $templateRootDirectory = $template->getAttribute('rootDirectory', ''); $templateRootDirectory = \rtrim($templateRootDirectory, '/'); $templateRootDirectory = \ltrim($templateRootDirectory, '.'); $templateRootDirectory = \ltrim($templateRootDirectory, '/'); - if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) { + if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateReferenceType) && !empty($templateReferenceValue)) { $stdout = ''; $stderr = ''; // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template'; - $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); + + $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory); + $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); if ($exit !== 0) { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index dc90045b0c..dc7d4c4ace 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -23,6 +23,7 @@ use Utopia\Swoole\Request; use Utopia\System\System; use Utopia\Validator\Boolean; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base @@ -67,7 +68,8 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.') + ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -85,7 +87,8 @@ class Create extends Base string $repository, string $owner, string $rootDirectory, - string $version, + string $type, + string $reference, bool $activate, Request $request, Response $response, @@ -102,11 +105,15 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } + $branchUrl = "https://github.com/$owner/$repository/blob/$reference"; + $repositoryUrl = "https://github.com/$owner/$repository"; + $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'version' => $version + 'referenceType' => $type, + 'referenceValue' => $reference ]); if (!empty($site->getAttribute('providerRepositoryId'))) { @@ -157,9 +164,14 @@ class Create extends Base 'resourceType' => 'sites', 'buildCommands' => \implode(' && ', $commands), 'buildOutput' => $site->getAttribute('outputDirectory', ''), + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '', 'adapter' => $site->getAttribute('adapter', ''), 'fallbackFile' => $site->getAttribute('fallbackFile', ''), - 'type' => 'manual', + 'type' => 'vcs', 'activate' => $activate, ])); diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php new file mode 100644 index 0000000000..3ef0becf1d --- /dev/null +++ b/src/Appwrite/Utopia/Request/Filters/V21.php @@ -0,0 +1,34 @@ +convertVersionToTypeAndReference($content); + break; + } + return $content; + } + + /** + * Convert version parameter to type and reference for backwards compatibility + * with 1.8.0 template deployment endpoints + */ + protected function convertVersionToTypeAndReference(array $content): array + { + if (!empty($content['version'])) { + $content['type'] = 'tag'; + $content['reference'] = $content['version']; + unset($content['version']); + } + return $content; + } +} diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 8f5477331a..dc49d27aea 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -28,6 +28,7 @@ class UsageTest extends Scope FunctionsBase::createVariable insteadof SitesBase; FunctionsBase::getVariable insteadof SitesBase; FunctionsBase::listVariables insteadof SitesBase; + FunctionsBase::helperGetLatestCommit insteadof SitesBase; FunctionsBase::updateVariable insteadof SitesBase; FunctionsBase::deleteVariable insteadof SitesBase; FunctionsBase::getDeployment insteadof SitesBase; diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 27b67d851d..7403b23a73 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -271,6 +271,29 @@ trait FunctionsBase return $template; } + protected function helperGetLatestCommit(string $owner, string $repository): ?string + { + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + return $commitData['sha']; + } + } + + return null; + } + protected function createExecution(string $functionId, mixed $params = []): mixed { $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index f5846af959..8cc986b072 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -361,7 +361,7 @@ class FunctionsCustomServerTest extends Scope $starterTemplate = $this->getTemplate('starter'); $this->assertEquals(200, $starterTemplate['headers']['status-code']); - $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { return $runtime['name'] === 'node-22'; }))[0]; @@ -374,15 +374,15 @@ class FunctionsCustomServerTest extends Scope 'name' => $starterTemplate['body']['name'], 'runtime' => 'node-22', 'execute' => $starterTemplate['body']['permissions'], - 'entrypoint' => $phpRuntime['entrypoint'], + 'entrypoint' => $runtime['entrypoint'], 'events' => $starterTemplate['body']['events'], 'schedule' => $starterTemplate['body']['cron'], 'timeout' => $starterTemplate['body']['timeout'], - 'commands' => $phpRuntime['commands'], + 'commands' => $runtime['commands'], 'scopes' => $starterTemplate['body']['scopes'], 'templateRepository' => $starterTemplate['body']['providerRepositoryId'], 'templateOwner' => $starterTemplate['body']['providerOwner'], - 'templateRootDirectory' => $phpRuntime['providerRootDirectory'], + 'templateRootDirectory' => $runtime['providerRootDirectory'], 'templateVersion' => $starterTemplate['body']['providerVersion'], ] ); @@ -399,19 +399,29 @@ class FunctionsCustomServerTest extends Scope 'activate' => true, 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], - 'rootDirectory' => $phpRuntime['providerRootDirectory'], - 'version' => $starterTemplate['body']['providerVersion'], + 'rootDirectory' => $runtime['providerRootDirectory'], + 'type' => 'tag', + 'reference' => $starterTemplate['body']['providerVersion'], ] ); $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + // Wait for deployment to be ready + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + // Verify deployment sizes + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals(0, $deployment['body']['sourceSize']); - $this->assertEquals(0, $deployment['body']['buildSize']); - $this->assertEquals(0, $deployment['body']['totalSize']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); $deployments = $this->listDeployments($functionId); @@ -433,16 +443,7 @@ class FunctionsCustomServerTest extends Scope $lastDeployment = $deployments['body']['deployments'][0]; $this->assertNotEmpty($lastDeployment['$id']); - $this->assertEquals(0, $lastDeployment['sourceSize']); - - $deploymentId = $lastDeployment['$id']; - - $this->assertEventually(function () use ($functionId, $deploymentId) { - $deployment = $this->getDeployment($functionId, $deploymentId); - - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('ready', $deployment['body']['status']); - }, 50000, 1000); + $this->assertGreaterThan(0, $lastDeployment['sourceSize']); $function = $this->getFunction($functionId); @@ -511,7 +512,144 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($deployment['body']['$id'], $function['body']['deploymentId']); $this->assertEquals($deployment['body']['$createdAt'], $function['body']['deploymentCreatedAt']); - $function = $this->cleanupFunction($functionId); + $this->cleanupFunction($functionId); + } + + public function testCreateFunctionAndDeploymentFromTemplateBranch() + { + $starterTemplate = $this->getTemplate('starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); + + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + return $runtime['name'] === 'node-22'; + }))[0]; + + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + + $function = $this->createFunction( + [ + 'functionId' => ID::unique(), + 'name' => $starterTemplate['body']['name'] . ' - Branch Test', + 'runtime' => 'node-22', + 'execute' => $starterTemplate['body']['permissions'], + 'entrypoint' => $runtime['entrypoint'], + 'events' => $starterTemplate['body']['events'], + 'schedule' => $starterTemplate['body']['cron'], + 'timeout' => $starterTemplate['body']['timeout'], + 'commands' => $runtime['commands'], + 'scopes' => $starterTemplate['body']['scopes'], + ] + ); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $function['body']['$id'] ?? ''; + + // Deploy using branch + $deployment = $this->createTemplateDeployment( + $functionId, + [ + 'resourceId' => ID::unique(), + 'activate' => true, + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'owner' => $starterTemplate['body']['providerOwner'], + 'rootDirectory' => $runtime['providerRootDirectory'], + 'type' => 'branch', + 'reference' => 'main', + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupFunction($functionId); + } + + public function testCreateFunctionAndDeploymentFromTemplateCommit() + { + $starterTemplate = $this->getTemplate('starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); + + // Get latest commit using helper function + $latestCommit = $this->helperGetLatestCommit( + $starterTemplate['body']['providerOwner'], + $starterTemplate['body']['providerRepositoryId'] + ); + $this->assertNotNull($latestCommit); + + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + return $runtime['name'] === 'node-22'; + }))[0]; + + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + + $function = $this->createFunction( + [ + 'functionId' => ID::unique(), + 'name' => $starterTemplate['body']['name'] . ' - Commit Test', + 'runtime' => 'node-22', + 'execute' => $starterTemplate['body']['permissions'], + 'entrypoint' => $runtime['entrypoint'], + 'events' => $starterTemplate['body']['events'], + 'schedule' => $starterTemplate['body']['cron'], + 'timeout' => $starterTemplate['body']['timeout'], + 'commands' => $runtime['commands'], + 'scopes' => $starterTemplate['body']['scopes'], + ] + ); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $function['body']['$id'] ?? ''; + + // Deploy using commit + $deployment = $this->createTemplateDeployment( + $functionId, + [ + 'resourceId' => ID::unique(), + 'activate' => true, + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'owner' => $starterTemplate['body']['providerOwner'], + 'rootDirectory' => $runtime['providerRootDirectory'], + 'type' => 'commit', + 'reference' => $latestCommit, + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupFunction($functionId); } /** diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 93c55b82b7..7eb5d9699c 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -329,9 +329,33 @@ trait SitesBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); + return $template; } + protected function helperGetLatestCommit(string $owner, string $repository): ?string + { + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + return $commitData['sha']; + } + } + + return null; + } + protected function deleteSite(string $siteId): mixed { $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 8591514796..8c03ec7649 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1567,7 +1567,157 @@ class SitesCustomServerTest extends Scope 'repository' => $template['providerRepositoryId'], 'owner' => $template['providerOwner'], 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], - 'version' => $template['providerVersion'], + 'type' => 'tag', + 'reference' => $template['providerVersion'], + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals(0, $deployment['body']['sourceSize']); + $this->assertEquals(0, $deployment['body']['buildSize']); + $this->assertEquals(0, $deployment['body']['totalSize']); + + $this->assertEventually(function () use ($siteId) { + $site = $this->getSite($siteId); + $this->assertNotEmpty($site['body']['deploymentId']); + }, 50000, 500); + + $domain = $this->setupSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("Hello, Astronaut!", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/about'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("About Me", $response['body']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupSite($siteId); + } + + public function testCreateSiteFromTemplateBranch() + { + $template = $this->getTemplate('playground-for-astro'); + $this->assertEquals(200, $template['headers']['status-code']); + + $template = $template['body']; + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro Blog - Branch Test', + 'framework' => $template['frameworks'][0]['key'], + 'adapter' => $template['frameworks'][0]['adapter'], + 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], + 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], + 'buildCommand' => $template['frameworks'][0]['buildCommand'], + 'installCommand' => $template['frameworks'][0]['installCommand'], + 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], + ]); + + $this->assertNotEmpty($siteId); + + // Deploy using branch + $deployment = $this->createTemplateDeployment($siteId, [ + 'repository' => $template['providerRepositoryId'], + 'owner' => $template['providerOwner'], + 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], + 'type' => 'branch', + 'reference' => 'main', + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals(0, $deployment['body']['sourceSize']); + $this->assertEquals(0, $deployment['body']['buildSize']); + $this->assertEquals(0, $deployment['body']['totalSize']); + + $this->assertEventually(function () use ($siteId) { + $site = $this->getSite($siteId); + $this->assertNotEmpty($site['body']['deploymentId']); + }, 50000, 500); + + $domain = $this->setupSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("Hello, Astronaut!", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/about'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("About Me", $response['body']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupSite($siteId); + } + + public function testCreateSiteFromTemplateCommit() + { + $template = $this->getTemplate('playground-for-astro'); + $this->assertEquals(200, $template['headers']['status-code']); + + // Get latest commit using helper function + $latestCommit = $this->helperGetLatestCommit( + $template['body']['providerOwner'], + $template['body']['providerRepositoryId'] + ); + $this->assertNotNull($latestCommit); + + $template = $template['body']; + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro Blog - Commit Test', + 'framework' => $template['frameworks'][0]['key'], + 'adapter' => $template['frameworks'][0]['adapter'], + 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], + 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], + 'buildCommand' => $template['frameworks'][0]['buildCommand'], + 'installCommand' => $template['frameworks'][0]['installCommand'], + 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], + ]); + + $this->assertNotEmpty($siteId); + + // Deploy using commit + $deployment = $this->createTemplateDeployment($siteId, [ + 'repository' => $template['providerRepositoryId'], + 'owner' => $template['providerOwner'], + 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], + 'type' => 'commit', + 'reference' => $latestCommit, 'activate' => true ]);