From 03ccca2c359b8996a2c57c6f74c5ad288bc70ff4 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 13:05:41 +0000 Subject: [PATCH 1/8] Add after create success hook in file creation process --- .../Storage/Http/Buckets/Files/Create.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index b2d9af5a08..ed5c23b6c1 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -385,6 +385,9 @@ class Create extends Action } $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); } + + // Trigger after create success hook + $this->afterCreateSuccess($file); } else { if ($file->isEmpty()) { $doc = new Document([ @@ -448,4 +451,17 @@ class Create extends Action ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($file, Response::MODEL_FILE); } + + /** + * Hook to run after file is created successfully + * + * @param Document $file + * @return void + */ + protected function afterCreateSuccess(Document $file) + { + if (!($file instanceof Document)) { + throw new Exception('file must be an instance of document'); + } + } } From eecfba2a7292e514afbf333b98f68f9b1ba9fa3f Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:38:52 +0000 Subject: [PATCH 2/8] feat: graceful workers --- app/worker.php | 5 ----- composer.json | 4 ++-- composer.lock | 32 ++++++++++++++++---------------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/app/worker.php b/app/worker.php index 7868861cf4..094d2b993e 100644 --- a/app/worker.php +++ b/app/worker.php @@ -544,9 +544,4 @@ $worker Console::error('[Error] Line: ' . $error->getLine()); }); -$worker->workerStart() - ->action(function () use ($workerName) { - Console::info("Worker $workerName started"); - }); - $worker->start(); diff --git a/composer.json b/composer.json index d45d723430..f5fbc80170 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.11.*", + "utopia-php/queue": "0.15.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "0.18.*", "utopia-php/swoole": "0.8.*", @@ -115,4 +115,4 @@ "tbachert/spi": true } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 73abeb57f0..bb8ea25b67 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "375a062e8675e7e6938c1d8cc7b61ecf", + "content-hash": "aecb99247e6e5090561afd7134c247f9", "packages": [ { "name": "adhocore/jwt", @@ -4700,16 +4700,16 @@ }, { "name": "utopia-php/platform", - "version": "0.7.13", + "version": "0.7.14", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "77a863a920122e2c6a6bc6ee5548d366a3f4c6c7" + "reference": "9f18ce63f1425ae2dae57468200e4a5d1239d57b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/77a863a920122e2c6a6bc6ee5548d366a3f4c6c7", - "reference": "77a863a920122e2c6a6bc6ee5548d366a3f4c6c7", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/9f18ce63f1425ae2dae57468200e4a5d1239d57b", + "reference": "9f18ce63f1425ae2dae57468200e4a5d1239d57b", "shasum": "" }, "require": { @@ -4718,7 +4718,7 @@ "php": ">=8.0", "utopia-php/cli": "0.15.*", "utopia-php/framework": "0.33.*", - "utopia-php/queue": "0.11.*" + "utopia-php/queue": "0.15.*" }, "require-dev": { "laravel/pint": "1.*", @@ -4745,9 +4745,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.13" + "source": "https://github.com/utopia-php/platform/tree/0.7.14" }, - "time": "2025-12-08T10:02:40+00:00" + "time": "2026-01-06T15:39:45+00:00" }, { "name": "utopia-php/pools", @@ -4856,22 +4856,22 @@ }, { "name": "utopia-php/queue", - "version": "0.11.3", + "version": "0.15.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d" + "reference": "6abb268ba7ec00dea4e5201b007776ea1bce9242" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/f3b2623efe87595c9ed907b3efd587e77c622d3d", - "reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/6abb268ba7ec00dea4e5201b007776ea1bce9242", + "reference": "6abb268ba7ec00dea4e5201b007776ea1bce9242", "shasum": "" }, "require": { "php": ">=8.3", "php-amqplib/php-amqplib": "^3.7", - "utopia-php/cli": "0.15.*", + "utopia-php/console": "0.0.*", "utopia-php/fetch": "0.5.*", "utopia-php/framework": "0.33.*", "utopia-php/pools": "0.8.*", @@ -4916,9 +4916,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.11.3" + "source": "https://github.com/utopia-php/queue/tree/0.15.0" }, - "time": "2025-12-19T10:56:22+00:00" + "time": "2026-01-06T12:41:51+00:00" }, { "name": "utopia-php/registry", @@ -8989,5 +8989,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 4319c1658428681964698933169326de3d8b5a71 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 8 Jan 2026 11:13:16 +0530 Subject: [PATCH 3/8] bump: migrations. --- composer.json | 6 ------ composer.lock | 38 ++++++++++---------------------------- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/composer.json b/composer.json index f5fbc80170..131455d206 100644 --- a/composer.json +++ b/composer.json @@ -100,12 +100,6 @@ "provide": { "ext-phpiredis": "*" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/utopia-php/migration.git" - } - ], "config": { "platform": { "php": "8.3" diff --git a/composer.lock b/composer.lock index bb8ea25b67..c047bf2e03 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aecb99247e6e5090561afd7134c247f9", + "content-hash": "ff3172688b600aa3c560131c9d9c5588", "packages": [ { "name": "adhocore/jwt", @@ -4516,16 +4516,16 @@ }, { "name": "utopia-php/migration", - "version": "1.3.12", + "version": "1.3.13", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "1b8d5519c50630e4c0b6a79be615b70d5f23d2e4" + "reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/1b8d5519c50630e4c0b6a79be615b70d5f23d2e4", - "reference": "1b8d5519c50630e4c0b6a79be615b70d5f23d2e4", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5e3f5e970e62e8f7db97b5b90baae2af800a715", + "reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715", "shasum": "" }, "require": { @@ -4551,25 +4551,7 @@ "Utopia\\Migration\\": "src/Migration" } }, - "autoload-dev": { - "psr-4": { - "Utopia\\Tests\\": "tests/Migration" - } - }, - "scripts": { - "test": [ - "./vendor/bin/phpunit" - ], - "lint": [ - "./vendor/bin/pint --test" - ], - "format": [ - "./vendor/bin/pint" - ], - "check": [ - "./vendor/bin/phpstan analyse --level 3 src tests --memory-limit 2G" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -4582,10 +4564,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/migration/tree/1.3.12", - "issues": "https://github.com/utopia-php/migration/issues" + "issues": "https://github.com/utopia-php/migration/issues", + "source": "https://github.com/utopia-php/migration/tree/1.3.13" }, - "time": "2026-01-07T06:07:33+00:00" + "time": "2026-01-07T14:48:05+00:00" }, { "name": "utopia-php/mongo", @@ -8989,5 +8971,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } From d71b289025ee62a83c927cb7dd89be340a0e9a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 8 Jan 2026 16:51:04 +0100 Subject: [PATCH 4/8] Implement screenshot worker --- Dockerfile | 1 + app/controllers/api/health.php | 7 +- app/init/resources.php | 4 + app/worker.php | 5 + bin/worker-screenshots | 3 + docker-compose.yml | 61 +++- src/Appwrite/Event/Event.php | 3 + src/Appwrite/Event/Screenshot.php | 50 +++ .../Modules/Functions/Services/Workers.php | 2 + .../Modules/Functions/Workers/Builds.php | 194 +----------- .../Modules/Functions/Workers/Screenshots.php | 299 ++++++++++++++++++ 11 files changed, 443 insertions(+), 186 deletions(-) create mode 100644 bin/worker-screenshots create mode 100644 src/Appwrite/Event/Screenshot.php create mode 100644 src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php diff --git a/Dockerfile b/Dockerfile index ecc5112cc4..ac8cff0884 100755 --- a/Dockerfile +++ b/Dockerfile @@ -77,6 +77,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/queue-count-success && \ chmod +x /usr/local/bin/worker-audits && \ chmod +x /usr/local/bin/worker-builds && \ + chmod +x /usr/local/bin/worker-screenshots && \ chmod +x /usr/local/bin/worker-certificates && \ chmod +x /usr/local/bin/worker-databases && \ chmod +x /usr/local/bin/worker-deletes && \ diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 97ddf8391c..db3dc3d95b 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -11,6 +11,7 @@ use Appwrite\Event\Func; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; @@ -955,6 +956,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME), System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME), System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME), + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_CLASS_NAME), System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME), System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) ]), 'The name of the queue') @@ -972,6 +974,7 @@ App::get('/v1/health/queue/failed/:name') ->inject('queueForBuilds') ->inject('queueForMessaging') ->inject('queueForMigrations') + ->inject('queueForScreenshots') ->action(function ( string $name, int|string $threshold, @@ -987,7 +990,8 @@ App::get('/v1/health/queue/failed/:name') Certificate $queueForCertificates, Build $queueForBuilds, Messaging $queueForMessaging, - Migration $queueForMigrations + Migration $queueForMigrations, + Screenshot $queueForScreenshots, ) { $threshold = \intval($threshold); @@ -1003,6 +1007,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks, System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates, System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds, + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_CLASS_NAME) => $queueForScreenshots, System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging, System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations, }; diff --git a/app/init/resources.php b/app/init/resources.php index d56354c14b..a3aa3ae47c 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -15,6 +15,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; @@ -129,6 +130,9 @@ App::setResource('queueForMails', function (Publisher $publisher) { App::setResource('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); +App::setResource('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); +}, ['publisher']); App::setResource('queueForDatabase', function (Publisher $publisher) { return new EventDatabase($publisher); }, ['publisher']); diff --git a/app/worker.php b/app/worker.php index 094d2b993e..3720fb85fe 100644 --- a/app/worker.php +++ b/app/worker.php @@ -14,6 +14,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; use Appwrite\Platform\Appwrite; @@ -307,6 +308,10 @@ Server::setResource('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); +Server::setResource('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); +}, ['publisher']); + Server::setResource('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); diff --git a/bin/worker-screenshots b/bin/worker-screenshots new file mode 100644 index 0000000000..0252556075 --- /dev/null +++ b/bin/worker-screenshots @@ -0,0 +1,3 @@ +#!/bin/sh + +exec php /usr/src/code/app/worker.php screenshots "$@" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 14591db926..20c0ad8f79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -466,14 +466,12 @@ services: - appwrite-functions:/storage/functions:rw - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw - - appwrite-uploads:/storage/uploads:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src depends_on: - redis - mariadb environment: - - _APP_BROWSER_HOST - _APP_ENV - _APP_WORKER_PER_CORE - _APP_OPENSSL_KEY_V1 @@ -529,6 +527,65 @@ services: extra_hosts: - "host.docker.internal:host-gateway" + appwrite-worker-screenshots: + entrypoint: worker-screenshots + <<: *x-logging + container_name: appwrite-worker-screenshots + image: appwrite-dev + networks: + - appwrite + volumes: + - appwrite-uploads:/storage/uploads:rw + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + depends_on: + - redis + - mariadb + environment: + # Specific + - _APP_BROWSER_HOST + # Basic + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_LOGGING_CONFIG + # Database + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_DATABASE_SHARED_TABLES + # Storage + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_S3_ENDPOINT + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + extra_hosts: + - "host.docker.internal:host-gateway" + appwrite-worker-certificates: entrypoint: worker-certificates <<: *x-logging diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index c7bb22f715..9805ab7830 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -39,6 +39,9 @@ class Event public const BUILDS_QUEUE_NAME = 'v1-builds'; public const BUILDS_CLASS_NAME = 'BuildsV1'; + public const SCREENSHOTS_QUEUE_NAME = 'v1-screenshots'; + public const SCREENSHOTS_CLASS_NAME = 'ScreenshotsV1'; + public const MESSAGING_QUEUE_NAME = 'v1-messaging'; public const MESSAGING_CLASS_NAME = 'MessagingV1'; diff --git a/src/Appwrite/Event/Screenshot.php b/src/Appwrite/Event/Screenshot.php new file mode 100644 index 0000000000..e2721d1939 --- /dev/null +++ b/src/Appwrite/Event/Screenshot.php @@ -0,0 +1,50 @@ +setQueue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME)) + ->setClass(System::getEnv('_APP_BUILDS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME)); + } + + public function setDeploymentId(string $deploymentId): self + { + $this->deploymentId = $deploymentId; + + return $this; + } + + protected function preparePayload(): array + { + $platform = $this->platform; + if (empty($platform)) { + $platform = Config::getParam('platform', []); + } + + return [ + 'project' => $this->project, + 'deploymentId' => $this->deploymentId, + 'platform' => $platform, + ]; + } + + public function reset(): self + { + $this->deploymentId = ''; + parent::reset(); + + return $this; + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Services/Workers.php b/src/Appwrite/Platform/Modules/Functions/Services/Workers.php index 61256b6bf9..2acb31ad8a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Services/Workers.php +++ b/src/Appwrite/Platform/Modules/Functions/Services/Workers.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Services; use Appwrite\Platform\Modules\Functions\Workers\Builds; +use Appwrite\Platform\Modules\Functions\Workers\Screenshots; use Utopia\Platform\Service; class Workers extends Service @@ -11,5 +12,6 @@ class Workers extends Service { $this->type = Service::TYPE_WORKER; $this->addAction(Builds::getName(), new Builds()); + $this->addAction(Screenshots::getName(), new Screenshots()); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index e38a56bd2b..530203734f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -6,10 +6,9 @@ use Ahc\Jwt\JWT; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; -use Appwrite\Permission; -use Appwrite\Role; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; use Exception; @@ -25,24 +24,19 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; -use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Detector\Detection\Rendering\SSR; use Utopia\Detector\Detection\Rendering\XStatic; use Utopia\Detector\Detector\Rendering; -use Utopia\Fetch\Client as FetchClient; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; -use Utopia\Storage\Compression\Compression; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; use Utopia\VCS\Adapter\Git\GitHub; -use function Swoole\Coroutine\batch; - class Builds extends Action { public static function getName(): string @@ -62,6 +56,7 @@ class Builds extends Action ->inject('project') ->inject('dbForPlatform') ->inject('queueForEvents') + ->inject('queueForScreenshots') ->inject('queueForWebhooks') ->inject('queueForFunctions') ->inject('queueForRealtime') @@ -83,6 +78,7 @@ class Builds extends Action * @param Document $project * @param Database $dbForPlatform * @param Event $queueForEvents + * @param Screenshot $queueForScreenshots * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime @@ -103,6 +99,7 @@ class Builds extends Action Document $project, Database $dbForPlatform, Event $queueForEvents, + Screenshot $queueForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -143,6 +140,7 @@ class Builds extends Action $deviceForFunctions, $deviceForSites, $deviceForFiles, + $queueForScreenshots, $queueForWebhooks, $queueForFunctions, $queueForRealtime, @@ -172,6 +170,7 @@ class Builds extends Action * @param Device $deviceForFunctions * @param Device $deviceForSites * @param Device $deviceForFiles + * @param Screenshot $queueForScreenshots * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime @@ -196,6 +195,7 @@ class Builds extends Action Device $deviceForFunctions, Device $deviceForSites, Device $deviceForFiles, + Screenshot $queueForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -917,182 +917,12 @@ class Builds extends Action /** Screenshot site */ if ($resource->getCollection() === 'sites') { - Console::log('Site screenshot started'); - - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing started. \n"; - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - $queueForRealtime - ->setPayload($deployment->getArrayCopy()) + $queueForScreenshots + ->setDeploymentId($deployment->getId()) + ->setProject($project) ->trigger(); - try { - $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ - Query::equal("projectInternalId", [$project->getSequence()]), - Query::equal("type", ["deployment"]), - Query::equal('deploymentInternalId', [$deployment->getSequence()]), - ])); - - if ($rule->isEmpty()) { - throw new \Exception("Rule for build not found"); - } - - $client = new FetchClient(); - $client->setTimeout(\intval($resource->getAttribute('timeout', '15')) * 1000); - $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); - - $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); - - $configs = [ - 'screenshotLight' => [ - 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light', - 'theme' => 'light' - ], - 'screenshotDark' => [ - 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark', - 'theme' => 'dark' - ], - ]; - - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); - $apiKey = $jwtObj->encode([ - 'hostnameOverride' => true, - 'disabledMetrics' => [ - METRIC_EXECUTIONS, - METRIC_EXECUTIONS_COMPUTE, - METRIC_EXECUTIONS_MB_SECONDS, - METRIC_NETWORK_REQUESTS, - METRIC_NETWORK_INBOUND, - METRIC_NETWORK_OUTBOUND, - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS), - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), - ], - 'bannerDisabled' => true, - 'projectCheckDisabled' => true, - 'previewAuthDisabled' => true, - 'deploymentStatusIgnored' => true - ]); - - $screenshotError = null; - $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $resource, $client, &$screenshotError) { - return function () use ($key, $configs, $apiKey, $resource, $client, &$screenshotError) { - try { - $config = $configs[$key]; - - $config['headers'] = \array_merge($config['headers'] ?? [], [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey - ]); - $config['sleep'] = 3000; - - $frameworks = Config::getParam('frameworks', []); - $framework = $frameworks[$resource->getAttribute('framework', '')] ?? null; - if (!is_null($framework)) { - $config['sleep'] = $framework['screenshotSleep']; - } - - $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); - $fetchResponse = $client->fetch( - url: $browserEndpoint . '/screenshots', - method: 'POST', - body: $config - ); - - if ($fetchResponse->getStatusCode() >= 400) { - throw new \Exception($fetchResponse->getBody()); - } - - $screenshot = $fetchResponse->getBody(); - - return ['key' => $key, 'screenshot' => $screenshot]; - } catch (\Throwable $th) { - $screenshotError = $th->getMessage(); - return; - } - }; - }, \array_keys($configs))); - - if (!\is_null($screenshotError)) { - throw new \Exception($screenshotError); - } - - $mimeType = "image/png"; - - foreach ($screenshots as $data) { - $key = $data['key']; - $screenshot = $data['screenshot']; - - $fileId = ID::unique(); - $fileName = $fileId . '.png'; - $path = $deviceForFiles->getPath($fileName); - $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root - $success = $deviceForFiles->write($path, $screenshot, $mimeType); - - if (!$success) { - throw new \Exception("Screenshot failed to save"); - } - - $teamId = $project->getAttribute('teamId', ''); - $file = new Document([ - '$id' => $fileId, - '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - ], - 'bucketId' => $bucket->getId(), - 'bucketInternalId' => $bucket->getSequence(), - 'name' => $fileName, - 'path' => $path, - 'signature' => $deviceForFiles->getFileHash($path), - 'mimeType' => $mimeType, - 'sizeOriginal' => \strlen($screenshot), - 'sizeActual' => $deviceForFiles->getFileSize($path), - 'algorithm' => Compression::NONE, - 'comment' => '', - 'chunksTotal' => 1, - 'chunksUploaded' => 1, - 'openSSLVersion' => null, - 'openSSLCipher' => null, - 'openSSLTag' => null, - 'openSSLIV' => null, - 'search' => implode(' ', [$fileId, $fileName]), - 'metadata' => ['content_type' => $mimeType], - ]); - - Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file)); - - $deployment->setAttribute($key, $fileId); - } - - $logs = $deployment->getAttribute('buildLogs', ''); - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing finished. \n"; - - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - - $queueForRealtime - ->setPayload($deployment->getArrayCopy()) - ->trigger(); - } catch (\Throwable $th) { - Console::warning("Screenshot failed to generate:"); - Console::warning($th->getMessage()); - Console::warning($th->getTraceAsString()); - - $logs = $deployment->getAttribute('buildLogs', ''); - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"; - - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } - - Console::log('Site screenshot finished'); + Console::log('Site screenshot queued'); } $logs = $deployment->getAttribute('buildLogs', ''); @@ -1171,8 +1001,6 @@ class Builds extends Action 'live' => true, 'deploymentId' => $deployment->getId(), 'deploymentInternalId' => $deployment->getSequence(), - 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), - 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), 'deploymentCreatedAt' => $deployment->getCreatedAt(), ])); $queries = [ diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php new file mode 100644 index 0000000000..8037da4f9f --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -0,0 +1,299 @@ +desc('Screenshots worker') + ->groups(['screenshots']) + ->inject('message') + ->inject('queueForRealtime') + ->inject('dbForPlatform') + ->inject('dbForProject') + ->inject('project') + ->inject('deviceForFiles') + ->callback($this->action(...)); + } + + public function action( + Message $message, + Realtime $queueForRealtime, + Database $dbForPlatform, + Database $dbForProject, + Document $project, + Device $deviceForFiles + ): void { + Console::log('Build action started'); + + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new \Exception('Missing payload'); + } + + Console::log('Site screenshot started'); + + $deploymentId = $payload['deploymentId'] ?? null; + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + $siteId = $deployment->getAttribute('resourceId'); + $site = $dbForProject->getDocument('sites', $siteId); + + // Realtime preparation + $event = "sites.[siteId].deployments.[deploymentId].update"; + $queueForRealtime + ->setSubscribers(['console']) + ->setProject($project) + ->setEvent($event) + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing started. \n"); + + try { + $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + Query::equal("projectInternalId", [$project->getSequence()]), + Query::equal("type", ["deployment"]), + Query::equal('deploymentInternalId', [$deployment->getSequence()]), + ])); + + if ($rule->isEmpty()) { + throw new \Exception("Rule for build not found"); + } + + $client = new FetchClient(); + $client->setTimeout(\intval($site->getAttribute('timeout', '15')) * 1000); + $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); + + $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + + $configs = [ + 'screenshotLight' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light', + 'theme' => 'light' + ], + 'screenshotDark' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark', + 'theme' => 'dark' + ], + ]; + + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); + $apiKey = $jwtObj->encode([ + 'hostnameOverride' => true, + 'disabledMetrics' => [ + METRIC_EXECUTIONS, + METRIC_EXECUTIONS_COMPUTE, + METRIC_EXECUTIONS_MB_SECONDS, + METRIC_NETWORK_REQUESTS, + METRIC_NETWORK_INBOUND, + METRIC_NETWORK_OUTBOUND, + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS), + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), + ], + 'bannerDisabled' => true, + 'projectCheckDisabled' => true, + 'previewAuthDisabled' => true, + 'deploymentStatusIgnored' => true + ]); + + $screenshotError = null; + $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $site, $client, &$screenshotError) { + return function () use ($key, $configs, $apiKey, $site, $client, &$screenshotError) { + try { + $config = $configs[$key]; + + $config['headers'] = \array_merge($config['headers'] ?? [], [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey + ]); + $config['sleep'] = 3000; + + $frameworks = Config::getParam('frameworks', []); + $framework = $frameworks[$site->getAttribute('framework', '')] ?? null; + if (!is_null($framework)) { + $config['sleep'] = $framework['screenshotSleep']; + } + + $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); + $fetchResponse = $client->fetch( + url: $browserEndpoint . '/screenshots', + method: 'POST', + body: $config + ); + + if ($fetchResponse->getStatusCode() >= 400) { + throw new \Exception($fetchResponse->getBody()); + } + + $screenshot = $fetchResponse->getBody(); + + return ['key' => $key, 'screenshot' => $screenshot]; + } catch (\Throwable $th) { + $screenshotError = $th->getMessage(); + return; + } + }; + }, \array_keys($configs))); + + if (!\is_null($screenshotError)) { + throw new \Exception($screenshotError); + } + + $mimeType = "image/png"; + $updates = new Document([]); + + foreach ($screenshots as $data) { + $key = $data['key']; + $screenshot = $data['screenshot']; + + $fileId = ID::unique(); + $fileName = $fileId . '.png'; + $path = $deviceForFiles->getPath($fileName); + $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root + $success = $deviceForFiles->write($path, $screenshot, $mimeType); + + if (!$success) { + throw new \Exception("Screenshot failed to save"); + } + + $teamId = $project->getAttribute('teamId', ''); + $file = new Document([ + '$id' => $fileId, + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + ], + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getSequence(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $deviceForFiles->getFileHash($path), + 'mimeType' => $mimeType, + 'sizeOriginal' => \strlen($screenshot), + 'sizeActual' => $deviceForFiles->getFileSize($path), + 'algorithm' => Compression::NONE, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => ['content_type' => $mimeType], + ]); + + Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file)); + + $updates->setAttribute($key, $fileId); + } + + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing finished. \n"); + + // Apply screenshot properties + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $updates); + + $queueForRealtime + ->setPayload($deployment->getArrayCopy()) + ->trigger(); + + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), + 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), + ])); + } catch (\Throwable $th) { + Console::warning("Screenshot failed to generate:"); + Console::warning($th->getMessage()); + Console::warning($th->getTraceAsString()); + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"); + } finally { + // Fill failure screenshots if not successful + + if (\is_null($deployment) || $deployment->isEmpty()) { + return; + } + + if (\is_null($site) || $site->isEmpty()) { + return; + } + + $updates = new Document(); + + if (empty($deployment->getAttribute('screenshotDark', ''))) { + $updates->setAttribute('screenshotDark', '/console/images/sites/screenshot-placeholder-dark.svg'); + } + if (empty($deployment->getAttribute('screenshotLight', ''))) { + $updates->setAttribute('screenshotLight', '/console/images/sites/screenshot-placeholder-light.svg'); + } + + if (!$updates->isEmpty()) { + // Apply screenshot properties + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $updates); + + $queueForRealtime + ->setPayload($deployment->getArrayCopy()) + ->trigger(); + + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), + 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), + ])); + } + } + } + + protected function appendToLogs(Database $dbForProject, string $deploymentId, Realtime $queueForRealtime, string $logs) + { + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + $buildLogs = $deployment->getAttribute('buildLogs', ''); + $buildLogs .= $logs; + + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $buildLogs + ])); + + $queueForRealtime + ->setPayload($deployment->getArrayCopy()) + ->trigger(); + } +} From 71f389e1c0611069d3514478ca045bfbde210844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 9 Jan 2026 10:41:58 +0100 Subject: [PATCH 5/8] AI PR review --- app/controllers/api/health.php | 4 +-- src/Appwrite/Event/Screenshot.php | 4 +-- .../Modules/Functions/Workers/Screenshots.php | 26 ++++++++++++++----- .../Services/Sites/SitesConsoleClientTest.php | 23 ++++++++++------ 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index db3dc3d95b..907ed54de8 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -956,7 +956,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME), System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME), System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME), - System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_CLASS_NAME), + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME), System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME), System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) ]), 'The name of the queue') @@ -1007,7 +1007,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks, System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates, System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds, - System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_CLASS_NAME) => $queueForScreenshots, + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots, System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging, System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations, }; diff --git a/src/Appwrite/Event/Screenshot.php b/src/Appwrite/Event/Screenshot.php index e2721d1939..acacf2b872 100644 --- a/src/Appwrite/Event/Screenshot.php +++ b/src/Appwrite/Event/Screenshot.php @@ -15,8 +15,8 @@ class Screenshot extends Event parent::__construct($publisher); $this - ->setQueue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME)) - ->setClass(System::getEnv('_APP_BUILDS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME)); + ->setQueue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME)) + ->setClass(System::getEnv('_APP_SCREENSHOTS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME)); } public function setDeploymentId(string $deploymentId): self diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php index 8037da4f9f..7540b74759 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -13,7 +13,6 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Fetch\Client as FetchClient; use Utopia\Platform\Action; use Utopia\Queue\Message; @@ -55,7 +54,7 @@ class Screenshots extends Action Document $project, Device $deviceForFiles ): void { - Console::log('Build action started'); + Console::log('Screenshot action started'); $payload = $message->getPayload() ?? []; @@ -67,9 +66,18 @@ class Screenshots extends Action $deploymentId = $payload['deploymentId'] ?? null; $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->isEmpty()) { + throw new \Exception('Deployment not found'); + } + $siteId = $deployment->getAttribute('resourceId'); $site = $dbForProject->getDocument('sites', $siteId); + if ($site->isEmpty()) { + throw new \Exception('Site not found'); + } + // Realtime preparation $event = "sites.[siteId].deployments.[deploymentId].update"; $queueForRealtime @@ -83,21 +91,25 @@ class Screenshots extends Action $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing started. \n"); try { - $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + $rule = $dbForPlatform->findOne('rules', [ Query::equal("projectInternalId", [$project->getSequence()]), Query::equal("type", ["deployment"]), Query::equal('deploymentInternalId', [$deployment->getSequence()]), - ])); + ]); if ($rule->isEmpty()) { - throw new \Exception("Rule for build not found"); + throw new \Exception("Rule for deployment not found"); } $client = new FetchClient(); $client->setTimeout(\intval($site->getAttribute('timeout', '15')) * 1000); $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); - $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); + + if ($bucket->isEmpty()) { + throw new \Exception('Bucket not found'); + } $configs = [ 'screenshotLight' => [ @@ -220,7 +232,7 @@ class Screenshots extends Action 'metadata' => ['content_type' => $mimeType], ]); - Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file)); + $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file); $updates->setAttribute($key, $fileId); } diff --git a/tests/e2e/Services/Sites/SitesConsoleClientTest.php b/tests/e2e/Services/Sites/SitesConsoleClientTest.php index 2b75402b25..31cee13261 100644 --- a/tests/e2e/Services/Sites/SitesConsoleClientTest.php +++ b/tests/e2e/Services/Sites/SitesConsoleClientTest.php @@ -54,15 +54,22 @@ class SitesConsoleClientTest extends Scope $this->assertStringContainsString("Themed website", $response['body']); $this->assertStringContainsString("@media (prefers-color-scheme: dark)", $response['body']); - $deployment = $this->getDeployment($siteId, $deploymentId); - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertNotEmpty($deployment['body']['screenshotLight']); - $this->assertNotEmpty($deployment['body']['screenshotDark']); + $deployment = null; + $site = null; + $this->assertEventually(function () use ($siteId, $deploymentId, &$deployment, &$site) { + $deployment = $this->getDeployment($siteId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['screenshotLight']); + $this->assertNotEmpty($deployment['body']['screenshotDark']); - $site = $this->getSite($siteId); - $this->assertEquals(200, $site['headers']['status-code']); - $this->assertEquals($deployment['body']['screenshotLight'], $site['body']['deploymentScreenshotLight']); - $this->assertEquals($deployment['body']['screenshotDark'], $site['body']['deploymentScreenshotDark']); + $site = $this->getSite($siteId); + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertEquals($deployment['body']['screenshotLight'], $site['body']['deploymentScreenshotLight']); + $this->assertEquals($deployment['body']['screenshotDark'], $site['body']['deploymentScreenshotDark']); + }); + + $this->assertNotNull($site); + $this->assertNotNull($deployment); $screenshotId = $deployment['body']['screenshotLight']; $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console", array_merge($this->getHeaders(), [ From eec023aab365c5356c4660342896c2641adead79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 9 Jan 2026 11:25:25 +0100 Subject: [PATCH 6/8] Simplify screenshot failures --- .../Modules/Functions/Workers/Screenshots.php | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php index 7540b74759..6adfcacab4 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -237,6 +237,7 @@ class Screenshots extends Action $updates->setAttribute($key, $fileId); } + $date = \date('H:i:s'); $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing finished. \n"); // Apply screenshot properties @@ -257,39 +258,8 @@ class Screenshots extends Action $date = \date('H:i:s'); $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"); - } finally { - // Fill failure screenshots if not successful - - if (\is_null($deployment) || $deployment->isEmpty()) { - return; - } - - if (\is_null($site) || $site->isEmpty()) { - return; - } - - $updates = new Document(); - - if (empty($deployment->getAttribute('screenshotDark', ''))) { - $updates->setAttribute('screenshotDark', '/console/images/sites/screenshot-placeholder-dark.svg'); - } - if (empty($deployment->getAttribute('screenshotLight', ''))) { - $updates->setAttribute('screenshotLight', '/console/images/sites/screenshot-placeholder-light.svg'); - } - - if (!$updates->isEmpty()) { - // Apply screenshot properties - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $updates); - - $queueForRealtime - ->setPayload($deployment->getArrayCopy()) - ->trigger(); - - $site = $dbForProject->updateDocument('sites', $site->getId(), new Document([ - 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), - 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), - ])); - } + + throw $th; } } From 2a9f9f68515f8bdf4db8d30de071111d822ba5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 9 Jan 2026 11:25:39 +0100 Subject: [PATCH 7/8] Fix race condition with screenshot worker updating dpeloyment --- .../Modules/Functions/Workers/Builds.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 530203734f..932e250028 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -913,17 +913,6 @@ class Builds extends Action Console::log('Build details stored'); $this->afterBuildSuccess($queueForRealtime, $dbForProject, $deployment, $runtime, $adapter); - $logs = $deployment->getAttribute('buildLogs', ''); - - /** Screenshot site */ - if ($resource->getCollection() === 'sites') { - $queueForScreenshots - ->setDeploymentId($deployment->getId()) - ->setProject($project) - ->trigger(); - - Console::log('Site screenshot queued'); - } $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); @@ -943,6 +932,16 @@ class Builds extends Action if ($isVcsEnabled) { $this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); } + + /** Screenshot site */ + if ($resource->getCollection() === 'sites') { + $queueForScreenshots + ->setDeploymentId($deployment->getId()) + ->setProject($project) + ->trigger(); + + Console::log('Site screenshot queued'); + } /** Set auto deploy */ $activateBuild = false; @@ -1093,9 +1092,10 @@ class Builds extends Action $endTime = DateTime::now(); $durationEnd = \microtime(true); - $deployment->setAttribute('buildEndedAt', $endTime); - $deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart))); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildEndedAt' => $endTime, + 'buildDuration' => \intval(\ceil($durationEnd - $durationStart)), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); From aebbe91c34350b4d362ae307bdfbacd8272ed1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 9 Jan 2026 11:25:47 +0100 Subject: [PATCH 8/8] formatting fix --- src/Appwrite/Platform/Modules/Functions/Workers/Builds.php | 2 +- src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 932e250028..285f78319a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -932,7 +932,7 @@ class Builds extends Action if ($isVcsEnabled) { $this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); } - + /** Screenshot site */ if ($resource->getCollection() === 'sites') { $queueForScreenshots diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php index 6adfcacab4..ca89532307 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -258,7 +258,7 @@ class Screenshots extends Action $date = \date('H:i:s'); $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"); - + throw $th; } }