Resolve merge conflicts

This commit is contained in:
Khushboo Verma 2025-06-13 18:06:47 +05:30
commit 55be41f633
27 changed files with 358 additions and 344 deletions

View file

@ -251,7 +251,7 @@ CLI::setResource('logError', function (Registry $register) {
};
}, ['register']);
CLI::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
CLI::setResource('executor', fn () => new Executor());
CLI::setResource('telemetry', fn () => new NoTelemetry());

View file

@ -59,7 +59,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '16.1.0',
'version' => '17.0.1',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@ -77,7 +77,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '10.1.0',
'version' => '10.1.1',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@ -217,7 +217,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '7.0.0',
'version' => '8.0.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@ -231,6 +231,11 @@ return [
'gitRepoName' => 'sdk-for-cli',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'exclude' => [
'services' => [
['name' => 'assistant'],
],
],
],
],
],
@ -411,7 +416,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '10.0.0',
'version' => '10.1.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,

View file

@ -4490,6 +4490,29 @@
}
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
},
{
"name": "createDocuments",
"auth": {
"Key": []
},
"parameters": [
"databaseId",
"collectionId",
"documents"
],
"required": [
"databaseId",
"collectionId",
"documents"
],
"responses": [
{
"code": 201,
"model": "#\/components\/schemas\/documentList"
}
],
"description": "Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
}
],
"auth": {
@ -6595,7 +6618,8 @@
"png",
"webp",
"heic",
"avif"
"avif",
"gif"
],
"x-enum-name": "ImageFormat",
"x-enum-keys": [],

View file

@ -9767,6 +9767,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -9792,7 +9793,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -10395,6 +10397,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -10420,7 +10423,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -25446,12 +25450,33 @@
"Temporary Redirect 307",
"Permanent Redirect 308"
]
},
"resourceId": {
"type": "string",
"description": "ID of parent resource.",
"x-example": "<RESOURCE_ID>"
},
"resourceType": {
"type": "string",
"description": "Type of parent resource.",
"x-example": "site",
"enum": [
"site",
"function"
],
"x-enum-name": "ProxyResourceType",
"x-enum-keys": [
"Site",
"Function"
]
}
},
"required": [
"domain",
"url",
"statusCode"
"statusCode",
"resourceId",
"resourceType"
]
}
}
@ -25936,6 +25961,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -25961,7 +25987,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -26580,6 +26607,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -26605,7 +26633,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -29364,7 +29393,8 @@
"png",
"webp",
"heic",
"avif"
"avif",
"gif"
],
"x-enum-name": "ImageFormat",
"x-enum-keys": [],
@ -34846,6 +34876,17 @@
"default": ""
},
"in": "query"
},
{
"name": "providerReference",
"description": "Git reference (branch, tag, commit) to get contents from",
"required": false,
"schema": {
"type": "string",
"x-example": "<PROVIDER_REFERENCE>",
"default": ""
},
"in": "query"
}
]
}

View file

@ -8844,6 +8844,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -8869,7 +8870,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -9244,6 +9246,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -9269,7 +9272,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -17490,6 +17494,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -17515,7 +17520,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -17906,6 +17912,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -17931,7 +17938,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -20646,7 +20654,8 @@
"png",
"webp",
"heic",
"avif"
"avif",
"gif"
],
"x-enum-name": "ImageFormat",
"x-enum-keys": [],

View file

@ -4636,6 +4636,29 @@
}
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
},
{
"name": "createDocuments",
"auth": {
"Key": []
},
"parameters": [
"databaseId",
"collectionId",
"documents"
],
"required": [
"databaseId",
"collectionId",
"documents"
],
"responses": [
{
"code": 201,
"model": "#\/definitions\/documentList"
}
],
"description": "Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
}
],
"auth": {
@ -6729,7 +6752,8 @@
"png",
"webp",
"heic",
"avif"
"avif",
"gif"
],
"x-enum-name": "ImageFormat",
"x-enum-keys": [],

View file

@ -9835,6 +9835,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -9860,7 +9861,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -10466,6 +10468,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -10491,7 +10494,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -25696,12 +25700,35 @@
"Temporary Redirect 307",
"Permanent Redirect 308"
]
},
"resourceId": {
"type": "string",
"description": "ID of parent resource.",
"default": null,
"x-example": "<RESOURCE_ID>"
},
"resourceType": {
"type": "string",
"description": "Type of parent resource.",
"default": null,
"x-example": "site",
"enum": [
"site",
"function"
],
"x-enum-name": "ProxyResourceType",
"x-enum-keys": [
"Site",
"Function"
]
}
},
"required": [
"domain",
"url",
"statusCode"
"statusCode",
"resourceId",
"resourceType"
]
}
}
@ -26203,6 +26230,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -26228,7 +26256,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -26850,6 +26879,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -26875,7 +26905,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -29604,7 +29635,8 @@
"png",
"webp",
"heic",
"avif"
"avif",
"gif"
],
"x-enum-name": "ImageFormat",
"x-enum-keys": [],
@ -35063,6 +35095,15 @@
"x-example": "<PROVIDER_ROOT_DIRECTORY>",
"default": "",
"in": "query"
},
{
"name": "providerReference",
"description": "Git reference (branch, tag, commit) to get contents from",
"required": false,
"type": "string",
"x-example": "<PROVIDER_REFERENCE>",
"default": "",
"in": "query"
}
]
}

View file

@ -8927,6 +8927,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -8952,7 +8953,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -9340,6 +9342,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -9365,7 +9368,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -17792,6 +17796,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -17817,7 +17822,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -18221,6 +18227,7 @@
"dart-3.1",
"dart-3.3",
"dart-3.5",
"dart-3.8",
"dotnet-6.0",
"dotnet-7.0",
"dotnet-8.0",
@ -18246,7 +18253,8 @@
"static-1",
"flutter-3.24",
"flutter-3.27",
"flutter-3.29"
"flutter-3.29",
"flutter-3.32"
],
"x-enum-name": null,
"x-enum-keys": []
@ -20935,7 +20943,8 @@
"png",
"webp",
"heic",
"avif"
"avif",
"gif"
],
"x-enum-name": "ImageFormat",
"x-enum-keys": [],

View file

@ -845,15 +845,18 @@ App::get('/v1/health/storage')
$checkStart = \microtime(true);
foreach ($devices as $device) {
if (!$device->write($device->getPath('health.txt'), 'test', 'text/plain')) {
$uniqueFileName = \uniqid('health', true);
$filePath = $device->getPath($uniqueFileName);
if (!$device->write($filePath, 'test', 'text/plain')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot());
}
if ($device->read($device->getPath('health.txt')) !== 'test') {
if ($device->read($filePath) !== 'test') {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot());
}
if (!$device->delete($device->getPath('health.txt'))) {
if (!$device->delete($filePath)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot());
}
}

View file

@ -605,11 +605,12 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->param('providerRootDirectory', '', new Text(256, 0), 'Path to get contents of nested directory', true)
->param('providerReference', '', new Text(256, 0), 'Git reference (branch, tag, commit) to get contents from', true)
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, string $providerReference, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
@ -631,7 +632,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory);
$contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference);
$vcsContents = [];
foreach ($contents as $content) {

View file

@ -880,7 +880,7 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key
return Key::decode($project, $key);
}, ['request', 'project']);
App::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
App::setResource('executor', fn () => new Executor());
App::setResource('resourceToken', function ($project, $dbForProject, $request) {
$tokenJWT = $request->getParam('token');

View file

@ -402,7 +402,7 @@ Server::setResource('logError', function (Registry $register, Document $project)
};
}, ['register', 'project']);
Server::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
Server::setResource('executor', fn () => new Executor());
$pools = $register->get('pools');
$platform = new Appwrite();

24
composer.lock generated
View file

@ -4584,16 +4584,16 @@
},
{
"name": "utopia-php/vcs",
"version": "0.10.4",
"version": "0.10.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "f635b368909eb3c3fe57344fe43525e74e8fdc03"
"reference": "b358439dc387f6097019eb83ebb9fc258fe9da05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/f635b368909eb3c3fe57344fe43525e74e8fdc03",
"reference": "f635b368909eb3c3fe57344fe43525e74e8fdc03",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/b358439dc387f6097019eb83ebb9fc258fe9da05",
"reference": "b358439dc387f6097019eb83ebb9fc258fe9da05",
"shasum": ""
},
"require": {
@ -4627,9 +4627,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/0.10.4"
"source": "https://github.com/utopia-php/vcs/tree/0.10.5"
},
"time": "2025-06-02T09:18:36+00:00"
"time": "2025-06-10T15:01:16+00:00"
},
{
"name": "utopia-php/websocket",
@ -4807,16 +4807,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.41.1",
"version": "0.41.4",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "6d9318abf4542a757c87abf056557d6afa1dc06b"
"reference": "07804269131f411576aac60c795a5ebc3afaa48a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6d9318abf4542a757c87abf056557d6afa1dc06b",
"reference": "6d9318abf4542a757c87abf056557d6afa1dc06b",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/07804269131f411576aac60c795a5ebc3afaa48a",
"reference": "07804269131f411576aac60c795a5ebc3afaa48a",
"shasum": ""
},
"require": {
@ -4852,9 +4852,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.1"
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.4"
},
"time": "2025-06-01T04:20:04+00:00"
"time": "2025-06-10T08:28:11+00:00"
},
{
"name": "doctrine/annotations",

View file

@ -213,7 +213,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:6.0.32
image: appwrite/console:6.0.41
restart: unless-stopped
networks:
- appwrite

View file

@ -105,9 +105,11 @@ class Get extends Action
$response
->setContentType('application/gzip')
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');
$size = $device->getFileSize($path);
$rangeHeader = $request->getHeader('range');

View file

@ -877,6 +877,10 @@ class Builds extends Action
}
}
$deployment->setAttribute('buildLogs', $logs);
$this->afterBuildSuccess($dbForProject, $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$queueForRealtime
@ -1313,6 +1317,19 @@ class Builds extends Action
->trigger();
}
/**
* Hook to run after build success
*
* @param Database $dbForProject
* @param Document $deployment
* @return void
*/
protected function afterBuildSuccess(Database $dbForProject, Document &$deployment): void
{
assert($dbForProject instanceof Database);
assert($deployment instanceof Document);
}
protected function getRuntime(Document $resource, string $version): array
{
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);

View file

@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@ -65,15 +66,18 @@ class Create extends Action
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('url', null, new URL(), 'Target URL of redirection')
->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection')
->param('resourceId', '', new UID(), 'ID of parent resource.')
->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $domain, string $url, int $statusCode, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
{
$deniedDomains = [
'localhost',
@ -116,6 +120,15 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$collection = match ($resourceType) {
'site' => 'sites',
'function' => 'functions'
};
$resource = $dbForProject->getDocument($collection, $resourceId);
if ($resource->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
@ -164,6 +177,9 @@ class Create extends Action
'trigger' => 'manual',
'redirectUrl' => $url,
'redirectStatusCode' => $statusCode,
'deploymentResourceType' => $resourceType,
'deploymentResourceId' => $resource->getId(),
'deploymentResourceInternalId' => $resource->getInternalId(),
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,

View file

@ -99,12 +99,14 @@ class Get extends Action
}
if (!$device->exists($path)) {
throw new Exception(Exception::BUILD_NOT_FOUND);
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$response
->setContentType('application/gzip')
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');

View file

@ -4,9 +4,11 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use DateInterval;
use DateTime;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\DateTime as DatabaseDateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Platform\Action;
@ -58,29 +60,35 @@ class Maintenance extends Action
Console::info('Setting loop start time to ' . $next->format("Y-m-d H:i:s.v") . '. Delaying for ' . $delay . ' seconds.');
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
$time = DateTime::now();
$time = DatabaseDateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
// Iterate through project only if it was accessed in last 30 days
$dateInterval = DateInterval::createFromDateString('30 days');
$before30days = (new DateTime())->sub($dateInterval);
$dbForPlatform->foreach(
'projects',
function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$queueForDeletes
->setType(DELETE_TYPE_MAINTENANCE)
->setProject($project)
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
->trigger();
},
[
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::limit(100),
Query::greaterThanEqual('accessedAt', DatabaseDateTime::format($before30days)),
Query::orderAsc('teamInternalId'),
]
);
$queueForDeletes
->setType(DELETE_TYPE_MAINTENANCE)
->setProject($console)
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
->trigger();
$this->notifyDeleteConnections($queueForDeletes);
@ -94,13 +102,13 @@ class Maintenance extends Action
{
$queueForDeletes
->setType(DELETE_TYPE_REALTIME)
->setDatetime(DateTime::addSeconds(new \DateTime(), -60))
->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -60))
->trigger();
}
private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void
{
$time = DateTime::now();
$time = DatabaseDateTime::now();
$certificates = $dbForPlatform->find('certificates', [
Query::lessThan('attempts', 5), // Maximum 5 attempts
@ -129,7 +137,7 @@ class Maintenance extends Action
{
$queueForDeletes
->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}
@ -137,7 +145,7 @@ class Maintenance extends Action
{
$queueForDeletes
->setType(DELETE_TYPE_SCHEDULES)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}
}

View file

@ -251,7 +251,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setDiscord(APP_SOCIAL_DISCORD_CHANNEL, APP_SOCIAL_DISCORD)
->setDefaultHeaders([
'X-Appwrite-Response-Format' => '1.7.0',
]);
])
->setExclude($language['exclude'] ?? []);
// Make sure we have a clean slate.
// Otherwise, all files in this dir will be pushed,

View file

@ -12,13 +12,13 @@ use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Queue\Result\Commit;
use Utopia\Queue\Result\NoCommit;
use Utopia\System\System;
class Audits extends Action
{
protected const BATCH_SIZE_DEVELOPMENT = 1; // smaller batch size for development
protected const BATCH_SIZE_PRODUCTION = 5_000;
protected const BATCH_AGGREGATION_INTERVAL = 60; // in seconds
protected const int BATCH_AGGREGATION_INTERVAL = 60; // in seconds
private int $lastTriggeredTime = 0;
@ -27,9 +27,7 @@ class Audits extends Action
protected function getBatchSize(): int
{
return System::getEnv('_APP_ENV', 'development') === 'development'
? self::BATCH_SIZE_DEVELOPMENT
: self::BATCH_SIZE_PRODUCTION;
return intval(System::getEnv('_APP_QUEUE_PREFETCH_COUNT', 1));
}
public static function getName(): string
@ -57,13 +55,13 @@ class Audits extends Action
* @param Message $message
* @param callable $getProjectDB
* @param Document $project
* @return void
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, callable $getProjectDB, Document $project): void
public function action(Message $message, callable $getProjectDB, Document $project): Commit|NoCommit
{
$payload = $message->getPayload() ?? [];
@ -123,29 +121,32 @@ class Audits extends Action
// Check if we should process the batch by checking both for the batch size and the elapsed time
$batchSize = $this->getBatchSize();
$shouldProcessBatch = \count($this->logs) >= $batchSize;
if (!$shouldProcessBatch && \count($this->logs) > 0) {
$logCount = array_reduce($this->logs, fn (int $current, $logs) => $current + count($logs['logs']), 0);
$shouldProcessBatch = $logCount >= $batchSize;
if (!$shouldProcessBatch && $logCount > 0) {
$shouldProcessBatch = (\time() - $this->lastTriggeredTime) >= self::BATCH_AGGREGATION_INTERVAL;
}
if ($shouldProcessBatch) {
try {
foreach ($this->logs as $internalId => $projectLogs) {
$dbForProject = $getProjectDB($projectLogs['project']);
Console::log('Processing batch with ' . count($projectLogs['logs']) . ' events');
$audit = new Audit($dbForProject);
$audit->logBatch($projectLogs['logs']);
Console::success('Audit logs processed successfully');
unset($this->logs[$internalId]);
}
} catch (Throwable $e) {
Console::error('Error processing audit logs: ' . $e->getMessage());
} finally {
$this->lastTriggeredTime = time();
}
if (!$shouldProcessBatch) {
return new NoCommit();
}
try {
foreach ($this->logs as $internalId => $projectLogs) {
$dbForProject = $getProjectDB($projectLogs['project']);
Console::log('Processing batch with ' . count($projectLogs['logs']) . ' events');
$audit = new Audit($dbForProject);
$audit->logBatch($projectLogs['logs']);
Console::success('Audit logs processed successfully');
unset($this->logs[$internalId]);
}
} catch (Throwable $e) {
Console::error('Error processing audit logs: ' . $e->getMessage());
}
$this->lastTriggeredTime = time();
return new Commit();
}
}

View file

@ -1,206 +0,0 @@
<?php
namespace Appwrite\Platform\Workers;
use Appwrite\Extend\Exception;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Registry\Registry;
use Utopia\System\System;
/**
* TODO remove later
*/
class StatsUsageDump extends Action
{
public const METRIC_COLLECTION_LEVEL_STORAGE = 4;
public const METRIC_DATABASE_LEVEL_STORAGE = 3;
public const METRIC_PROJECT_LEVEL_STORAGE = 2;
protected array $stats = [];
protected Registry $register;
/**
* Metrics to skip writing to logsDB
* As these metrics are calculated separately
* by logs DB
* @var array
*/
protected array $skipBaseMetrics = [
METRIC_DATABASES => true,
METRIC_BUCKETS => true,
METRIC_USERS => true,
METRIC_FUNCTIONS => true,
METRIC_TEAMS => true,
METRIC_MESSAGES => true,
METRIC_MAU => true,
METRIC_WEBHOOKS => true,
METRIC_PLATFORMS => true,
METRIC_PROVIDERS => true,
METRIC_TOPICS => true,
METRIC_KEYS => true,
METRIC_FILES => true,
METRIC_FILES_STORAGE => true,
METRIC_DEPLOYMENTS_STORAGE => true,
METRIC_BUILDS_STORAGE => true,
METRIC_DEPLOYMENTS => true,
METRIC_BUILDS => true,
METRIC_COLLECTIONS => true,
METRIC_DOCUMENTS => true,
METRIC_DATABASES_STORAGE => true,
];
/**
* Skip metrics associated with parent IDs
* these need to be checked individually with `str_ends_with`
*/
protected array $skipParentIdMetrics = [
'.files',
'.files.storage',
'.collections',
'.documents',
'.deployments',
'.deployments.storage',
'.builds',
'.builds.storage',
'.databases.storage'
];
/**
* @var callable(Document): Database
*/
protected $getLogsDB;
protected array $periods = [
'1h' => 'Y-m-d H:00',
'1d' => 'Y-m-d 00:00',
'inf' => '0000-00-00 00:00'
];
public static function getName(): string
{
return 'stats-usage-dump';
}
/**
* @throws \Exception
*/
public function __construct()
{
$this
->inject('message')
->inject('getProjectDB')
->inject('getLogsDB')
->inject('register')
->callback($this->action(...));
}
/**
* @param Message $message
* @param callable $getProjectDB
* @param callable $getLogsDB
* @param Registry $register
* @return void
* @throws Exception
* @throws \Throwable
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, callable $getProjectDB, callable $getLogsDB, Registry $register): void
{
$this->getLogsDB = $getLogsDB;
$this->register = $register;
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
}
foreach ($payload['stats'] ?? [] as $stats) {
$project = new Document($stats['project'] ?? []);
$numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0;
$receivedAt = $stats['receivedAt'] ?? null;
if ($numberOfKeys === 0) {
continue;
}
Console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys);
try {
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
foreach ($stats['keys'] ?? [] as $key => $value) {
if ($value == 0) {
continue;
}
if (str_contains($key, METRIC_DATABASES_STORAGE)) {
continue;
}
foreach ($this->periods as $period => $format) {
$time = null;
if ($period !== 'inf') {
$time = !empty($receivedAt) ? (new \DateTime($receivedAt))->format($format) : date($format, time());
}
$id = \md5("{$time}_{$period}_{$key}");
$document = new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $key,
'value' => $value,
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$documentClone = clone $document;
$dbForProject->createOrUpdateDocumentsWithIncrease(
'stats',
'value',
[$document]
);
$this->writeToLogsDB($project, $documentClone);
}
}
} catch (\Exception $e) {
Console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage());
}
}
}
protected function writeToLogsDB(Document $project, Document $document): void
{
if (System::getEnv('_APP_STATS_USAGE_DUAL_WRITING', 'disabled') === 'disabled') {
Console::log('Dual Writing is disabled. Skipping...');
return;
}
if (array_key_exists($document->getAttribute('metric'), $this->skipBaseMetrics)) {
return;
}
foreach ($this->skipParentIdMetrics as $skipMetric) {
if (str_ends_with($document->getAttribute('metric'), $skipMetric)) {
return;
}
}
$dbForLogs = ($this->getLogsDB)($project);
try {
$dbForLogs->createOrUpdateDocumentsWithIncrease(
'stats',
'value',
[$document]
);
Console::success('Usage logs pushed to Logs DB');
} catch (\Throwable $th) {
Console::error($th->getMessage());
}
}
}

View file

@ -113,6 +113,16 @@ abstract class Format
protected function getEnumName(string $service, string $method, string $param): ?string
{
switch ($service) {
case 'proxy':
switch ($method) {
case 'createRedirectRule':
switch ($param) {
case 'resourceType':
return 'ProxyResourceType';
}
break;
}
break;
case 'console':
switch ($method) {
case 'getResource':
@ -441,7 +451,13 @@ abstract class Format
case 'proxy':
switch ($method) {
case 'createRedirectRule':
return ['Moved Permanently 301', 'Found 302', 'Temporary Redirect 307', 'Permanent Redirect 308'];
switch ($param) {
case 'statusCode':
return ['Moved Permanently 301', 'Found 302', 'Temporary Redirect 307', 'Permanent Redirect 308'];
case 'resourceType':
return ['Site', 'Function'];
}
break;
}
break;
case 'functions':

View file

@ -19,21 +19,14 @@ class Executor
public const METHOD_CONNECT = 'CONNECT';
public const METHOD_TRACE = 'TRACE';
private bool $selfSigned = false;
/**
* @var callable(string, string): string $endpoint
*/
private $endpointSelector;
protected bool $selfSigned = false;
protected string $endpoint;
protected array $headers;
/**
* @param callable(string, string): string $endpointSelector
*/
public function __construct(callable $endpointSelector)
public function __construct()
{
$this->endpointSelector = $endpointSelector;
$this->endpoint = System::getEnv('_APP_EXECUTOR_HOST', '');
$this->headers = [
'content-type' => 'application/json',
'authorization' => 'Bearer ' . System::getEnv('_APP_EXECUTOR_SECRET', ''),
@ -97,8 +90,8 @@ class Executor
'outputDirectory' => $outputDirectory
];
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$status = $response['headers']['status-code'];
if ($status >= 400) {
@ -128,8 +121,7 @@ class Executor
'timeout' => $timeout
];
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$this->call($endpoint, self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback);
$this->call($this->endpoint, self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback);
}
/**
@ -145,8 +137,7 @@ class Executor
$runtimeId = "$projectId-$deploymentId" . $suffix;
$route = "/runtimes/$runtimeId";
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$response = $this->call($endpoint, self::METHOD_DELETE, $route, [
$response = $this->call($this->endpoint, self::METHOD_DELETE, $route, [
'x-opr-addressing-method' => 'broadcast'
], [], true, 30);
@ -239,8 +230,7 @@ class Executor
$requestTimeout = $timeout + 15;
}
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout);
$response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout);
$status = $response['headers']['status-code'];
if ($status >= 400) {
@ -274,8 +264,7 @@ class Executor
'timeout' => $timeout
];
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$status = $response['headers']['status-code'];
if ($status >= 400) {
@ -465,9 +454,4 @@ class Executor
return $output;
}
private function selectEndpoint(string $projectId, string $deploymentId): string
{
return call_user_func($this->endpointSelector, $projectId, $deploymentId);
}
}

View file

@ -68,7 +68,7 @@ trait ProxyBase
return $rule;
}
protected function createRedirectRule(string $domain, string $url, int $statusCode): mixed
protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
'content-type' => 'application/json',
@ -77,6 +77,8 @@ trait ProxyBase
'domain' => $domain,
'url' => $url,
'statusCode' => $statusCode,
'resourceType' => $resourceType,
'resourceId' => $resourceId,
]);
return $rule;
@ -115,9 +117,9 @@ trait ProxyBase
return $rule['body']['$id'];
}
protected function setupRedirectRule(string $domain, string $url, int $statusCode): string
protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string
{
$rule = $this->createRedirectRule($domain, $url, $statusCode);
$rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));

View file

@ -131,7 +131,9 @@ class ProxyCustomServerTest extends Scope
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(404, $response['headers']['status-code']);
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301);
$siteId = $this->setupSite()['siteId'];
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
@ -147,7 +149,7 @@ class ProxyCustomServerTest extends Scope
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
$domain = \uniqid() . '-redirect-307.custom.localhost';
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307);
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
$this->assertNotEmpty($ruleId);
$proxyClient = new Client();
@ -158,6 +160,18 @@ class ProxyCustomServerTest extends Scope
$this->assertEquals(307, $response['headers']['status-code']);
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
$rules = $this->listRules([
'queries' => [
Query::equal('type', ['redirect'])->toString(),
Query::equal('trigger', ['manual'])->toString(),
Query::equal('deploymentResourceType', ['site'])->toString(),
Query::equal('deploymentResourceId', [$siteId])->toString(),
],
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
$this->cleanupSite($siteId);
$this->cleanupRule($ruleId);
}

View file

@ -477,7 +477,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($domain);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
@ -856,7 +856,7 @@ class SitesCustomServerTest extends Scope
$deployment = $this->createDeployment($siteId, [
'siteId' => $siteId,
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => true,
]);
@ -874,7 +874,7 @@ class SitesCustomServerTest extends Scope
}, 50000, 500);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'false'
]);
@ -917,7 +917,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotNull($siteId);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'false'
]);
@ -969,7 +969,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotNull($siteId);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'false'
]);
@ -1014,7 +1014,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotNull($siteId);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'false'
]);
@ -1022,7 +1022,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'false'
]);
@ -1193,7 +1193,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotNull($siteId);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'false'
]);
@ -1318,7 +1318,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotNull($siteId);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'false'
]);
@ -1818,7 +1818,7 @@ class SitesCustomServerTest extends Scope
]);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => true
]);
@ -2435,7 +2435,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'true'
]);
@ -2477,7 +2477,7 @@ class SitesCustomServerTest extends Scope
// test canceled deployment error page
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'true'
]);
$deploymentId = $deployment['body']['$id'] ?? '';
@ -2517,7 +2517,7 @@ class SitesCustomServerTest extends Scope
$this->assertStringContainsString('View deployments', $response['body']);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('astro'),
'code' => $this->packageSite('static-single-file'),
'activate' => 'true'
]);
@ -2606,7 +2606,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => true
]);
$this->assertEquals(202, $deployment['headers']['status-code']);
@ -2635,7 +2635,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static'),
'code' => $this->packageSite('static-single-file'),
'activate' => true
]);
$this->assertEquals(202, $deployment['headers']['status-code']);