From ad625029e8898bff2beb79df477f96a4215bcb12 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 24 Feb 2026 12:11:14 +0000 Subject: [PATCH 01/28] feat: add migration resource enums to SDK specification --- src/Appwrite/SDK/Specification/Format.php | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php index 53b4f1b3fa..bd2f063073 100644 --- a/src/Appwrite/SDK/Specification/Format.php +++ b/src/Appwrite/SDK/Specification/Format.php @@ -453,6 +453,38 @@ abstract class Format break; } break; + case 'migrations': + switch ($method) { + case 'createAppwriteMigration': + case 'getAppwriteReport': + switch ($param) { + case 'resources': + return 'AppwriteMigrationResource'; + } + break; + case 'createFirebaseMigration': + case 'getFirebaseReport': + switch ($param) { + case 'resources': + return 'FirebaseMigrationResource'; + } + break; + case 'createSupabaseMigration': + case 'getSupabaseReport': + switch ($param) { + case 'resources': + return 'SupabaseMigrationResource'; + } + break; + case 'createNHostMigration': + case 'getNHostReport': + switch ($param) { + case 'resources': + return 'NHostMigrationResource'; + } + break; + } + break; case 'project': switch ($method) { case 'getUsage': From 105c885c3f284e122078555f17637f2af1f558db Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Sun, 22 Feb 2026 16:22:44 +0000 Subject: [PATCH 02/28] added site migration --- composer.json | 17 +- composer.lock | 1297 ++++++++--------- src/Appwrite/Platform/Workers/Migrations.php | 2 + .../Utopia/Response/Model/MigrationReport.php | 18 + 4 files changed, 610 insertions(+), 724 deletions(-) diff --git a/composer.json b/composer.json index 931e5fe862..fc70e8a988 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.6.*", + "utopia-php/migration": "1.6.x-dev", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", @@ -113,5 +113,18 @@ "php-http/discovery": true, "tbachert/spi": true } - } + }, + "repositories": [ + { + "type": "path", + "url": "../utopia-migration", + "options": { + "versions": { + "utopia-php/migration": "1.6.x-dev" + } + } + } + ], + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/composer.lock b/composer.lock index 49db72a67e..031bf10a1b 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": "cb2ca42cfed8603c31b75113c85a4699", + "content-hash": "9e05c1a1869d52d4e7679f99fb04b51b", "packages": [ { "name": "adhocore/jwt", @@ -276,31 +276,34 @@ }, { "name": "chillerlan/php-qrcode", - "version": "4.3.4", + "version": "4.4.2", "source": { "type": "git", "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d" + "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d", - "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", + "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", "shasum": "" }, "require": { - "chillerlan/php-settings-container": "^2.1.4", + "chillerlan/php-settings-container": "^2.1.6 || ^3.2.1", "ext-mbstring": "*", "php": "^7.4 || ^8.0" }, "require-dev": { - "phan/phan": "^5.3", - "phpunit/phpunit": "^9.5", - "setasign/fpdf": "^1.8.2" + "phan/phan": "^5.4.5", + "phpmd/phpmd": "^2.15", + "phpunit/phpunit": "^9.6", + "setasign/fpdf": "^1.8.2", + "squizlabs/php_codesniffer": "^3.11" }, "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", - "setasign/fpdf": "Required to use the QR FPDF output." + "setasign/fpdf": "Required to use the QR FPDF output.", + "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" }, "type": "library", "autoload": { @@ -327,7 +330,7 @@ "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" } ], - "description": "A QR code generator. PHP 7.4+", + "description": "A QR code generator with a user friendly API. PHP 7.4+", "homepage": "https://github.com/chillerlan/php-qrcode", "keywords": [ "phpqrcode", @@ -338,43 +341,39 @@ ], "support": { "issues": "https://github.com/chillerlan/php-qrcode/issues", - "source": "https://github.com/chillerlan/php-qrcode/tree/4.3.4" + "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.2" }, "funding": [ - { - "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", - "type": "custom" - }, { "url": "https://ko-fi.com/codemasher", "type": "ko_fi" } ], - "time": "2022-07-25T09:12:45+00:00" + "time": "2024-11-15T15:36:24+00:00" }, { "name": "chillerlan/php-settings-container", - "version": "2.1.6", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/chillerlan/php-settings-container.git", - "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb" + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/5553558bd381fce5108c6d0343c12e488cfec6bb", - "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", "shasum": "" }, "require": { "ext-json": "*", - "php": "^7.4 || ^8.0" + "php": "^8.1" }, "require-dev": { "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^1.11", "phpstan/phpstan-deprecation-rules": "^1.2", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.5", "squizlabs/php_codesniffer": "^3.10" }, "type": "library", @@ -394,10 +393,9 @@ "homepage": "https://github.com/codemasher" } ], - "description": "A container class for immutable settings objects. Not a DI container. PHP 7.4+", + "description": "A container class for immutable settings objects. Not a DI container.", "homepage": "https://github.com/chillerlan/php-settings-container", "keywords": [ - "PHP7", "Settings", "configuration", "container", @@ -417,7 +415,7 @@ "type": "ko_fi" } ], - "time": "2024-07-17T01:04:28+00:00" + "time": "2024-07-16T11:13:48+00:00" }, { "name": "composer/semver", @@ -792,40 +790,42 @@ }, { "name": "league/csv", - "version": "9.14.0", + "version": "9.24.1", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "34bf0df7340b60824b9449b5c526fcc3325070d5" + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/34bf0df7340b60824b9449b5c526fcc3325070d5", - "reference": "34bf0df7340b60824b9449b5c526fcc3325070d5", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8", + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8", "shasum": "" }, "require": { "ext-filter": "*", - "ext-json": "*", - "ext-mbstring": "*", "php": "^8.1.2" }, "require-dev": { - "doctrine/collections": "^2.1.4", "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^v3.22.0", - "phpbench/phpbench": "^1.2.15", - "phpstan/phpstan": "^1.10.50", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpunit": "^10.5.3", - "symfony/var-dumper": "^6.4.0" + "friendsofphp/php-cs-fixer": "^3.75.0", + "phpbench/phpbench": "^1.4.1", + "phpstan/phpstan": "^1.12.27", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.2", + "phpunit/phpunit": "^10.5.16 || ^11.5.22", + "symfony/var-dumper": "^6.4.8 || ^7.3.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", - "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters" + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters", + "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters", + "ext-mysqli": "Requiered to use the package with the MySQLi extension", + "ext-pdo": "Required to use the package with the PDO extension", + "ext-pgsql": "Requiered to use the package with the PgSQL extension", + "ext-sqlite3": "Required to use the package with the SQLite3 extension" }, "type": "library", "extra": { @@ -877,7 +877,7 @@ "type": "github" } ], - "time": "2023-12-29T07:34:53+00:00" + "time": "2025-06-25T14:53:51+00:00" }, { "name": "matomo/device-detector", @@ -3401,60 +3401,6 @@ }, "time": "2026-02-02T10:43:10+00:00" }, - { - "name": "utopia-php/agents", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/agents.git", - "reference": "9291c4616b794bd3f0a342bfabaad2c2a4de7a19" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/agents/zipball/9291c4616b794bd3f0a342bfabaad2c2a4de7a19", - "reference": "9291c4616b794bd3f0a342bfabaad2c2a4de7a19", - "shasum": "" - }, - "require": { - "php": ">=8.3", - "utopia-php/fetch": "0.5.*" - }, - "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.9.x-dev", - "phpunit/phpunit": "^9.3", - "vimeo/psalm": "4.0.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Agents\\": "src/Agents" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Utopia PHP", - "email": "team@appwrite.io" - } - ], - "description": "A simple PHP AI agents library", - "keywords": [ - "agents", - "ai", - "automation", - "machine-learning", - "php" - ], - "support": { - "issues": "https://github.com/utopia-php/agents/issues", - "source": "https://github.com/utopia-php/agents/tree/1.2.0" - }, - "time": "2026-02-23T12:08:31+00:00" - }, { "name": "utopia-php/analytics", "version": "0.15.2", @@ -3851,16 +3797,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.4", + "version": "5.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5" + "reference": "e3305f6ca64eb5dd715e30212fd32d5d834a3a46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ba1ee9cb2c7624d0fada782b285bd9958a07bbe5", - "reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e3305f6ca64eb5dd715e30212fd32d5d834a3a46", + "reference": "e3305f6ca64eb5dd715e30212fd32d5d834a3a46", "shasum": "" }, "require": { @@ -3903,22 +3849,22 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.4" + "source": "https://github.com/utopia-php/database/tree/5.3.1" }, - "time": "2026-02-24T00:37:36+00:00" + "time": "2026-02-18T07:15:40+00:00" }, { "name": "utopia-php/detector", - "version": "0.2.4", + "version": "0.2.3", "source": { "type": "git", "url": "https://github.com/utopia-php/detector.git", - "reference": "a564381108518ce6d18b6ab441f1448bd8355afd" + "reference": "c1f49b3e82250c3256ffba48aa9737d6c17a243a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/detector/zipball/a564381108518ce6d18b6ab441f1448bd8355afd", - "reference": "a564381108518ce6d18b6ab441f1448bd8355afd", + "url": "https://api.github.com/repos/utopia-php/detector/zipball/c1f49b3e82250c3256ffba48aa9737d6c17a243a", + "reference": "c1f49b3e82250c3256ffba48aa9737d6c17a243a", "shasum": "" }, "require": { @@ -3948,9 +3894,9 @@ ], "support": { "issues": "https://github.com/utopia-php/detector/issues", - "source": "https://github.com/utopia-php/detector/tree/0.2.4" + "source": "https://github.com/utopia-php/detector/tree/0.2.3" }, - "time": "2026-02-17T11:26:34+00:00" + "time": "2025-11-24T15:52:51+00:00" }, { "name": "utopia-php/di", @@ -4002,16 +3948,16 @@ }, { "name": "utopia-php/dns", - "version": "1.6.5", + "version": "1.6.3", "source": { "type": "git", "url": "https://github.com/utopia-php/dns.git", - "reference": "574327f0f5fabefa7048030c5634cde33ad10640" + "reference": "c46a13c42bc19365985264a6fdaaaf7d8e736d3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/dns/zipball/574327f0f5fabefa7048030c5634cde33ad10640", - "reference": "574327f0f5fabefa7048030c5634cde33ad10640", + "url": "https://api.github.com/repos/utopia-php/dns/zipball/c46a13c42bc19365985264a6fdaaaf7d8e736d3e", + "reference": "c46a13c42bc19365985264a6fdaaaf7d8e736d3e", "shasum": "" }, "require": { @@ -4053,9 +3999,9 @@ ], "support": { "issues": "https://github.com/utopia-php/dns/issues", - "source": "https://github.com/utopia-php/dns/tree/1.6.5" + "source": "https://github.com/utopia-php/dns/tree/1.6.3" }, - "time": "2026-02-19T16:06:46+00:00" + "time": "2026-02-17T10:45:46+00:00" }, { "name": "utopia-php/domains", @@ -4268,16 +4214,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.40", + "version": "0.33.39", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1" + "reference": "409a258814d664d3a50fa2f48b6695679334d30b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/0ba25e1282c6a2f849053f7ccf28d567c2c321b1", - "reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1", + "url": "https://api.github.com/repos/utopia-php/http/zipball/409a258814d664d3a50fa2f48b6695679334d30b", + "reference": "409a258814d664d3a50fa2f48b6695679334d30b", "shasum": "" }, "require": { @@ -4311,9 +4257,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.40" + "source": "https://github.com/utopia-php/http/tree/0.33.39" }, - "time": "2026-02-19T13:00:08+00:00" + "time": "2026-02-11T06:33:42+00:00" }, { "name": "utopia-php/image", @@ -4518,17 +4464,11 @@ }, { "name": "utopia-php/migration", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/migration.git", - "reference": "c5c7544d02d2418536d41050794050132f247d62" - }, + "version": "1.6.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5c7544d02d2418536d41050794050132f247d62", - "reference": "c5c7544d02d2418536d41050794050132f247d62", - "shasum": "" + "type": "path", + "url": "../utopia-migration", + "reference": "0152f6ddf33c1c9e359c32f8021691c4e7c124b5" }, "require": { "appwrite/appwrite": "19.*", @@ -4553,7 +4493,25 @@ "Utopia\\Migration\\": "src/Migration" } }, - "notification-url": "https://packagist.org/downloads/", + "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" + ] + }, "license": [ "MIT" ], @@ -4565,11 +4523,9 @@ "upf", "utopia" ], - "support": { - "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.6.1" - }, - "time": "2026-02-17T05:49:48+00:00" + "transport-options": { + "relative": true + } }, { "name": "utopia-php/mongo", @@ -4791,16 +4747,16 @@ }, { "name": "utopia-php/queue", - "version": "0.15.6", + "version": "0.15.5", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "08e361d69610f371382b344c369eef355ca414b4" + "reference": "aa80ffe6350efd3394615c3763dba138223a225a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/08e361d69610f371382b344c369eef355ca414b4", - "reference": "08e361d69610f371382b344c369eef355ca414b4", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/aa80ffe6350efd3394615c3763dba138223a225a", + "reference": "aa80ffe6350efd3394615c3763dba138223a225a", "shasum": "" }, "require": { @@ -4851,9 +4807,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.15.6" + "source": "https://github.com/utopia-php/queue/tree/0.15.5" }, - "time": "2026-02-23T13:03:51+00:00" + "time": "2026-02-10T12:30:46+00:00" }, { "name": "utopia-php/registry", @@ -5007,16 +4963,16 @@ }, { "name": "utopia-php/storage", - "version": "1.0.1", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "f014be445f0baa635d0764e1673196f412511618" + "reference": "f672e8865938e5d1d6dc3bd149ceba4e5582199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/f014be445f0baa635d0764e1673196f412511618", - "reference": "f014be445f0baa635d0764e1673196f412511618", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/f672e8865938e5d1d6dc3bd149ceba4e5582199e", + "reference": "f672e8865938e5d1d6dc3bd149ceba4e5582199e", "shasum": "" }, "require": { @@ -5054,9 +5010,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/1.0.1" + "source": "https://github.com/utopia-php/storage/tree/1.0.0" }, - "time": "2026-02-23T05:59:32+00:00" + "time": "2026-02-17T04:37:10+00:00" }, { "name": "utopia-php/system", @@ -5439,16 +5395,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.10.2", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "b31e6b4209ecad46d2fef509dc59f1945d0e83cb" + "reference": "5a8a304e404c84539f3ac6562c736f8c7ef2bef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b31e6b4209ecad46d2fef509dc59f1945d0e83cb", - "reference": "b31e6b4209ecad46d2fef509dc59f1945d0e83cb", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5a8a304e404c84539f3ac6562c736f8c7ef2bef2", + "reference": "5a8a304e404c84539f3ac6562c736f8c7ef2bef2", "shasum": "" }, "require": { @@ -5484,102 +5440,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/1.10.2" + "source": "https://github.com/appwrite/sdk-generator/tree/1.9.3" }, - "time": "2026-02-23T04:04:15+00:00" - }, - { - "name": "brianium/paratest", - "version": "v7.19.0", - "source": { - "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "fidry/cpu-core-counter": "^1.3.0", - "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.3.0 || ~8.4.0 || ~8.5.0", - "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", - "phpunit/php-file-iterator": "^6.0.1 || ^7", - "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.9 || ^13", - "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.4 || ^8.0.4", - "symfony/process": "^7.4.5 || ^8.0.5" - }, - "require-dev": { - "doctrine/coding-standard": "^14.0.0", - "ext-pcntl": "*", - "ext-pcov": "*", - "ext-posix": "*", - "phpstan/phpstan": "^2.1.38", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.12", - "phpstan/phpstan-strict-rules": "^2.0.8", - "symfony/filesystem": "^7.4.0 || ^8.0.1" - }, - "bin": [ - "bin/paratest", - "bin/paratest_for_phpstorm" - ], - "type": "library", - "autoload": { - "psr-4": { - "ParaTest\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "role": "Developer" - }, - { - "name": "Filippo Tessarotto", - "email": "zoeslam@gmail.com", - "role": "Developer" - } - ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", - "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" - ], - "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/Slamdunk", - "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" - } - ], - "time": "2026-02-06T10:53:26+00:00" + "time": "2026-02-17T11:48:05+00:00" }, { "name": "doctrine/annotations", @@ -5658,6 +5521,75 @@ "abandoned": true, "time": "2024-09-05T10:17:24+00:00" }, + { + "name": "doctrine/instantiator", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "shasum": "" + }, + "require": { + "php": "^8.4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2026-01-05T06:47:08+00:00" + }, { "name": "doctrine/lexer", "version": "3.0.1", @@ -5735,127 +5667,6 @@ ], "time": "2024-02-05T11:56:58+00:00" }, - { - "name": "fidry/cpu-core-counter", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-deprecation-rules": "^2.0.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^8.5.31 || ^9.5.26", - "webmozarts/strict-phpunit": "^7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" - } - ], - "description": "Tiny utility to get the number of CPU cores.", - "keywords": [ - "CPU", - "core" - ], - "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" - }, - "funding": [ - { - "url": "https://github.com/theofidry", - "type": "github" - } - ], - "time": "2025-08-14T07:29:31+00:00" - }, - { - "name": "jean85/pretty-package-versions", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.1.0", - "php": "^7.4|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.2", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^7.5|^8.5|^9.6", - "rector/rector": "^2.0", - "vimeo/psalm": "^4.3 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A library to get pretty versions strings of installed dependencies", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" - }, - "time": "2025-03-19T14:43:43+00:00" - }, { "name": "laravel/pint", "version": "v1.27.1", @@ -6486,34 +6297,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.3", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", - "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.7.0", - "php": ">=8.3", - "phpunit/php-file-iterator": "^6.0", - "phpunit/php-text-template": "^5.0", - "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0.3", - "sebastian/lines-of-code": "^4.0", - "sebastian/version": "^6.0", - "theseer/tokenizer": "^2.0.1" + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^12.5.1" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -6522,7 +6334,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5.x-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -6551,52 +6363,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", - "type": "tidelift" } ], - "time": "2026-02-06T06:01:44+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -6623,49 +6423,36 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", - "type": "tidelift" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2021-12-02T12:48:52+00:00" }, { "name": "phpunit/php-invoker", - "version": "6.0.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-pcntl": "*" @@ -6673,7 +6460,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -6699,8 +6486,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" }, "funding": [ { @@ -6708,32 +6494,32 @@ "type": "github" } ], - "time": "2025-02-07T04:58:58+00:00" + "time": "2020-09-28T05:58:55+00:00" }, { "name": "phpunit/php-text-template", - "version": "5.0.0", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -6759,8 +6545,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" }, "funding": [ { @@ -6768,32 +6553,32 @@ "type": "github" } ], - "time": "2025-02-07T04:59:16+00:00" + "time": "2020-10-26T05:33:50+00:00" }, { "name": "phpunit/php-timer", - "version": "8.0.0", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -6819,8 +6604,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" }, "funding": [ { @@ -6828,23 +6612,24 @@ "type": "github" } ], - "time": "2025-02-07T04:59:38+00:00" + "time": "2020-10-26T13:16:10+00:00" }, { "name": "phpunit/phpunit", - "version": "12.5.14", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", - "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -6854,23 +6639,27 @@ "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.3", - "phpunit/php-file-iterator": "^6.0.1", - "phpunit/php-invoker": "^6.0.0", - "phpunit/php-text-template": "^5.0.0", - "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.4", - "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.0.3", - "sebastian/exporter": "^7.0.2", - "sebastian/global-state": "^8.0.2", - "sebastian/object-enumerator": "^7.0.0", - "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.3", - "sebastian/version": "^6.0.0", - "staabm/side-effects-detector": "^1.0.5" + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -6878,7 +6667,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -6910,7 +6699,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -6934,7 +6723,7 @@ "type": "tidelift" } ], - "time": "2026-02-18T12:38:40+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/cache", @@ -6987,28 +6776,28 @@ }, { "name": "sebastian/cli-parser", - "version": "4.2.0", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", - "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.2-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -7031,60 +6820,153 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", - "type": "tidelift" } ], - "time": "2025-09-14T09:36:45+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { - "name": "sebastian/comparator", - "version": "7.1.4", + "name": "sebastian/code-unit", + "version": "1.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", - "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/diff": "^7.0", - "sebastian/exporter": "^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.2" - }, - "suggest": { - "ext-bcmath": "For comparing BcMath\\Number objects" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.1-dev" + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" } }, "autoload": { @@ -7123,8 +7005,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { @@ -7144,33 +7025,33 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:28:48+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", - "version": "5.0.0", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.3" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -7193,8 +7074,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -7202,33 +7082,33 @@ "type": "github" } ], - "time": "2025-02-07T04:55:25+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -7260,8 +7140,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -7269,27 +7148,27 @@ "type": "github" } ], - "time": "2025-02-07T04:55:46+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", - "version": "8.0.3", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-posix": "*" @@ -7297,7 +7176,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -7316,7 +7195,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", + "homepage": "http://www.github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -7324,55 +7203,42 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", - "type": "tidelift" } ], - "time": "2025-08-12T14:11:56+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.2", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", - "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/recursion-context": "^7.0" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -7414,8 +7280,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { @@ -7435,35 +7300,38 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:16:11+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.2", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", - "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -7482,14 +7350,13 @@ } ], "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { @@ -7509,33 +7376,33 @@ "type": "tidelift" } ], - "time": "2025-08-29T11:29:25+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", - "version": "4.0.0", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", - "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.3" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -7558,8 +7425,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -7567,34 +7433,34 @@ "type": "github" } ], - "time": "2025-02-07T04:57:28+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", - "version": "7.0.0", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -7616,8 +7482,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" }, "funding": [ { @@ -7625,32 +7490,32 @@ "type": "github" } ], - "time": "2025-02-07T04:57:48+00:00" + "time": "2020-10-26T13:12:34+00:00" }, { "name": "sebastian/object-reflector", - "version": "5.0.0", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -7672,8 +7537,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" }, "funding": [ { @@ -7681,32 +7545,32 @@ "type": "github" } ], - "time": "2025-02-07T04:58:17+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/recursion-context", - "version": "7.0.1", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -7736,8 +7600,7 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { @@ -7757,32 +7620,86 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:44:59+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { - "name": "sebastian/type", - "version": "6.0.3", + "name": "sebastian/resource-operations", + "version": "3.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", - "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" } }, "autoload": { @@ -7805,50 +7722,37 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/type", - "type": "tidelift" } ], - "time": "2025-08-09T06:57:12+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", - "version": "6.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=7.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -7871,8 +7775,7 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, "funding": [ { @@ -7880,7 +7783,7 @@ "type": "github" } ], - "time": "2025-02-07T05:00:38+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { "name": "seld/jsonlint", @@ -7946,58 +7849,6 @@ ], "time": "2024-07-11T14:55:45+00:00" }, - { - "name": "staabm/side-effects-detector", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/staabm/side-effects-detector.git", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.6", - "phpunit/phpunit": "^9.6.21", - "symfony/var-dumper": "^5.4.43", - "tomasvotruba/type-coverage": "1.0.0", - "tomasvotruba/unused-public": "1.0.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "lib/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A static analysis tool to detect side effects in PHP code", - "keywords": [ - "static analysis" - ], - "support": { - "issues": "https://github.com/staabm/side-effects-detector/issues", - "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" - }, - "funding": [ - { - "url": "https://github.com/staabm", - "type": "github" - } - ], - "time": "2024-10-20T05:08:20+00:00" - }, { "name": "swoole/ide-helper", "version": "6.0.2", @@ -8865,23 +8716,23 @@ }, { "name": "theseer/tokenizer", - "version": "2.0.1", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", - "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^8.1" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -8903,7 +8754,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -8911,7 +8762,7 @@ "type": "github" } ], - "time": "2025-12-08T11:19:18+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "twig/twig", @@ -9044,7 +8895,9 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/migration": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -9068,5 +8921,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 5e729af98f..7bc6fe8d32 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -313,6 +313,8 @@ class Migrations extends Action 'files.write', 'functions.read', 'functions.write', + 'sites.read', + 'sites.write', 'tokens.read', 'tokens.write', ] diff --git a/src/Appwrite/Utopia/Response/Model/MigrationReport.php b/src/Appwrite/Utopia/Response/Model/MigrationReport.php index 56d17d3f7f..d20c1035ea 100644 --- a/src/Appwrite/Utopia/Response/Model/MigrationReport.php +++ b/src/Appwrite/Utopia/Response/Model/MigrationReport.php @@ -53,6 +53,24 @@ class MigrationReport extends Model 'default' => 0, 'example' => 20, ]) + ->addRule(Resource::TYPE_SITE, [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of sites to be migrated.', + 'default' => 0, + 'example' => 5, + ]) + ->addRule(Resource::TYPE_SITE_DEPLOYMENT, [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of site deployments to be migrated.', + 'default' => 0, + 'example' => 5, + ]) + ->addRule(Resource::TYPE_SITE_VARIABLE, [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of site variables to be migrated.', + 'default' => 0, + 'example' => 10, + ]) ->addRule('size', [ 'type' => self::TYPE_INTEGER, 'description' => 'Size of files to be migrated in mb.', From d58873ef14e062effb3dde8f04ebb427c423f4a2 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Sun, 22 Feb 2026 20:53:18 +0000 Subject: [PATCH 03/28] added site migration E2E test --- .../Services/Migrations/MigrationsBase.php | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 8f0fdfa733..606f9e8127 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -7,6 +7,7 @@ use Tests\E2E\Client; use Tests\E2E\General\UsageTest; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Services\Functions\FunctionsBase; +use Utopia\Console; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -1017,6 +1018,158 @@ trait MigrationsBase ]); } + /** + * Sites + */ + public function testAppwriteMigrationSite(): void + { + $site = $this->client->call(Client::METHOD_POST, '/sites', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'siteId' => ID::unique(), + 'name' => 'Test Site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'adapter' => 'static', + 'outputDirectory' => './', + ]); + + $this->assertEquals(201, $site['headers']['status-code'], 'Create site failed: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + $this->assertNotEmpty($site['body']['$id']); + + $siteId = $site['body']['$id']; + + // Create deployment + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', [ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'code' => $this->packageSite('static'), + 'activate' => true, + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deploymentId = $deployment['body']['$id']; + + // Wait for deployment to be ready + $this->assertEventually(function () use ($siteId, $deploymentId) { + $response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('ready', $response['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($response['body'], JSON_PRETTY_PRINT)); + }, 300000, 500); + + // Create environment variable + $variable = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'key' => 'TEST_VAR', + 'value' => 'test_value', + ]); + + $this->assertEquals(201, $variable['headers']['status-code']); + + // Perform migration + $result = $this->performMigrationSync([ + 'resources' => [ + Resource::TYPE_SITE, + Resource::TYPE_SITE_DEPLOYMENT, + Resource::TYPE_SITE_VARIABLE, + ], + 'endpoint' => $this->webEndpoint, + 'projectId' => $this->getProject()['$id'], + 'apiKey' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals('completed', $result['status']); + $this->assertEquals([Resource::TYPE_SITE, Resource::TYPE_SITE_DEPLOYMENT, Resource::TYPE_SITE_VARIABLE], $result['resources']); + + foreach ([Resource::TYPE_SITE, Resource::TYPE_SITE_DEPLOYMENT, Resource::TYPE_SITE_VARIABLE] as $resource) { + $this->assertArrayHasKey($resource, $result['statusCounters']); + $this->assertEquals(0, $result['statusCounters'][$resource]['error']); + $this->assertEquals(0, $result['statusCounters'][$resource]['pending']); + $this->assertEquals(1, $result['statusCounters'][$resource]['success']); + $this->assertEquals(0, $result['statusCounters'][$resource]['processing']); + $this->assertEquals(0, $result['statusCounters'][$resource]['warning']); + } + + // Verify site in destination + $response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals($siteId, $response['body']['$id']); + $this->assertEquals('Test Site', $response['body']['name']); + $this->assertEquals('node-22', $response['body']['buildRuntime']); + $this->assertEquals('other', $response['body']['framework']); + $this->assertEquals('static', $response['body']['adapter']); + + // Verify deployment in destination + $this->assertEventually(function () use ($siteId) { + $deployments = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $deployments['headers']['status-code']); + $this->assertNotEmpty($deployments['body']); + $this->assertEquals(1, $deployments['body']['total']); + $this->assertEquals('ready', $deployments['body']['deployments'][0]['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployments['body']['deployments'][0], JSON_PRETTY_PRINT)); + }, 100000, 500); + + // Verify variable in destination + $variables = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/variables', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $variables['headers']['status-code']); + $this->assertEquals(1, $variables['body']['total']); + $this->assertEquals('TEST_VAR', $variables['body']['variables'][0]['key']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + } + + private function packageSite(string $site): CURLFile + { + $stdout = ''; + $stderr = ''; + $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + } + /** * Import documents from a CSV file. */ From b0c5185ebd0cdf77549ff63cdfaa09c7187fd258 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 24 Feb 2026 12:42:06 +0000 Subject: [PATCH 04/28] feat: add site migration --- composer.json | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index fc70e8a988..f9bf631688 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.6.x-dev", + "utopia-php/migration": "1.6.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", @@ -113,18 +113,5 @@ "php-http/discovery": true, "tbachert/spi": true } - }, - "repositories": [ - { - "type": "path", - "url": "../utopia-migration", - "options": { - "versions": { - "utopia-php/migration": "1.6.x-dev" - } - } - } - ], - "minimum-stability": "dev", - "prefer-stable": true -} + } +} \ No newline at end of file From 90ab70dfc6db128f211e0b6ef16a94769b2ce7d0 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 24 Feb 2026 14:01:12 +0000 Subject: [PATCH 05/28] fix: use dev branch of utopia-php/migration for site camelCase fix --- composer.json | 2 +- composer.lock | 1278 +++++++++++++++++++++++++++---------------------- 2 files changed, 714 insertions(+), 566 deletions(-) diff --git a/composer.json b/composer.json index f9bf631688..e113b702f6 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.6.*", + "utopia-php/migration": "dev-fix-kebab-case-site", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", diff --git a/composer.lock b/composer.lock index 031bf10a1b..99ab74c5ee 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": "9e05c1a1869d52d4e7679f99fb04b51b", + "content-hash": "bbadd08240c123309a6b7be4da296624", "packages": [ { "name": "adhocore/jwt", @@ -276,34 +276,31 @@ }, { "name": "chillerlan/php-qrcode", - "version": "4.4.2", + "version": "4.3.4", "source": { "type": "git", "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4" + "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", - "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d", + "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d", "shasum": "" }, "require": { - "chillerlan/php-settings-container": "^2.1.6 || ^3.2.1", + "chillerlan/php-settings-container": "^2.1.4", "ext-mbstring": "*", "php": "^7.4 || ^8.0" }, "require-dev": { - "phan/phan": "^5.4.5", - "phpmd/phpmd": "^2.15", - "phpunit/phpunit": "^9.6", - "setasign/fpdf": "^1.8.2", - "squizlabs/php_codesniffer": "^3.11" + "phan/phan": "^5.3", + "phpunit/phpunit": "^9.5", + "setasign/fpdf": "^1.8.2" }, "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", - "setasign/fpdf": "Required to use the QR FPDF output.", - "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" + "setasign/fpdf": "Required to use the QR FPDF output." }, "type": "library", "autoload": { @@ -330,7 +327,7 @@ "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" } ], - "description": "A QR code generator with a user friendly API. PHP 7.4+", + "description": "A QR code generator. PHP 7.4+", "homepage": "https://github.com/chillerlan/php-qrcode", "keywords": [ "phpqrcode", @@ -341,39 +338,43 @@ ], "support": { "issues": "https://github.com/chillerlan/php-qrcode/issues", - "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.2" + "source": "https://github.com/chillerlan/php-qrcode/tree/4.3.4" }, "funding": [ + { + "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", + "type": "custom" + }, { "url": "https://ko-fi.com/codemasher", "type": "ko_fi" } ], - "time": "2024-11-15T15:36:24+00:00" + "time": "2022-07-25T09:12:45+00:00" }, { "name": "chillerlan/php-settings-container", - "version": "3.2.1", + "version": "2.1.6", "source": { "type": "git", "url": "https://github.com/chillerlan/php-settings-container.git", - "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" + "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", - "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/5553558bd381fce5108c6d0343c12e488cfec6bb", + "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb", "shasum": "" }, "require": { "ext-json": "*", - "php": "^8.1" + "php": "^7.4 || ^8.0" }, "require-dev": { "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^1.11", "phpstan/phpstan-deprecation-rules": "^1.2", - "phpunit/phpunit": "^10.5", + "phpunit/phpunit": "^9.6", "squizlabs/php_codesniffer": "^3.10" }, "type": "library", @@ -393,9 +394,10 @@ "homepage": "https://github.com/codemasher" } ], - "description": "A container class for immutable settings objects. Not a DI container.", + "description": "A container class for immutable settings objects. Not a DI container. PHP 7.4+", "homepage": "https://github.com/chillerlan/php-settings-container", "keywords": [ + "PHP7", "Settings", "configuration", "container", @@ -415,7 +417,7 @@ "type": "ko_fi" } ], - "time": "2024-07-16T11:13:48+00:00" + "time": "2024-07-17T01:04:28+00:00" }, { "name": "composer/semver", @@ -790,42 +792,40 @@ }, { "name": "league/csv", - "version": "9.24.1", + "version": "9.14.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8" + "reference": "34bf0df7340b60824b9449b5c526fcc3325070d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8", - "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/34bf0df7340b60824b9449b5c526fcc3325070d5", + "reference": "34bf0df7340b60824b9449b5c526fcc3325070d5", "shasum": "" }, "require": { "ext-filter": "*", + "ext-json": "*", + "ext-mbstring": "*", "php": "^8.1.2" }, "require-dev": { + "doctrine/collections": "^2.1.4", "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.75.0", - "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^1.12.27", - "phpstan/phpstan-deprecation-rules": "^1.2.1", - "phpstan/phpstan-phpunit": "^1.4.2", - "phpstan/phpstan-strict-rules": "^1.6.2", - "phpunit/phpunit": "^10.5.16 || ^11.5.22", - "symfony/var-dumper": "^6.4.8 || ^7.3.0" + "friendsofphp/php-cs-fixer": "^v3.22.0", + "phpbench/phpbench": "^1.2.15", + "phpstan/phpstan": "^1.10.50", + "phpstan/phpstan-deprecation-rules": "^1.1.4", + "phpstan/phpstan-phpunit": "^1.3.15", + "phpstan/phpstan-strict-rules": "^1.5.2", + "phpunit/phpunit": "^10.5.3", + "symfony/var-dumper": "^6.4.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", - "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters", - "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters", - "ext-mysqli": "Requiered to use the package with the MySQLi extension", - "ext-pdo": "Required to use the package with the PDO extension", - "ext-pgsql": "Requiered to use the package with the PgSQL extension", - "ext-sqlite3": "Required to use the package with the SQLite3 extension" + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters" }, "type": "library", "extra": { @@ -877,7 +877,7 @@ "type": "github" } ], - "time": "2025-06-25T14:53:51+00:00" + "time": "2023-12-29T07:34:53+00:00" }, { "name": "matomo/device-detector", @@ -3401,6 +3401,59 @@ }, "time": "2026-02-02T10:43:10+00:00" }, + { + "name": "utopia-php/agents", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/agents.git", + "reference": "052227953678a30ecc4b5467401fcb0b2386471e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e", + "reference": "052227953678a30ecc4b5467401fcb0b2386471e", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "utopia-php/fetch": "0.5.*" + }, + "require-dev": { + "laravel/pint": "^1.18", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Agents\\": "src/Agents" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Utopia PHP", + "email": "team@appwrite.io" + } + ], + "description": "A simple PHP AI agents library", + "keywords": [ + "agents", + "ai", + "automation", + "machine-learning", + "php" + ], + "support": { + "issues": "https://github.com/utopia-php/agents/issues", + "source": "https://github.com/utopia-php/agents/tree/1.2.1" + }, + "time": "2026-02-24T06:03:55+00:00" + }, { "name": "utopia-php/analytics", "version": "0.15.2", @@ -3797,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.1", + "version": "5.3.4", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "e3305f6ca64eb5dd715e30212fd32d5d834a3a46" + "reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/e3305f6ca64eb5dd715e30212fd32d5d834a3a46", - "reference": "e3305f6ca64eb5dd715e30212fd32d5d834a3a46", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ba1ee9cb2c7624d0fada782b285bd9958a07bbe5", + "reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5", "shasum": "" }, "require": { @@ -3849,22 +3902,22 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.1" + "source": "https://github.com/utopia-php/database/tree/5.3.4" }, - "time": "2026-02-18T07:15:40+00:00" + "time": "2026-02-24T00:37:36+00:00" }, { "name": "utopia-php/detector", - "version": "0.2.3", + "version": "0.2.4", "source": { "type": "git", "url": "https://github.com/utopia-php/detector.git", - "reference": "c1f49b3e82250c3256ffba48aa9737d6c17a243a" + "reference": "a564381108518ce6d18b6ab441f1448bd8355afd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/detector/zipball/c1f49b3e82250c3256ffba48aa9737d6c17a243a", - "reference": "c1f49b3e82250c3256ffba48aa9737d6c17a243a", + "url": "https://api.github.com/repos/utopia-php/detector/zipball/a564381108518ce6d18b6ab441f1448bd8355afd", + "reference": "a564381108518ce6d18b6ab441f1448bd8355afd", "shasum": "" }, "require": { @@ -3894,9 +3947,9 @@ ], "support": { "issues": "https://github.com/utopia-php/detector/issues", - "source": "https://github.com/utopia-php/detector/tree/0.2.3" + "source": "https://github.com/utopia-php/detector/tree/0.2.4" }, - "time": "2025-11-24T15:52:51+00:00" + "time": "2026-02-17T11:26:34+00:00" }, { "name": "utopia-php/di", @@ -3948,16 +4001,16 @@ }, { "name": "utopia-php/dns", - "version": "1.6.3", + "version": "1.6.5", "source": { "type": "git", "url": "https://github.com/utopia-php/dns.git", - "reference": "c46a13c42bc19365985264a6fdaaaf7d8e736d3e" + "reference": "574327f0f5fabefa7048030c5634cde33ad10640" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/dns/zipball/c46a13c42bc19365985264a6fdaaaf7d8e736d3e", - "reference": "c46a13c42bc19365985264a6fdaaaf7d8e736d3e", + "url": "https://api.github.com/repos/utopia-php/dns/zipball/574327f0f5fabefa7048030c5634cde33ad10640", + "reference": "574327f0f5fabefa7048030c5634cde33ad10640", "shasum": "" }, "require": { @@ -3999,9 +4052,9 @@ ], "support": { "issues": "https://github.com/utopia-php/dns/issues", - "source": "https://github.com/utopia-php/dns/tree/1.6.3" + "source": "https://github.com/utopia-php/dns/tree/1.6.5" }, - "time": "2026-02-17T10:45:46+00:00" + "time": "2026-02-19T16:06:46+00:00" }, { "name": "utopia-php/domains", @@ -4214,16 +4267,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.39", + "version": "0.33.40", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "409a258814d664d3a50fa2f48b6695679334d30b" + "reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/409a258814d664d3a50fa2f48b6695679334d30b", - "reference": "409a258814d664d3a50fa2f48b6695679334d30b", + "url": "https://api.github.com/repos/utopia-php/http/zipball/0ba25e1282c6a2f849053f7ccf28d567c2c321b1", + "reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1", "shasum": "" }, "require": { @@ -4257,9 +4310,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.39" + "source": "https://github.com/utopia-php/http/tree/0.33.40" }, - "time": "2026-02-11T06:33:42+00:00" + "time": "2026-02-19T13:00:08+00:00" }, { "name": "utopia-php/image", @@ -4464,11 +4517,17 @@ }, { "name": "utopia-php/migration", - "version": "1.6.x-dev", + "version": "dev-fix-kebab-case-site", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/migration.git", + "reference": "599d1a29cef64354a872f9f683497057a7a40d4c" + }, "dist": { - "type": "path", - "url": "../utopia-migration", - "reference": "0152f6ddf33c1c9e359c32f8021691c4e7c124b5" + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/599d1a29cef64354a872f9f683497057a7a40d4c", + "reference": "599d1a29cef64354a872f9f683497057a7a40d4c", + "shasum": "" }, "require": { "appwrite/appwrite": "19.*", @@ -4493,25 +4552,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" ], @@ -4523,9 +4564,11 @@ "upf", "utopia" ], - "transport-options": { - "relative": true - } + "support": { + "issues": "https://github.com/utopia-php/migration/issues", + "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" + }, + "time": "2026-02-24T13:55:52+00:00" }, { "name": "utopia-php/mongo", @@ -4747,16 +4790,16 @@ }, { "name": "utopia-php/queue", - "version": "0.15.5", + "version": "0.15.6", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "aa80ffe6350efd3394615c3763dba138223a225a" + "reference": "08e361d69610f371382b344c369eef355ca414b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/aa80ffe6350efd3394615c3763dba138223a225a", - "reference": "aa80ffe6350efd3394615c3763dba138223a225a", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/08e361d69610f371382b344c369eef355ca414b4", + "reference": "08e361d69610f371382b344c369eef355ca414b4", "shasum": "" }, "require": { @@ -4807,9 +4850,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.15.5" + "source": "https://github.com/utopia-php/queue/tree/0.15.6" }, - "time": "2026-02-10T12:30:46+00:00" + "time": "2026-02-23T13:03:51+00:00" }, { "name": "utopia-php/registry", @@ -4963,16 +5006,16 @@ }, { "name": "utopia-php/storage", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "f672e8865938e5d1d6dc3bd149ceba4e5582199e" + "reference": "f014be445f0baa635d0764e1673196f412511618" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/f672e8865938e5d1d6dc3bd149ceba4e5582199e", - "reference": "f672e8865938e5d1d6dc3bd149ceba4e5582199e", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/f014be445f0baa635d0764e1673196f412511618", + "reference": "f014be445f0baa635d0764e1673196f412511618", "shasum": "" }, "require": { @@ -5010,9 +5053,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/1.0.0" + "source": "https://github.com/utopia-php/storage/tree/1.0.1" }, - "time": "2026-02-17T04:37:10+00:00" + "time": "2026-02-23T05:59:32+00:00" }, { "name": "utopia-php/system", @@ -5395,16 +5438,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.9.3", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "5a8a304e404c84539f3ac6562c736f8c7ef2bef2" + "reference": "b31e6b4209ecad46d2fef509dc59f1945d0e83cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5a8a304e404c84539f3ac6562c736f8c7ef2bef2", - "reference": "5a8a304e404c84539f3ac6562c736f8c7ef2bef2", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b31e6b4209ecad46d2fef509dc59f1945d0e83cb", + "reference": "b31e6b4209ecad46d2fef509dc59f1945d0e83cb", "shasum": "" }, "require": { @@ -5440,9 +5483,102 @@ "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/1.9.3" + "source": "https://github.com/appwrite/sdk-generator/tree/1.10.2" }, - "time": "2026-02-17T11:48:05+00:00" + "time": "2026-02-23T04:04:15+00:00" + }, + { + "name": "brianium/paratest", + "version": "v7.19.0", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.9 || ^13", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/process": "^7.4.5 || ^8.0.5" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.38", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.12", + "phpstan/phpstan-strict-rules": "^2.0.8", + "symfony/filesystem": "^7.4.0 || ^8.0.1" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2026-02-06T10:53:26+00:00" }, { "name": "doctrine/annotations", @@ -5521,75 +5657,6 @@ "abandoned": true, "time": "2024-09-05T10:17:24+00:00" }, - { - "name": "doctrine/instantiator", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", - "shasum": "" - }, - "require": { - "php": "^8.4" - }, - "require-dev": { - "doctrine/coding-standard": "^14", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.58" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.1.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2026-01-05T06:47:08+00:00" - }, { "name": "doctrine/lexer", "version": "3.0.1", @@ -5667,6 +5734,127 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, { "name": "laravel/pint", "version": "v1.27.1", @@ -6297,35 +6485,34 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", - "theseer/tokenizer": "^1.2.3" + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -6334,7 +6521,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -6363,40 +6550,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2026-02-06T06:01:44+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -6423,36 +6622,49 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -6460,7 +6672,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -6486,7 +6698,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -6494,32 +6707,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -6545,7 +6758,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -6553,32 +6767,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -6604,7 +6818,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -6612,24 +6827,23 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.34", + "version": "12.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b36f02317466907a230d3aa1d34467041271ef4a" + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", - "reference": "b36f02317466907a230d3aa1d34467041271ef4a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -6639,27 +6853,23 @@ "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.10", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.8", - "sebastian/global-state": "^5.0.8", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" }, "bin": [ "phpunit" @@ -6667,7 +6877,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -6699,7 +6909,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ { @@ -6723,7 +6933,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T05:45:00+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { "name": "psr/cache", @@ -6776,28 +6986,28 @@ }, { "name": "sebastian/cli-parser", - "version": "1.0.2", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -6820,153 +7030,60 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.10", + "version": "7.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", - "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -7005,7 +7122,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "funding": [ { @@ -7025,33 +7143,33 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:22:56+00:00" + "time": "2026-01-24T09:28:48+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -7074,7 +7192,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { @@ -7082,33 +7201,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -7140,7 +7259,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -7148,27 +7268,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-posix": "*" @@ -7176,7 +7296,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -7195,7 +7315,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -7203,42 +7323,55 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2025-08-12T14:11:56+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.8", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -7280,7 +7413,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { @@ -7300,38 +7434,35 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:03:27+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.8", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -7350,13 +7481,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { @@ -7376,33 +7508,33 @@ "type": "tidelift" } ], - "time": "2025-08-10T07:10:35+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -7425,7 +7557,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" }, "funding": [ { @@ -7433,34 +7566,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2025-02-07T04:57:28+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -7482,7 +7615,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -7490,32 +7624,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -7537,7 +7671,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { @@ -7545,32 +7680,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.6", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -7600,7 +7735,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { @@ -7620,86 +7756,32 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:57:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -7722,37 +7804,50 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -7775,7 +7870,8 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -7783,7 +7879,7 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { "name": "seld/jsonlint", @@ -7849,6 +7945,58 @@ ], "time": "2024-07-11T14:55:45+00:00" }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, { "name": "swoole/ide-helper", "version": "6.0.2", @@ -8716,23 +8864,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -8754,7 +8902,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -8762,7 +8910,7 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { "name": "twig/twig", @@ -8921,5 +9069,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 715adf62ca5e2cb73a708fb9e20e3663d2e41e91 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 24 Feb 2026 14:58:06 +0000 Subject: [PATCH 06/28] fix: update utopia-migration to snake_case resource types --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 99ab74c5ee..82c8d1aab2 100644 --- a/composer.lock +++ b/composer.lock @@ -4521,12 +4521,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "599d1a29cef64354a872f9f683497057a7a40d4c" + "reference": "f8b2fb82f68f01cf6660810bfc9755c6c75c5e45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/599d1a29cef64354a872f9f683497057a7a40d4c", - "reference": "599d1a29cef64354a872f9f683497057a7a40d4c", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/f8b2fb82f68f01cf6660810bfc9755c6c75c5e45", + "reference": "f8b2fb82f68f01cf6660810bfc9755c6c75c5e45", "shasum": "" }, "require": { @@ -4568,7 +4568,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" }, - "time": "2026-02-24T13:55:52+00:00" + "time": "2026-02-24T14:54:45+00:00" }, { "name": "utopia-php/mongo", From 9745df4d3777514d130306e2a9f954385f296b0f Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 24 Feb 2026 22:30:17 +0000 Subject: [PATCH 07/28] fix: update utopia-migration to camelCase resource types --- composer.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index 82c8d1aab2..e059ded865 100644 --- a/composer.lock +++ b/composer.lock @@ -4521,12 +4521,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "f8b2fb82f68f01cf6660810bfc9755c6c75c5e45" + "reference": "79cab8efb98f3c9c7f8821813e72226113db4afb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/f8b2fb82f68f01cf6660810bfc9755c6c75c5e45", - "reference": "f8b2fb82f68f01cf6660810bfc9755c6c75c5e45", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/79cab8efb98f3c9c7f8821813e72226113db4afb", + "reference": "79cab8efb98f3c9c7f8821813e72226113db4afb", "shasum": "" }, "require": { @@ -4568,7 +4568,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" }, - "time": "2026-02-24T14:54:45+00:00" + "time": "2026-02-24T22:28:20+00:00" }, { "name": "utopia-php/mongo", @@ -9069,5 +9069,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } From dc441c39739248956dfc2f0ac1d5596ce3e9feb9 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 24 Feb 2026 22:44:57 +0000 Subject: [PATCH 08/28] fix: enable strict mode for Appwrite migration resource WhiteList --- app/controllers/api/migrations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 5a864ab928..05462ae1f5 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -62,7 +62,7 @@ Http::post('/v1/migrations/appwrite') ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources(), true)), 'List of resources to migrate') ->param('endpoint', '', new URL(), 'Source Appwrite endpoint') ->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject']) ->param('apiKey', '', new Text(512), 'Source API Key') @@ -708,7 +708,7 @@ Http::get('/v1/migrations/appwrite/report') ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources(), true)), 'List of resources to migrate') ->param('endpoint', '', new URL(), "Source's Appwrite Endpoint") ->param('projectID', '', new Text(512), "Source's Project ID") ->param('key', '', new Text(512), "Source's API Key") From 3ba5e12f89519aa63da1cb5d9fda2bbbf4d3081f Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 24 Feb 2026 23:19:21 +0000 Subject: [PATCH 09/28] fix: add deployment and environmentVariable to MigrationReport model --- .../Utopia/Response/Model/MigrationReport.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Appwrite/Utopia/Response/Model/MigrationReport.php b/src/Appwrite/Utopia/Response/Model/MigrationReport.php index d20c1035ea..824fcaa616 100644 --- a/src/Appwrite/Utopia/Response/Model/MigrationReport.php +++ b/src/Appwrite/Utopia/Response/Model/MigrationReport.php @@ -53,6 +53,18 @@ class MigrationReport extends Model 'default' => 0, 'example' => 20, ]) + ->addRule(Resource::TYPE_DEPLOYMENT, [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of deployments to be migrated.', + 'default' => 0, + 'example' => 5, + ]) + ->addRule(Resource::TYPE_ENVIRONMENT_VARIABLE, [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of environment variables to be migrated.', + 'default' => 0, + 'example' => 10, + ]) ->addRule(Resource::TYPE_SITE, [ 'type' => self::TYPE_INTEGER, 'description' => 'Number of sites to be migrated.', From 6b90d00cc2727e50ef3a2a925ba1c152606188f7 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 25 Feb 2026 00:24:16 +0000 Subject: [PATCH 10/28] fix: update utopia-migration lock to include strtolower removal --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index e059ded865..53866ca191 100644 --- a/composer.lock +++ b/composer.lock @@ -4521,12 +4521,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "79cab8efb98f3c9c7f8821813e72226113db4afb" + "reference": "987d11c9f744348908b5af237291cf2be5a66e9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/79cab8efb98f3c9c7f8821813e72226113db4afb", - "reference": "79cab8efb98f3c9c7f8821813e72226113db4afb", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/987d11c9f744348908b5af237291cf2be5a66e9e", + "reference": "987d11c9f744348908b5af237291cf2be5a66e9e", "shasum": "" }, "require": { @@ -4568,7 +4568,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" }, - "time": "2026-02-24T22:28:20+00:00" + "time": "2026-02-24T23:51:51+00:00" }, { "name": "utopia-php/mongo", From 5a6cfdbe4c5d97e50911e25338c486aebc1c4f2f Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 25 Feb 2026 06:42:46 +0000 Subject: [PATCH 11/28] fix: revert composer to 1.8.x and remove child resource counts from MigrationReport --- composer.json | 4 +- composer.lock | 39 +++++++++---------- .../Utopia/Response/Model/MigrationReport.php | 18 --------- 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/composer.json b/composer.json index e113b702f6..931e5fe862 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "dev-fix-kebab-case-site", + "utopia-php/migration": "1.6.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", @@ -114,4 +114,4 @@ "tbachert/spi": true } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 53866ca191..49db72a67e 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": "bbadd08240c123309a6b7be4da296624", + "content-hash": "cb2ca42cfed8603c31b75113c85a4699", "packages": [ { "name": "adhocore/jwt", @@ -3403,16 +3403,16 @@ }, { "name": "utopia-php/agents", - "version": "1.2.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/agents.git", - "reference": "052227953678a30ecc4b5467401fcb0b2386471e" + "reference": "9291c4616b794bd3f0a342bfabaad2c2a4de7a19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e", - "reference": "052227953678a30ecc4b5467401fcb0b2386471e", + "url": "https://api.github.com/repos/utopia-php/agents/zipball/9291c4616b794bd3f0a342bfabaad2c2a4de7a19", + "reference": "9291c4616b794bd3f0a342bfabaad2c2a4de7a19", "shasum": "" }, "require": { @@ -3420,9 +3420,10 @@ "utopia-php/fetch": "0.5.*" }, "require-dev": { - "laravel/pint": "^1.18", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^12.0" + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.9.x-dev", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" }, "type": "library", "autoload": { @@ -3450,9 +3451,9 @@ ], "support": { "issues": "https://github.com/utopia-php/agents/issues", - "source": "https://github.com/utopia-php/agents/tree/1.2.1" + "source": "https://github.com/utopia-php/agents/tree/1.2.0" }, - "time": "2026-02-24T06:03:55+00:00" + "time": "2026-02-23T12:08:31+00:00" }, { "name": "utopia-php/analytics", @@ -4517,16 +4518,16 @@ }, { "name": "utopia-php/migration", - "version": "dev-fix-kebab-case-site", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "987d11c9f744348908b5af237291cf2be5a66e9e" + "reference": "c5c7544d02d2418536d41050794050132f247d62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/987d11c9f744348908b5af237291cf2be5a66e9e", - "reference": "987d11c9f744348908b5af237291cf2be5a66e9e", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5c7544d02d2418536d41050794050132f247d62", + "reference": "c5c7544d02d2418536d41050794050132f247d62", "shasum": "" }, "require": { @@ -4566,9 +4567,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" + "source": "https://github.com/utopia-php/migration/tree/1.6.1" }, - "time": "2026-02-24T23:51:51+00:00" + "time": "2026-02-17T05:49:48+00:00" }, { "name": "utopia-php/mongo", @@ -9043,9 +9044,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/migration": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -9069,5 +9068,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Appwrite/Utopia/Response/Model/MigrationReport.php b/src/Appwrite/Utopia/Response/Model/MigrationReport.php index 824fcaa616..17c1e89ce7 100644 --- a/src/Appwrite/Utopia/Response/Model/MigrationReport.php +++ b/src/Appwrite/Utopia/Response/Model/MigrationReport.php @@ -59,30 +59,12 @@ class MigrationReport extends Model 'default' => 0, 'example' => 5, ]) - ->addRule(Resource::TYPE_ENVIRONMENT_VARIABLE, [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Number of environment variables to be migrated.', - 'default' => 0, - 'example' => 10, - ]) ->addRule(Resource::TYPE_SITE, [ 'type' => self::TYPE_INTEGER, 'description' => 'Number of sites to be migrated.', 'default' => 0, 'example' => 5, ]) - ->addRule(Resource::TYPE_SITE_DEPLOYMENT, [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Number of site deployments to be migrated.', - 'default' => 0, - 'example' => 5, - ]) - ->addRule(Resource::TYPE_SITE_VARIABLE, [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Number of site variables to be migrated.', - 'default' => 0, - 'example' => 10, - ]) ->addRule('size', [ 'type' => self::TYPE_INTEGER, 'description' => 'Size of files to be migrated in mb.', From ed93b77faa723759dd110a0e6d4ca625a66d4268 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 25 Feb 2026 06:44:42 +0000 Subject: [PATCH 12/28] fix: remove strict WhiteList for Appwrite migration endpoints --- app/controllers/api/migrations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 05462ae1f5..5a864ab928 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -62,7 +62,7 @@ Http::post('/v1/migrations/appwrite') ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources(), true)), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate') ->param('endpoint', '', new URL(), 'Source Appwrite endpoint') ->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject']) ->param('apiKey', '', new Text(512), 'Source API Key') @@ -708,7 +708,7 @@ Http::get('/v1/migrations/appwrite/report') ) ] )) - ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources(), true)), 'List of resources to migrate') + ->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate') ->param('endpoint', '', new URL(), "Source's Appwrite Endpoint") ->param('projectID', '', new Text(512), "Source's Project ID") ->param('key', '', new Text(512), "Source's API Key") From 754ddbef3c3960904b2ad3e773f45cff7495016c Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 25 Feb 2026 06:47:08 +0000 Subject: [PATCH 13/28] fix: remove deployment from MigrationReport model --- src/Appwrite/Utopia/Response/Model/MigrationReport.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/MigrationReport.php b/src/Appwrite/Utopia/Response/Model/MigrationReport.php index 17c1e89ce7..7ebc22d22e 100644 --- a/src/Appwrite/Utopia/Response/Model/MigrationReport.php +++ b/src/Appwrite/Utopia/Response/Model/MigrationReport.php @@ -53,12 +53,6 @@ class MigrationReport extends Model 'default' => 0, 'example' => 20, ]) - ->addRule(Resource::TYPE_DEPLOYMENT, [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Number of deployments to be migrated.', - 'default' => 0, - 'example' => 5, - ]) ->addRule(Resource::TYPE_SITE, [ 'type' => self::TYPE_INTEGER, 'description' => 'Number of sites to be migrated.', From f8929e1f2e977011d3885676f0ad1a7acb57bceb Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 25 Feb 2026 10:25:05 +0000 Subject: [PATCH 14/28] fix: use utopia-migration dev branch with deployment download fixes --- composer.json | 4 ++-- composer.lock | 32 +++++++++++++++++--------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 931e5fe862..e113b702f6 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.6.*", + "utopia-php/migration": "dev-fix-kebab-case-site", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", @@ -114,4 +114,4 @@ "tbachert/spi": true } } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 49db72a67e..ba22531b20 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": "cb2ca42cfed8603c31b75113c85a4699", + "content-hash": "bbadd08240c123309a6b7be4da296624", "packages": [ { "name": "adhocore/jwt", @@ -4518,16 +4518,16 @@ }, { "name": "utopia-php/migration", - "version": "1.6.1", + "version": "dev-fix-kebab-case-site", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "c5c7544d02d2418536d41050794050132f247d62" + "reference": "3190013af7e167258bad3ae90db7176a0d8cb31e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5c7544d02d2418536d41050794050132f247d62", - "reference": "c5c7544d02d2418536d41050794050132f247d62", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/3190013af7e167258bad3ae90db7176a0d8cb31e", + "reference": "3190013af7e167258bad3ae90db7176a0d8cb31e", "shasum": "" }, "require": { @@ -4567,9 +4567,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.6.1" + "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" }, - "time": "2026-02-17T05:49:48+00:00" + "time": "2026-02-25T09:29:21+00:00" }, { "name": "utopia-php/mongo", @@ -5439,16 +5439,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.10.2", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "b31e6b4209ecad46d2fef509dc59f1945d0e83cb" + "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b31e6b4209ecad46d2fef509dc59f1945d0e83cb", - "reference": "b31e6b4209ecad46d2fef509dc59f1945d0e83cb", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb", + "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb", "shasum": "" }, "require": { @@ -5484,9 +5484,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/1.10.2" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.1" }, - "time": "2026-02-23T04:04:15+00:00" + "time": "2026-02-25T07:15:19+00:00" }, { "name": "brianium/paratest", @@ -9044,7 +9044,9 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/migration": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -9068,5 +9070,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } From 4a0f00f7dba4c250ad6d875a33e62c35a0f2e4b9 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 25 Feb 2026 11:34:55 +0000 Subject: [PATCH 15/28] chore: update utopia-migration to 134efdc --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 5ea9f371fc..6deac61a88 100644 --- a/composer.lock +++ b/composer.lock @@ -4521,12 +4521,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "3190013af7e167258bad3ae90db7176a0d8cb31e" + "reference": "134efdc72bf2fed59d98c07eb8e6a2e6fee64e67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/3190013af7e167258bad3ae90db7176a0d8cb31e", - "reference": "3190013af7e167258bad3ae90db7176a0d8cb31e", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/134efdc72bf2fed59d98c07eb8e6a2e6fee64e67", + "reference": "134efdc72bf2fed59d98c07eb8e6a2e6fee64e67", "shasum": "" }, "require": { @@ -4568,7 +4568,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" }, - "time": "2026-02-25T09:29:21+00:00" + "time": "2026-02-25T11:33:23+00:00" }, { "name": "utopia-php/mongo", From 88ce622e4a2c2d523c9273584ff87704f42258c5 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 25 Feb 2026 12:05:59 +0000 Subject: [PATCH 16/28] chore: update utopia-php/migration to 1.6.2 --- composer.json | 4 ++-- composer.lock | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index e113b702f6..931e5fe862 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "dev-fix-kebab-case-site", + "utopia-php/migration": "1.6.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", @@ -114,4 +114,4 @@ "tbachert/spi": true } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 6deac61a88..2c087e244b 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": "bbadd08240c123309a6b7be4da296624", + "content-hash": "cb2ca42cfed8603c31b75113c85a4699", "packages": [ { "name": "adhocore/jwt", @@ -4517,16 +4517,16 @@ }, { "name": "utopia-php/migration", - "version": "dev-fix-kebab-case-site", + "version": "1.6.2", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "134efdc72bf2fed59d98c07eb8e6a2e6fee64e67" + "reference": "037bf4b3813d44f1b0990bc124e35b501ed27fca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/134efdc72bf2fed59d98c07eb8e6a2e6fee64e67", - "reference": "134efdc72bf2fed59d98c07eb8e6a2e6fee64e67", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/037bf4b3813d44f1b0990bc124e35b501ed27fca", + "reference": "037bf4b3813d44f1b0990bc124e35b501ed27fca", "shasum": "" }, "require": { @@ -4566,9 +4566,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/fix-kebab-case-site" + "source": "https://github.com/utopia-php/migration/tree/1.6.2" }, - "time": "2026-02-25T11:33:23+00:00" + "time": "2026-02-25T12:00:11+00:00" }, { "name": "utopia-php/mongo", @@ -9043,9 +9043,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/migration": 20 - }, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { From 5c2b1be581dea368f4f38dbc7772ca0dcdd6a7f7 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 27 Feb 2026 09:04:18 +0000 Subject: [PATCH 17/28] chore: update sdk-generator to 1.11.3 --- composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index acfe009ae2..a025627279 100644 --- a/composer.lock +++ b/composer.lock @@ -5438,16 +5438,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.1", + "version": "1.11.3", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb" + "reference": "45d22c0107a53bb9a0a4e39db0e738d461631d11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb", - "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/45d22c0107a53bb9a0a4e39db0e738d461631d11", + "reference": "45d22c0107a53bb9a0a4e39db0e738d461631d11", "shasum": "" }, "require": { @@ -5483,9 +5483,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/1.11.1" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.3" }, - "time": "2026-02-25T07:15:19+00:00" + "time": "2026-02-27T06:54:59+00:00" }, { "name": "brianium/paratest", @@ -9067,5 +9067,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } From 3d4f37c482b53e428426437c30f5ad1fe88f0028 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 2 Mar 2026 14:12:13 +0530 Subject: [PATCH 18/28] refactor: improve channel handling for Realtime events and update tests for TablesDB compatibility --- src/Appwrite/Event/Event.php | 2 +- src/Appwrite/Messaging/Adapter/Realtime.php | 25 +- .../Realtime/RealtimeCustomClientTest.php | 464 ++++++++++++++++-- 3 files changed, 434 insertions(+), 57 deletions(-) diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 53c071573e..731bf8b404 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -725,7 +725,7 @@ class Event $events = $pairedEvents; } // mirrored events can have duplicates in case of smaller events - return array_unique($events); + return array_values(array_unique($events)); } /** diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 29ea0ff1b6..c82be25d03 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -510,25 +510,13 @@ class Realtime extends MessagingAdapter $collectionId = $payload->getAttribute('$collectionId', ''); $resourceId = $tableId ?: $collectionId; $channels = []; - // backward compat(tablesdb will have databases channels + tablesdb prefixed channels) - if ($parts[0] === 'databases' || $parts[0] === 'tablesdb') { - $prefix = 'databases'; - $channels = self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), $prefix); - - $channels = array_unique([ - ...$channels, - ...self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), $prefix) - ]); - } - - // prefixed channels -> tablesdb - if ($parts[0] !== 'databases') { - $channels = array_unique([ - ...$channels, - ...self::getDatabaseChannels($parts[0], $database->getId(), $resourceId, $payload->getId()), - ]); - } + // sending legacy + tablesdb events to both legacy and tablesdb + $channels = array_merge( + self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'), + self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'), + self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId()) + ); $roles = $collection->getAttribute('documentSecurity', false) ? \array_merge($collection->getRead(), $payload->getRead()) @@ -620,6 +608,7 @@ class Realtime extends MessagingAdapter $channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents"; $channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents.{$payloadId}"; break; + case 'tablesdb': $channels[] = 'rows'; $channels[] = "{$basePrefix}.{$databaseId}.tables.{$resourceId}.rows"; diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index ddd620f27b..d3f0486c35 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -123,6 +123,8 @@ class RealtimeCustomClientTest extends Scope $this->assertNotEmpty($response['data']); $this->assertNotEmpty($response['data']['user']); $this->assertCount(16, $response['data']['channels']); + $this->assertIsList($response['data']['channels']); + $this->assertTrue(array_is_list($response['data']['channels'])); $this->assertContains('account', $response['data']['channels']); $this->assertContains('account.' . $userId, $response['data']['channels']); $this->assertContains('files', $response['data']['channels']); @@ -818,7 +820,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']); $this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']); $this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']); @@ -863,7 +865,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']); @@ -919,7 +921,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']); @@ -975,7 +977,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']); @@ -1007,7 +1009,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']); @@ -1056,7 +1058,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']); @@ -1084,7 +1086,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']); @@ -1112,7 +1114,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']); @@ -1149,7 +1151,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']); @@ -1178,7 +1180,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']); @@ -1207,7 +1209,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']); @@ -1254,7 +1256,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); @@ -1771,7 +1773,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); @@ -1809,7 +1811,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']); @@ -1951,7 +1953,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']); @@ -1990,7 +1992,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']); @@ -2040,7 +2042,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('timestamp', $response['data']); - $this->assertCount(6, $response['data']['channels']); + $this->assertCount(8, $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']); @@ -2565,17 +2567,66 @@ class RealtimeCustomClientTest extends Scope $session = $user['session'] ?? ''; $projectId = $this->getProject()['$id']; + /** + * Create a shared TablesDB database using the /tablesdb API. + * This database will then be accessed via both /databases and /tablesdb routes. + */ $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'x-appwrite-key' => $this->getProject()['apiKey'], - ], $this->getHeaders()), [ + ]), [ 'databaseId' => ID::unique(), - 'name' => 'TablesDB Realtime DB', + 'name' => 'TablesDB Cross API Realtime DB', ]); $databaseId = $database['body']['$id']; + $this->assertEquals(201, $database['headers']['status-code']); + + /** + * Legacy collection in the shared database (/databases API). + */ + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'Legacy Actors', + 'permissions' => [ + Permission::create(Role::user($user['$id'])), + ], + 'documentSecurity' => true, + ]); + + $collectionId = $collection['body']['$id']; + + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code']); + + $this->assertEventually(function () use ($databaseId, $collectionId) { + $attribute = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals('available', $attribute['body']['status']); + }, 30000, 250); + + /** + * TablesDB table in the same database (/tablesdb API). + */ $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, @@ -2616,20 +2667,92 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('available', $column['body']['status']); }, 120000, 500); - $client = $this->getWebsocket(['documents', 'collections'], [ + /** + * Two different clients subscribing via legacy (documents/collections) + * and new (rows/tables) channels. + */ + $clientLegacy = $this->getWebsocket(['documents', 'collections'], [ 'origin' => 'http://localhost', 'cookie' => 'a_session_' . $projectId . '=' . $session, ]); - $response = json_decode($client->receive(), true); + $response = json_decode($clientLegacy->receive(), true); $this->assertArrayHasKey('type', $response); $this->assertArrayHasKey('data', $response); $this->assertEquals('connected', $response['type']); $this->assertNotEmpty($response['data']); - $this->assertCount(2, $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']); + $clientTables = $this->getWebsocket(['rows', 'tables'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session, + ]); + + $response = json_decode($clientTables->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertArrayHasKey('data', $response); + $this->assertEquals('connected', $response['type']); + $this->assertNotEmpty($response['data']); + $this->assertContains('rows', $response['data']['channels']); + + /** + * 1) Operation via legacy /databases API (document create). + * Both clients should receive an event that includes both document- + * style and row-style channels on the shared database. + */ + $documentId = ID::unique(); + + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'documentId' => $documentId, + 'data' => [ + 'name' => 'Legacy Chris Evans', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $document['headers']['status-code']); + + $legacyEventForLegacyClient = json_decode($clientLegacy->receive(), true); + $legacyEventForTablesClient = json_decode($clientTables->receive(), true); + + foreach ([$legacyEventForLegacyClient, $legacyEventForTablesClient] as $event) { + $this->assertArrayHasKey('type', $event); + $this->assertArrayHasKey('data', $event); + $this->assertEquals('event', $event['type']); + $this->assertNotEmpty($event['data']); + $this->assertArrayHasKey('timestamp', $event['data']); + + $channels = $event['data']['channels']; + + // Legacy-style channels + $this->assertContains('documents', $channels); + $this->assertContains("databases.{$databaseId}.collections.{$collectionId}.documents", $channels); + $this->assertContains("databases.{$databaseId}.collections.{$collectionId}.documents.{$documentId}", $channels); + + // New rows-style channels mirrored for legacy API + $this->assertContains('rows', $channels); + $this->assertContains("databases.{$databaseId}.tables.{$collectionId}.rows", $channels); + $this->assertContains("databases.{$databaseId}.tables.{$collectionId}.rows.{$documentId}", $channels); + + // TablesDB-prefixed channels should also be present for a tablesdb database + $this->assertContains("tablesdb.{$databaseId}.tables.{$collectionId}.rows", $channels); + $this->assertContains("tablesdb.{$databaseId}.tables.{$collectionId}.rows.{$documentId}", $channels); + } + + /** + * 2) Operation via /tablesdb API (row create). + * Both clients should again receive an event that now also includes + * the tablesdb-prefixed channels alongside the databases-prefixed ones. + */ $rowId = ID::unique(); $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ @@ -2650,31 +2773,296 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals(201, $row['headers']['status-code']); + $tablesEventForLegacyClient = json_decode($clientLegacy->receive(), true); + $tablesEventForTablesClient = json_decode($clientTables->receive(), true); + + foreach ([$tablesEventForLegacyClient, $tablesEventForTablesClient] as $event) { + $this->assertArrayHasKey('type', $event); + $this->assertArrayHasKey('data', $event); + $this->assertEquals('event', $event['type']); + $this->assertNotEmpty($event['data']); + $this->assertArrayHasKey('timestamp', $event['data']); + + $channels = $event['data']['channels']; + + // Core tablesdb row channels + $this->assertContains('rows', $channels); + $this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows", $channels); + $this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels); + + // Collections/legacy-style compatibility channels + $this->assertContains('documents', $channels); + $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows", $channels); + $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels); + $this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents", $channels); + $this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents.{$rowId}", $channels); + + $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}.create", $event['data']['events']); + $this->assertNotEmpty($event['data']['payload']); + $this->assertEquals('Chris Evans', $event['data']['payload']['name']); + } + + /** + * 3) Legacy database accessed via /tablesdb routes. + * A database created via /databases but operated on via /tablesdb + * should also expose both legacy and tablesdb-prefixed channels. + */ + $legacyDatabase = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Legacy DB via TablesDB Route', + ]); + + $this->assertEquals(201, $legacyDatabase['headers']['status-code']); + + $legacyDatabaseId = $legacyDatabase['body']['$id']; + + $legacyTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $legacyDatabaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'tableId' => ID::unique(), + 'name' => 'Legacy Actors', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $legacyTableId = $legacyTable['body']['$id']; + + $legacyColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $legacyDatabaseId . '/tables/' . $legacyTableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $legacyColumn['headers']['status-code']); + + $this->assertEventually(function () use ($legacyDatabaseId, $legacyTableId) { + $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $legacyDatabaseId . '/tables/' . $legacyTableId . '/columns/name', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders())); + + $this->assertEquals(200, $column['headers']['status-code']); + $this->assertEquals('available', $column['body']['status']); + }, 120000, 500); + + $legacyRowId = ID::unique(); + + $legacyRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $legacyDatabaseId . '/tables/' . $legacyTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'rowId' => $legacyRowId, + 'data' => [ + 'name' => 'Legacy Tables Route', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $legacyRow['headers']['status-code']); + + $legacyTablesEventForLegacyClient = json_decode($clientLegacy->receive(), true); + $legacyTablesEventForTablesClient = json_decode($clientTables->receive(), true); + + foreach ([$legacyTablesEventForLegacyClient, $legacyTablesEventForTablesClient] as $event) { + $this->assertArrayHasKey('type', $event); + $this->assertArrayHasKey('data', $event); + $this->assertEquals('event', $event['type']); + $this->assertNotEmpty($event['data']); + $this->assertArrayHasKey('timestamp', $event['data']); + + $channels = $event['data']['channels']; + $events = $event['data']['events']; + $this->assertIsList($channels); + $this->assertIsList($events); + + // Core tablesdb row channels for legacy db accessed via tablesdb + $this->assertContains('rows', $channels); + $this->assertContains("tablesdb.{$legacyDatabaseId}.tables.{$legacyTableId}.rows", $channels); + $this->assertContains("tablesdb.{$legacyDatabaseId}.tables.{$legacyTableId}.rows.{$legacyRowId}", $channels); + + // Legacy compatibility channels must also exist + $this->assertContains('documents', $channels); + $this->assertContains("databases.{$legacyDatabaseId}.tables.{$legacyTableId}.rows", $channels); + $this->assertContains("databases.{$legacyDatabaseId}.tables.{$legacyTableId}.rows.{$legacyRowId}", $channels); + $this->assertContains("databases.{$legacyDatabaseId}.collections.{$legacyTableId}.documents", $channels); + $this->assertContains("databases.{$legacyDatabaseId}.collections.{$legacyTableId}.documents.{$legacyRowId}", $channels); + } + + $clientLegacy->close(); + $clientTables->close(); + } + + public function testChannelTablesDBRowUpdate() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + /** + * Create a tablesdb database + table + column + row. + */ + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'databaseId' => ID::unique(), + 'name' => 'Row Update DB', + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'tableId' => ID::unique(), + 'name' => 'Assembly', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + $tableId = $table['body']['$id']; + + $column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $column['headers']['status-code']); + + $this->assertEventually(function () use ($databaseId, $tableId) { + $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders())); + + $this->assertEquals(200, $column['headers']['status-code']); + $this->assertEquals('available', $column['body']['status']); + }, 120000, 500); + + // Seed a row so we can listen to its update + $rowId = ID::unique(); + + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'rowId' => $rowId, + 'data' => [ + 'name' => 'Initial Name', + ], + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $row['headers']['status-code']); + + /** + * Subscribe to a specific row channel using both legacy and tablesdb-style prefixes. + * This mimics a client subscribing to a concrete "resource" channel and + * expecting a single event list for updates. + */ + $client = $this->getWebsocket([ + "databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}", + "tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}", + ], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $projectId . '=' . $session, + ]); + $response = json_decode($client->receive(), true); $this->assertArrayHasKey('type', $response); $this->assertArrayHasKey('data', $response); - $this->assertEquals('event', $response['type']); + $this->assertEquals('connected', $response['type']); $this->assertNotEmpty($response['data']); - $this->assertArrayHasKey('timestamp', $response['data']); + $this->assertIsList($response['data']['channels']); - // Core channels for tablesdb row events - $this->assertContains('rows', $response['data']['channels']); + /** + * Trigger a row update via the dedicated /tablesdb row update endpoint. + * Event label: databases.[databaseId].tables.[tableId].rows.[rowId].update + * Our Event + Realtime logic should enrich this to include: + * - databases.{dbId}.tables.{tableId}.rows.{rowId}.update + * - tablesdb.{dbId}.tables.{tableId}.rows.{rowId}.update + */ + $update = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders()), [ + 'data' => [ + 'name' => 'Updated Name', + ], + ]); - $this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows", $response['data']['channels']); - $this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $response['data']['channels']); + $this->assertEquals(200, $update['headers']['status-code']); - // Collections-style compatibility channels - $this->assertContains('documents', $response['data']['channels']); - $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows", $response['data']['channels']); - $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $response['data']['channels']); - $this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents", $response['data']['channels']); - $this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents.{$rowId}", $response['data']['channels']); + $event = json_decode($client->receive(), true); - // Primary event should still be present - $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}.create", $response['data']['events']); - $this->assertNotEmpty($response['data']['payload']); - $this->assertEquals('Chris Evans', $response['data']['payload']['name']); + $this->assertArrayHasKey('type', $event); + $this->assertArrayHasKey('data', $event); + $this->assertEquals('event', $event['type']); + $this->assertNotEmpty($event['data']); + $this->assertArrayHasKey('timestamp', $event['data']); + + $channels = $event['data']['channels']; + $events = $event['data']['events']; + + // Ensure channels and events are list-type arrays + $this->assertIsList($channels); + $this->assertIsList($events); + + // Legacy + tablesdb row channels must be present + $this->assertContains('rows', $channels); + $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows", $channels); + $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels); + $this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows", $channels); + $this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels); + + // Both databases.* and tablesdb.* update events should be emitted + $this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}.update", $events); + $this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}.update", $events); + + $this->assertNotEmpty($event['data']['payload']); + $this->assertEquals('Updated Name', $event['data']['payload']['name']); $client->close(); } From 82a5d138e0c895bb345b66909b022298b8c9033e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 2 Mar 2026 14:22:44 +0530 Subject: [PATCH 19/28] refactor: enhance event handling by ensuring unique events and improving channel merging in Realtime adapter --- src/Appwrite/Event/Event.php | 2 ++ src/Appwrite/Messaging/Adapter/Realtime.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 731bf8b404..ba633b4478 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -725,6 +725,8 @@ class Event $events = $pairedEvents; } // mirrored events can have duplicates in case of smaller events + // array unique can turns list to hasmap in case duplicates present + // so forcing array value will turn this to array list always return array_values(array_unique($events)); } diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index c82be25d03..3bc47c3c2a 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -512,11 +512,11 @@ class Realtime extends MessagingAdapter $channels = []; // sending legacy + tablesdb events to both legacy and tablesdb - $channels = array_merge( + $channels = array_values(array_merge( self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'), self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'), self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId()) - ); + )); $roles = $collection->getAttribute('documentSecurity', false) ? \array_merge($collection->getRead(), $payload->getRead()) From 05a2f56945f3343ba729f1f0ec0cb1c0b06f95b7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 2 Mar 2026 14:27:54 +0530 Subject: [PATCH 20/28] updated tests --- src/Appwrite/Messaging/Adapter/Realtime.php | 4 ++-- .../Realtime/RealtimeCustomClientTest.php | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 3bc47c3c2a..85ae4fde25 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -512,11 +512,11 @@ class Realtime extends MessagingAdapter $channels = []; // sending legacy + tablesdb events to both legacy and tablesdb - $channels = array_values(array_merge( + $channels = array_values(array_unique(array_merge( self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'), self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'), self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId()) - )); + ))); $roles = $collection->getAttribute('documentSecurity', false) ? \array_merge($collection->getRead(), $payload->getRead()) diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index d3f0486c35..b94e382fde 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -1435,7 +1435,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response1['type']); $this->assertNotEmpty($response1['data']); $this->assertArrayHasKey('timestamp', $response1['data']); - $this->assertCount(6, $response1['data']['channels']); + $this->assertCount(8, $response1['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.create", $response1['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.create", $response1['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response1['data']['events']); @@ -1466,7 +1466,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response2['type']); $this->assertNotEmpty($response2['data']); $this->assertArrayHasKey('timestamp', $response2['data']); - $this->assertCount(6, $response2['data']['channels']); + $this->assertCount(8, $response2['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.create", $response2['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.create", $response2['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response2['data']['events']); @@ -1516,7 +1516,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response1['type']); $this->assertNotEmpty($response1['data']); $this->assertArrayHasKey('timestamp', $response1['data']); - $this->assertCount(6, $response1['data']['channels']); + $this->assertCount(8, $response1['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']); @@ -1570,7 +1570,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response2['type']); $this->assertNotEmpty($response2['data']); $this->assertArrayHasKey('timestamp', $response2['data']); - $this->assertCount(6, $response2['data']['channels']); + $this->assertCount(8, $response2['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']); @@ -1623,7 +1623,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response1['type']); $this->assertNotEmpty($response1['data']); $this->assertArrayHasKey('timestamp', $response1['data']); - $this->assertCount(6, $response1['data']['channels']); + $this->assertCount(8, $response1['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']); @@ -1650,7 +1650,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response2['type']); $this->assertNotEmpty($response2['data']); $this->assertArrayHasKey('timestamp', $response2['data']); - $this->assertCount(6, $response2['data']['channels']); + $this->assertCount(8, $response2['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']); @@ -1689,7 +1689,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response1['type']); $this->assertNotEmpty($response1['data']); $this->assertArrayHasKey('timestamp', $response1['data']); - $this->assertCount(6, $response1['data']['channels']); + $this->assertCount(8, $response1['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.delete", $response1['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.delete", $response1['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response1['data']['events']); @@ -1720,7 +1720,7 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals('event', $response2['type']); $this->assertNotEmpty($response2['data']); $this->assertArrayHasKey('timestamp', $response2['data']); - $this->assertCount(6, $response2['data']['channels']); + $this->assertCount(8, $response2['data']['channels']); $this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.delete", $response2['data']['events']); $this->assertContains("databases.*.collections.*.documents.*.delete", $response2['data']['events']); $this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response2['data']['events']); From cb260fa3d8181ba25e0bc1c04ccac6807cadcc69 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:48:08 +0530 Subject: [PATCH 21/28] Add authorized field to VCS repository model (#11420) * Add authorized field to VCS repository model * use new API --- composer.lock | 12 ++++++------ .../VCS/Http/Installations/Repositories/Create.php | 2 ++ .../VCS/Http/Installations/Repositories/Get.php | 12 ++++++++++++ .../VCS/Http/Installations/Repositories/XList.php | 2 ++ .../Utopia/Response/Model/ProviderRepository.php | 12 ++++++++++++ 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index a025627279..ccf03ce835 100644 --- a/composer.lock +++ b/composer.lock @@ -5215,16 +5215,16 @@ }, { "name": "utopia-php/vcs", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14" + "reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14", - "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/92a1650824ba0c5e6a1bc46e622ac87c50a08920", + "reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920", "shasum": "" }, "require": { @@ -5258,9 +5258,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/2.0.0" + "source": "https://github.com/utopia-php/vcs/tree/2.0.1" }, - "time": "2026-02-25T11:36:45+00:00" + "time": "2026-02-27T12:18:49+00:00" }, { "name": "utopia-php/websocket", diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php index e357d1d5fa..bba062a730 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php @@ -152,6 +152,8 @@ class Create extends Action $repository['pushedAt'] = $repository['pushed_at'] ?? ''; $repository['organization'] = $installation->getAttribute('organization', ''); $repository['provider'] = $installation->getAttribute('provider', ''); + $repository['providerInstallationId'] = $installation->getAttribute('providerInstallationId', ''); + $repository['authorized'] = true; $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php index 4dee84752b..9e32ca8276 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php @@ -85,11 +85,23 @@ class Get extends Action $repository = $github->getRepository($owner, $repositoryName); + $authorized = false; + try { + $installationRepository = $github->getInstallationRepository($repositoryName); + if (!empty($installationRepository)) { + $authorized = true; + } + } catch (RepositoryNotFound $e) { + $authorized = false; + } + $repository['id'] = \strval($repository['id']) ?? ''; $repository['pushedAt'] = $repository['pushed_at'] ?? ''; $repository['organization'] = $installation->getAttribute('organization', ''); $repository['provider'] = $installation->getAttribute('provider', ''); $repository['defaultBranch'] = $repository['default_branch'] ?? ''; + $repository['authorized'] = $authorized; + $repository['providerInstallationId'] = $providerInstallationId; $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php index 2cb1a996a4..53713f8407 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php @@ -148,6 +148,8 @@ class XList extends Action $repo['pushedAt'] = $repo['pushed_at'] ?? null; $repo['provider'] = $installation->getAttribute('provider', '') ?? ''; $repo['organization'] = $installation->getAttribute('organization', '') ?? ''; + $repo['providerInstallationId'] = $installation->getAttribute('providerInstallationId', ''); + $repo['authorized'] = true; return $repo; }, $repos); diff --git a/src/Appwrite/Utopia/Response/Model/ProviderRepository.php b/src/Appwrite/Utopia/Response/Model/ProviderRepository.php index ddad0b059c..4dd9789872 100644 --- a/src/Appwrite/Utopia/Response/Model/ProviderRepository.php +++ b/src/Appwrite/Utopia/Response/Model/ProviderRepository.php @@ -47,6 +47,18 @@ class ProviderRepository extends Model 'default' => '', 'example' => 'main', ]) + ->addRule('providerInstallationId', [ + 'type' => self::TYPE_STRING, + 'description' => 'VCS (Version Control System) installation ID.', + 'default' => '', + 'example' => '108104697', + ]) + ->addRule('authorized', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Is VCS (Version Control System) repository authorized for the installation?', + 'default' => false, + 'example' => true, + ]) ->addRule('pushedAt', [ 'type' => self::TYPE_DATETIME, 'description' => 'Last commit date in ISO 8601 format.', From 164c372b219ead11daecc0ff7de22aee250bc074 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:10:58 +0530 Subject: [PATCH 22/28] Per-project permissions for installations (#11423) * Per-project permissions for installations * lint --- .../Modules/VCS/Http/GitHub/Callback/Get.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php index 1212c06a72..cb45efbf39 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php @@ -4,13 +4,12 @@ namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Callback; use Appwrite\Auth\OAuth2\Github as OAuth2Github; use Appwrite\Extend\Exception; +use Appwrite\Platform\Permission as AppwritePermission; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; -use Utopia\Database\Helpers\Permission; -use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -21,6 +20,7 @@ use Utopia\VCS\Adapter\Git\GitHub; class Get extends Action { use HTTP; + use AppwritePermission; public static function getName() { @@ -132,13 +132,7 @@ class Get extends Action $installation = new Document([ '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - Permission::update(Role::team(ID::custom($teamId), 'owner')), - Permission::update(Role::team(ID::custom($teamId), 'developer')), - Permission::delete(Role::team(ID::custom($teamId), 'owner')), - Permission::delete(Role::team(ID::custom($teamId), 'developer')), - ], + '$permissions' => $this->getPermissions($teamId, $projectId), 'providerInstallationId' => $providerInstallationId, 'projectId' => $projectId, 'projectInternalId' => $projectInternalId, From e7424e70aa9eeed4c75689decf1027c0f5184e56 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 2 Mar 2026 23:14:08 +0530 Subject: [PATCH 23/28] chore: improve sdk diff check --- src/Appwrite/Platform/Tasks/SDKs.php | 79 ++++++++++++++++++---------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index a480f4cb4f..1ec23d5f16 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -486,9 +486,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $useAi = ($ai !== 'no'); $apiKey = $useAi ? System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', '') : ''; $aiChangelog = ''; // Track AI-generated changelog for PR description - Console::info('Checking for _APP_ASSISTANT_OPENAI_API_KEY... [' . (! empty($apiKey) ? 'FOUND' : 'NOT FOUND') . ']'); + if (! empty($apiKey) && ! $examplesOnly) { - Console::info("Using AI to determine version bump and changelog for {$language['name']} SDK..."); + Console::info("Analyzing SDK changes with AI..."); $aiResult = $this->generateVersionAndChangelog($language, $result); if ($aiResult !== null) { @@ -502,6 +502,12 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Update the changelog file $this->updateChangelogFile($language['changelog'], $newVersion, $newChangelog); + // Also update CHANGELOG.md in the generated SDK directory + $sdkChangelogPath = $result . '/CHANGELOG.md'; + if (file_exists($sdkChangelogPath)) { + $this->updateChangelogFile($sdkChangelogPath, $newVersion, $newChangelog); + } + // Reload the language config with updated values $language['version'] = $newVersion; @@ -512,10 +518,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } catch (\Throwable $exception) { Console::error($exception->getMessage()); } - - Console::success("AI determined version: {$newVersion} ({$aiResult['versionBump']} bump)"); } else { - Console::warning('AI version generation failed, using existing version'); + Console::warning('AI analysis failed, using existing version'); } } @@ -524,33 +528,45 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $repoBranch = $language['repoBranch'] ?? 'main'; if ($git && ! empty($gitUrl)) { + Console::info("Preparing {$language['name']} SDK repository..."); + \exec('rm -rf ' . $target . ' && \ mkdir -p ' . $target . ' && \ cd ' . $target . ' && \ - git init && \ + git init --quiet && \ git config core.ignorecase false && \ git config pull.rebase false && \ + git config advice.defaultBranchName false && \ git remote add origin ' . $gitUrl . ' && \ - git fetch origin && \ + git fetch origin --quiet --no-tags --depth 1 ' . $repoBranch . ' 2>&1 | grep -v "^remote:" | grep -v "^From " | grep -v "^ \* " || true && \ (git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \ - git pull origin ' . $repoBranch . ' && \ + git pull origin ' . $repoBranch . ' --quiet --no-tags 2>&1 | grep -v "^From " | grep -v "^ \* " || true && \ (git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \ - (git fetch origin ' . $gitBranch . ' 2>/dev/null || git push -u origin ' . $gitBranch . ') && \ + (git fetch origin ' . $gitBranch . ' --quiet --no-tags --depth 1 2>/dev/null || git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -v "^remote:" || true) && \ git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \ - (test -d .github && cp -r .github /tmp/.github-backup-$$ || true) && \ - git rm -rf --cached . && \ - git clean -fdx -e .git -e .github && \ + (if [ -d .github ]; then cp -r .github /tmp/.github-backup-$$ 2>/dev/null; fi) && \ + git rm -rf --cached . 2>/dev/null && \ + git clean -fdx -e .git -e .github 2>/dev/null && \ cp -r ' . $result . '/. ' . $target . '/ && \ - (test -d /tmp/.github-backup-$$ && cp -rn /tmp/.github-backup-$$/.github . && rm -rf /tmp/.github-backup-$$ || true) && \ + (if [ -d /tmp/.github-backup-$$/.github ]; then cp -rn /tmp/.github-backup-$$/.github . 2>/dev/null && rm -rf /tmp/.github-backup-$$; fi) && \ git add -A && \ - git commit -m "' . $message . '" && \ - git push -u origin ' . $gitBranch . ' - '); + git commit -m "' . $message . '" --quiet && \ + git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true + ', $gitOutput, $gitReturnCode); + + if ($gitReturnCode !== 0) { + Console::warning("Git operations completed with warnings (exit code: {$gitReturnCode})"); + } Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); if ($git) { $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; - $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']} . "; + + // Build PR body with AI changelog if available + $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}."; + if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $prBody .= "\n\n## Changes\n\n{$aiChangelog}"; + } $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; Console::info("Creating pull request for {$language['name']} SDK..."); @@ -778,10 +794,12 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - `patch`: backward-compatible fixes or small improvements. Changelog rules: - - Include only user-facing SDK changes. - - Exclude internal/project-infra changes (for example `.github/workflows/**`, `.github/ISSUE_TEMPLATE/**`, CI/release automation/template cleanup). - - Never add "Internal housekeeping" style entries. - - If only excluded changes exist, return exactly: `* No user-facing SDK changes.` + - Keep entries brief and direct (15 words or less each) + - USER-FACING: Source code changes, commands/params, docs, examples, scripts + - EXCLUDE: .github/, CI configs, internal tooling + - Format: "Breaking: Removed X" or "Added Y feature" or "Fixed Z bug" + - One change per bullet, avoid combining multiple changes + - If no user-facing changes: `* No user-facing SDK changes.` Diff context: - Stats: {{diff_stats}} @@ -805,11 +823,13 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ->setExcludePaths([ '.github/workflows/**', '.github/ISSUE_TEMPLATE/**', + '.git/**', ]) ->setMaxDiffLines(500) ->setUserId('sdk-analyst'); Console::info("Running DiffCheck for {$language['name']} SDK..."); + $result = (new DiffCheck())->run( runner: $adapter, base: DiffCheckRepository::remote($gitUrl, $repoBranch), @@ -819,7 +839,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ); if (!$result['hasChanges']) { - Console::warning("No changes detected for {$language['name']} SDK"); + Console::info("✓ No changes detected - SDK is up to date"); return null; } @@ -831,15 +851,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND return null; } - Console::log('AI raw response:'); - Console::log($responseContent); - Console::log('--- End of AI response ---'); - $parsed = json_decode($responseContent, true); if (json_last_error() !== JSON_ERROR_NONE) { Console::warning('Failed to parse AI response as JSON: ' . json_last_error_msg()); - Console::log('Raw response that failed to parse:'); + Console::log('Raw response:'); Console::log($responseContent); return null; @@ -850,7 +866,14 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND return null; } - Console::info("AI analysis complete - Version bump: {$parsed['versionBump']}, New version: {$parsed['version']}"); + Console::success("✓ Analysis complete"); + Console::log(" Version: {$language['version']} → {$parsed['version']} ({$parsed['versionBump']} bump)"); + Console::log(" Changelog:"); + foreach (explode("\n", $parsed['changelog']) as $line) { + if (trim($line)) { + Console::log(" {$line}"); + } + } return [ 'version' => $parsed['version'], From e31843be4bcf88fe279df3141b4ca7fd29fcb92b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 2 Mar 2026 23:20:29 +0530 Subject: [PATCH 24/28] change --- src/Appwrite/Platform/Tasks/SDKs.php | 50 ++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 1ec23d5f16..0bdfd44e49 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -887,6 +887,16 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } } + /** + * Get the SDK config file path + * + * @return string Path to the SDK config file + */ + protected function getSdkConfigPath(): string + { + return __DIR__ . '/../../../../app/config/sdks.php'; + } + /** * Update SDK version in the config file * @@ -897,7 +907,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND */ private function updateSdkVersion(string $platform, string $sdkKey, string $newVersion): bool { - $configPath = __DIR__ . '/../../../../app/config/sdks.php'; + $configPath = $this->getSdkConfigPath(); if (! file_exists($configPath)) { Console::error("Config file not found: {$configPath}"); @@ -907,13 +917,13 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $content = file_get_contents($configPath); - // Find and replace the version for this specific SDK - // Pattern matches the version line in the SDK array - $pattern = '/(\[\s*[\'"]key[\'"]\s*=>\s*[\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*,[\s\S]*?[\'"]version[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"])/m'; + // First, try to find inline version in SDK array (pattern 1) + // Pattern matches: ['key' => 'nodejs', ... 'version' => '22.1.2'] + $inlinePattern = '/(\[\s*[\'"]key[\'"]\s*=>\s*[\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*,[\s\S]*?[\'"]version[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"])/m'; - if (preg_match($pattern, $content, $matches)) { + if (preg_match($inlinePattern, $content, $matches)) { $oldVersion = $matches[2]; - $newContent = preg_replace($pattern, '${1}' . $newVersion . '${3}', $content); + $newContent = preg_replace($inlinePattern, '${1}' . $newVersion . '${3}', $content); if (file_put_contents($configPath, $newContent) !== false) { Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); @@ -924,11 +934,31 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND return false; } - } else { - Console::warning("Could not find version entry for {$sdkKey} in config"); - - return false; } + + // Second, try to find version in array format (pattern 2) + // Pattern matches: 'nodejs' => '22.1.2', or "nodejs" => "22.1.2", + // Also handles extra whitespace: 'nodejs' => '22.1.2', + $arrayPattern = '/([\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"],)/m'; + + if (preg_match($arrayPattern, $content, $matches)) { + $oldVersion = $matches[2]; + $newContent = preg_replace($arrayPattern, '${1}' . $newVersion . '${3}', $content); + + if (file_put_contents($configPath, $newContent) !== false) { + Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); + + return true; + } else { + Console::error('Failed to write config file'); + + return false; + } + } + + Console::warning("Could not find version entry for {$sdkKey} in config"); + + return false; } /** From 94bd9661b3285af3dfb8578d4cac202821821bc0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 2 Mar 2026 23:24:48 +0530 Subject: [PATCH 25/28] change --- src/Appwrite/Platform/Tasks/SDKs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 0bdfd44e49..34fa0b5d2a 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -798,7 +798,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - USER-FACING: Source code changes, commands/params, docs, examples, scripts - EXCLUDE: .github/, CI configs, internal tooling - Format: "Breaking: Removed X" or "Added Y feature" or "Fixed Z bug" - - One change per bullet, avoid combining multiple changes + - Group related changes together (e.g., "Added A, B, and C options" not separate bullets) - If no user-facing changes: `* No user-facing SDK changes.` Diff context: From f41c19ed3eb34352fb87cf6a34416a3a031fa9a4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 2 Mar 2026 23:26:48 +0530 Subject: [PATCH 26/28] improve prompt --- src/Appwrite/Platform/Tasks/SDKs.php | 83 +++++++++++++++++----------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 34fa0b5d2a..0e37940a59 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -781,38 +781,57 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ); $prompt = <<", + "changelog": [ + "", + "" + ] + } + PROMPT; $options = (new DiffCheckOptions()) ->setSchema($schema) From feedd0eb4a5860d85ac138cce7f56ea74849ae4b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 2 Mar 2026 23:30:47 +0530 Subject: [PATCH 27/28] improve prompt --- src/Appwrite/Platform/Tasks/SDKs.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 0e37940a59..949dbb3e6b 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -821,16 +821,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ```diff {{diff}} ``` - - Respond with only valid JSON in this exact structure — no markdown fencing, no commentary: - { - "bump": "major" | "minor" | "patch", - "version": "", - "changelog": [ - "", - "" - ] - } PROMPT; $options = (new DiffCheckOptions()) From 598c71fb118280bf3d0c0e4ef2889fd44d181ab8 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:15:09 +0530 Subject: [PATCH 28/28] Move VCS events APIs to Modules (#11403) * Move VCS events APIs to Modules * trait * lint * fix external flow * fix overriden vars * feedback --- app/config/services.php | 2 +- app/controllers/api/vcs.php | 705 ------------------ .../Http/GitHub/Authorize/External/Update.php | 141 ++++ .../Modules/VCS/Http/GitHub/Deployment.php | 523 +++++++++++++ .../Modules/VCS/Http/GitHub/Events/Create.php | 244 ++++++ .../Platform/Modules/VCS/Services/Http.php | 6 + 6 files changed, 915 insertions(+), 706 deletions(-) delete mode 100644 app/controllers/api/vcs.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php diff --git a/app/config/services.php b/app/config/services.php index 0bbba96032..a99501c530 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -188,7 +188,7 @@ return [ 'name' => 'VCS', 'subtitle' => 'The VCS service allows you to interact with providers like GitHub, GitLab etc.', 'description' => '', - 'controller' => 'api/vcs.php', + 'controller' => '', // Uses modules 'sdk' => false, 'docs' => false, 'docsUrl' => '', diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php deleted file mode 100644 index 6dd18ed0f3..0000000000 --- a/app/controllers/api/vcs.php +++ /dev/null @@ -1,705 +0,0 @@ -getAttribute('resourceType'); - - if ($resourceType !== "function" && $resourceType !== "site") { - continue; - } - - $projectId = $repository->getAttribute('projectId'); - $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project'); - } - $dbForProject = $getProjectDB($project); - - $resourceCollection = $resourceType === "function" ? 'functions' : 'sites'; - $resourceId = $repository->getAttribute('resourceId'); - $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); - $resourceInternalId = $resource->getSequence(); - - $deploymentId = ID::unique(); - $repositoryId = $repository->getId(); - $repositoryInternalId = $repository->getSequence(); - $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); - $installationId = $repository->getAttribute('installationId'); - $installationInternalId = $repository->getAttribute('installationInternalId'); - $productionBranch = $resource->getAttribute('providerBranch'); - $activate = false; - - if ($providerBranch == $productionBranch && $external === false) { - $activate = true; - } - - $owner = $github->getOwnerName($providerInstallationId) ?? ''; - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - - $isAuthorized = !$external; - - if (!$isAuthorized && !empty($providerPullRequestId)) { - if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) { - $isAuthorized = true; - } - } - - $commentStatus = $isAuthorized ? 'waiting' : 'failed'; - $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; - $hostname = $platform['consoleHostname'] ?? ''; - - $authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}"; - - $action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl]; - - $latestCommentId = ''; - - if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) { - $latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [ - Query::equal('providerRepositoryId', [$providerRepositoryId]), - Query::equal('providerPullRequestId', [$providerPullRequestId]), - Query::orderDesc('$createdAt'), - ])); - - if (!$latestComment->isEmpty()) { - $latestCommentId = $latestComment->getAttribute('providerCommentId', ''); - - $retries = 0; - $lockAcquired = false; - - while ($retries < 9) { - $retries++; - - try { - $dbForPlatform->createDocument('vcsCommentLocks', new Document([ - '$id' => $latestCommentId - ])); - $lockAcquired = true; - break; - } catch (\Throwable $err) { - if ($retries >= 9) { - Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage()); - } - - \sleep(1); - } - } - - if ($lockAcquired) { - // Wrap in try/finally to ensure lock file gets deleted - try { - $comment = new Comment($platform); - $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); - $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); - - $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); - } finally { - $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); - } - } - } else { - $comment = new Comment($platform); - $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); - $latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment())); - - if (!empty($latestCommentId)) { - $teamId = $project->getAttribute('teamId', ''); - - $latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - Permission::update(Role::team(ID::custom($teamId), 'owner')), - Permission::update(Role::team(ID::custom($teamId), 'developer')), - Permission::delete(Role::team(ID::custom($teamId), 'owner')), - Permission::delete(Role::team(ID::custom($teamId), 'developer')), - ], - 'installationInternalId' => $installationInternalId, - 'installationId' => $installationId, - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), - 'providerRepositoryId' => $providerRepositoryId, - 'providerBranch' => $providerBranch, - 'providerPullRequestId' => $providerPullRequestId, - 'providerCommentId' => $latestCommentId - ]))); - } - } - } elseif (!empty($providerBranch)) { - $latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [ - Query::equal('providerRepositoryId', [$providerRepositoryId]), - Query::equal('providerBranch', [$providerBranch]), - Query::orderDesc('$createdAt'), - ])); - - foreach ($latestComments as $comment) { - $latestCommentId = $comment->getAttribute('providerCommentId', ''); - - $retries = 0; - $lockAcquired = false; - - while ($retries < 9) { - $retries++; - - try { - $dbForPlatform->createDocument('vcsCommentLocks', new Document([ - '$id' => $latestCommentId - ])); - $lockAcquired = true; - break; - } catch (\Throwable $err) { - if ($retries >= 9) { - Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage()); - } - - \sleep(1); - } - } - - if ($lockAcquired) { - // Wrap in try/finally to ensure lock file gets deleted - try { - $comment = new Comment($platform); - $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); - $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); - - $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); - } finally { - $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); - } - } - } - } - - if (!$isAuthorized) { - $resourceName = $resource->getAttribute('name'); - $projectName = $project->getAttribute('name'); - $name = "{$resourceName} ({$projectName})"; - $message = 'Authorization required for external contributor.'; - - $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - $owner = $github->getOwnerName($providerInstallationId); - $github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'failure', $message, $authorizeUrl, $name); - continue; - } - - if ($external) { - $pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId); - $providerRepositoryName = $pullRequestResponse['head']['repo']['owner']['login']; - $providerRepositoryOwner = $pullRequestResponse['head']['repo']['name']; - } - - $commands = []; - if (!empty($resource->getAttribute('installCommand', ''))) { - $commands[] = $resource->getAttribute('installCommand', ''); - } - if (!empty($resource->getAttribute('buildCommand', ''))) { - $commands[] = $resource->getAttribute('buildCommand', ''); - } - if (!empty($resource->getAttribute('commands', ''))) { - $commands[] = $resource->getAttribute('commands', ''); - } - - $deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceId' => $resourceId, - 'resourceInternalId' => $resourceInternalId, - 'resourceType' => $resourceCollection, - 'entrypoint' => $resource->getAttribute('entrypoint', ''), - 'buildCommands' => \implode(' && ', $commands), - 'startCommand' => $resource->getAttribute('startCommand', ''), - 'buildOutput' => $resource->getAttribute('outputDirectory', ''), - 'adapter' => $resource->getAttribute('adapter', ''), - 'fallbackFile' => $resource->getAttribute('fallbackFile', ''), - 'type' => 'vcs', - 'installationId' => $installationId, - 'installationInternalId' => $installationInternalId, - 'providerRepositoryId' => $providerRepositoryId, - 'repositoryId' => $repositoryId, - 'repositoryInternalId' => $repositoryInternalId, - 'providerBranchUrl' => $providerBranchUrl, - 'providerRepositoryName' => $providerRepositoryName, - 'providerRepositoryOwner' => $providerRepositoryOwner, - 'providerRepositoryUrl' => $providerRepositoryUrl, - 'providerCommitHash' => $providerCommitHash, - 'providerCommitAuthorUrl' => $providerCommitAuthorUrl, - 'providerCommitAuthor' => $providerCommitAuthor, - 'providerCommitMessage' => mb_strimwidth($providerCommitMessage, 0, 255, '...'), - 'providerCommitUrl' => $providerCommitUrl, - 'providerCommentId' => \strval($latestCommentId), - 'providerBranch' => $providerBranch, - 'activate' => $activate, - ]))); - - $resource = $resource - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource)); - - if ($resource->getCollection() === 'sites') { - $projectId = $project->getId(); - - // Deployment preview - $sitesDomain = $platform['sitesDomain']; - $domain = ID::unique() . "." . $sitesDomain; - $ruleId = md5($domain); - $previewRuleId = $ruleId; - $authorization->skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getSequence(), - 'domain' => $domain, - 'type' => 'deployment', - 'trigger' => 'deployment', - 'deploymentId' => $deployment->getId(), - 'deploymentInternalId' => $deployment->getSequence(), - 'deploymentResourceType' => 'site', - 'deploymentResourceId' => $resourceId, - 'deploymentResourceInternalId' => $resourceInternalId, - 'deploymentVcsProviderBranch' => $providerBranch, - 'status' => 'verified', - 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain]), - 'owner' => 'Appwrite', - 'region' => $project->getAttribute('region') - ])) - ); - - // VCS branch preview - if (!empty($providerBranch)) { - $domain = (new BranchDomainFilter())->apply([ - 'branch' => $providerBranch, - 'resourceId' => $resource->getId(), - 'projectId' => $project->getId(), - 'sitesDomain' => $sitesDomain, - ]); - $ruleId = md5($domain); - try { - $authorization->skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getSequence(), - 'domain' => $domain, - 'type' => 'deployment', - 'trigger' => 'deployment', - 'deploymentId' => $deployment->getId(), - 'deploymentInternalId' => $deployment->getSequence(), - 'deploymentResourceType' => 'site', - 'deploymentResourceId' => $resourceId, - 'deploymentResourceInternalId' => $resourceInternalId, - 'deploymentVcsProviderBranch' => $providerBranch, - 'status' => 'verified', - 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain]), - 'owner' => 'Appwrite', - 'region' => $project->getAttribute('region') - ])) - ); - } catch (Duplicate $err) { - // Ignore, rule already exists; will be updated by builds worker - } - } - - // VCS commit preview - if (!empty($providerCommitHash)) { - $domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}"; - $ruleId = md5($domain); - try { - $authorization->skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getSequence(), - 'domain' => $domain, - 'type' => 'deployment', - 'trigger' => 'deployment', - 'deploymentId' => $deployment->getId(), - 'deploymentInternalId' => $deployment->getSequence(), - 'deploymentResourceType' => 'site', - 'deploymentResourceId' => $resourceId, - 'deploymentResourceInternalId' => $resourceInternalId, - 'deploymentVcsProviderBranch' => $providerBranch, - 'status' => 'verified', - 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain]), - 'owner' => 'Appwrite', - 'region' => $project->getAttribute('region') - ])) - ); - } catch (Duplicate $err) { - // Ignore, rule already exists; will be updated by builds worker - } - } - } - - if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) { - $retries = 0; - $lockAcquired = false; - - while ($retries < 9) { - $retries++; - - try { - $dbForPlatform->createDocument('vcsCommentLocks', new Document([ - '$id' => $latestCommentId - ])); - $lockAcquired = true; - break; - } catch (\Throwable $err) { - if ($retries >= 9) { - Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage()); - } - - \sleep(1); - } - } - - if ($lockAcquired) { - // Wrap in try/finally to ensure lock file gets deleted - try { - $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId)); - - $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; - $previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : ''; - - if (!empty($previewUrl)) { - $comment = new Comment($platform); - $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); - $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl); - $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()); - } - } finally { - $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); - } - } - } - - if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) { - $resourceName = $resource->getAttribute('name'); - $projectName = $project->getAttribute('name'); - $region = $project->getAttribute('region', 'default'); - $name = "{$resourceName} ({$projectName})"; - $message = 'Starting...'; - - $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - $owner = $github->getOwnerName($providerInstallationId); - - $providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId"; - $github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name); - } - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($resource) - ->setDeployment($deployment) - ->setProject($project); // set the project because it won't be set for git deployments - - $queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site - - //TODO: Add event? - } catch (Throwable $e) { - $errors[] = $e->getMessage(); - } - } - - $queueForBuilds->reset(); // prevent shutdown hook from triggering again - - if (!empty($errors)) { - throw new Exception(Exception::GENERAL_UNKNOWN, \implode("\n", $errors)); - } -}; - -Http::post('/v1/vcs/github/events') - ->desc('Create event') - ->groups(['api', 'vcs']) - ->label('scope', 'public') - ->inject('gitHub') - ->inject('request') - ->inject('response') - ->inject('dbForPlatform') - ->inject('authorization') - ->inject('getProjectDB') - ->inject('queueForBuilds') - ->inject('platform') - ->action( - function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) { - $payload = $request->getRawPayload(); - $signatureRemote = $request->getHeader('x-hub-signature-256', ''); - $signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); - - $valid = empty($signatureRemote) ? true : $github->validateWebhookEvent($payload, $signatureRemote, $signatureLocal); - - if (!$valid) { - throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, "Invalid webhook payload signature. Please make sure the webhook secret has same value in your GitHub app and in the _APP_VCS_GITHUB_WEBHOOK_SECRET environment variable"); - } - - $event = $request->getHeader('x-github-event', ''); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $parsedPayload = $github->getEvent($event, $payload); - - if ($event == $github::EVENT_PUSH) { - $providerBranchCreated = $parsedPayload["branchCreated"] ?? false; - $providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false; - $providerBranch = $parsedPayload["branch"] ?? ''; - $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; - $providerRepositoryId = $parsedPayload["repositoryId"] ?? ''; - $providerRepositoryName = $parsedPayload["repositoryName"] ?? ''; - $providerInstallationId = $parsedPayload["installationId"] ?? ''; - $providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? ''; - $providerCommitHash = $parsedPayload["commitHash"] ?? ''; - $providerRepositoryOwner = $parsedPayload["owner"] ?? ''; - $providerCommitAuthorName = $parsedPayload["headCommitAuthorName"] ?? ''; - $providerCommitAuthorEmail = $parsedPayload["headCommitAuthorEmail"] ?? ''; - $providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? ''; - $providerCommitMessage = $parsedPayload["headCommitMessage"] ?? ''; - $providerCommitUrl = $parsedPayload["headCommitUrl"] ?? ''; - - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - //find resourceId from relevant resources table - $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ - Query::equal('providerRepositoryId', [$providerRepositoryId]), - Query::limit(100), - ])); - - // create new deployment only on push (not committed by us) and not when branch is created or deleted - if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) { - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform); - } - } elseif ($event == $github::EVENT_INSTALLATION) { - if ($parsedPayload["action"] == "deleted") { - // TODO: Use worker for this job instead (update function/site as well) - $providerInstallationId = $parsedPayload["installationId"]; - - $installations = $dbForPlatform->find('installations', [ - Query::equal('providerInstallationId', [$providerInstallationId]), - Query::limit(1000) - ]); - - foreach ($installations as $installation) { - $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ - Query::equal('installationInternalId', [$installation->getSequence()]), - Query::limit(1000) - ])); - - foreach ($repositories as $repository) { - $authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId())); - } - - $authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId())); - } - } - } elseif ($event == $github::EVENT_PULL_REQUEST) { - if ($parsedPayload["action"] == "opened" || $parsedPayload["action"] == "reopened" || $parsedPayload["action"] == "synchronize") { - $providerBranch = $parsedPayload["branch"] ?? ''; - $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; - $providerRepositoryId = $parsedPayload["repositoryId"] ?? ''; - $providerRepositoryName = $parsedPayload["repositoryName"] ?? ''; - $providerInstallationId = $parsedPayload["installationId"] ?? ''; - $providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? ''; - $providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? ''; - $providerCommitHash = $parsedPayload["commitHash"] ?? ''; - $providerRepositoryOwner = $parsedPayload["owner"] ?? ''; - $external = $parsedPayload["external"] ?? true; - $providerCommitUrl = $parsedPayload["headCommitUrl"] ?? ''; - $providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? ''; - - // Ignore sync for non-external. We handle it in push webhook - if (!$external && $parsedPayload["action"] == "synchronize") { - return $response->json($parsedPayload); - } - - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash); - $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; - $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - - $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ - Query::equal('providerRepositoryId', [$providerRepositoryId]), - Query::orderDesc('$createdAt') - ])); - - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform); - } elseif ($parsedPayload["action"] == "closed") { - // Allowed external contributions cleanup - - $providerRepositoryId = $parsedPayload["repositoryId"] ?? ''; - $providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? ''; - $external = $parsedPayload["external"] ?? true; - - if ($external) { - $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ - Query::equal('providerRepositoryId', [$providerRepositoryId]), - Query::orderDesc('$createdAt') - ])); - - foreach ($repositories as $repository) { - $providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []); - - if (\in_array($providerPullRequestId, $providerPullRequestIds)) { - $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); - $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); - } - } - } - } - } - - $response->json($parsedPayload); - } - ); - -Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') - ->desc('Update external deployment (authorize)') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.write') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'updateExternalDeployments', - description: '/docs/references/vcs/update-external-deployments.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('repositoryId', '', new Text(256), 'VCS Repository Id') - ->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id') - ->inject('gitHub') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->inject('authorization') - ->inject('getProjectDB') - ->inject('queueForBuilds') - ->inject('platform') - ->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [ - Query::equal('$id', [$repositoryId]), - Query::equal('projectInternalId', [$project->getSequence()]) - ])); - - if ($repository->isEmpty()) { - throw new Exception(Exception::REPOSITORY_NOT_FOUND); - } - - if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) { - throw new Exception(Exception::PROVIDER_CONTRIBUTION_CONFLICT); - } - - $providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId])); - $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - - // TODO: Delete from array when PR is closed - - $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); - - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $repositories = [$repository]; - $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); - - $owner = $github->getOwnerName($providerInstallationId); - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - $pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId); - - $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; - $providerCommitHash = $pullRequestResponse['head']['sha'] ?? ''; - $providerBranchUrl = $pullRequestResponse['head']['repo']['html_url'] ?? ''; - $providerRepositoryName = $pullRequestResponse['head']['repo']['name'] ?? ''; - $providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? ''; - $providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? ''; - $providerCommitAuthor = $pullRequestResponse['head']['user']['login'] ?? ''; - $providerCommitAuthorUrl = $pullRequestResponse['head']['user']['html_url'] ?? ''; - $providerCommitMessage = $pullRequestResponse['title'] ?? ''; - $providerCommitUrl = $pullRequestResponse['html_url'] ?? ''; - - $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform); - - $response->noContent(); - }); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php new file mode 100644 index 0000000000..6b521e56d0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php @@ -0,0 +1,141 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') + ->desc('Update external deployment (authorize)') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.write') + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'updateExternalDeployments', + description: '/docs/references/vcs/update-external-deployments.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_NOCONTENT, + model: Response::MODEL_NONE, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('repositoryId', '', new Text(256), 'VCS Repository Id') + ->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id') + ->inject('gitHub') + ->inject('response') + ->inject('project') + ->inject('dbForPlatform') + ->inject('authorization') + ->inject('getProjectDB') + ->inject('queueForBuilds') + ->inject('platform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $repositoryId, + string $providerPullRequestId, + GitHub $github, + Response $response, + Document $project, + Database $dbForPlatform, + Authorization $authorization, + callable $getProjectDB, + Build $queueForBuilds, + array $platform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [ + Query::equal('$id', [$repositoryId]), + Query::equal('projectInternalId', [$project->getSequence()]) + ])); + + if ($repository->isEmpty()) { + throw new Exception(Exception::REPOSITORY_NOT_FOUND); + } + + if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) { + throw new Exception(Exception::PROVIDER_CONTRIBUTION_CONFLICT); + } + + $providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId])); + $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); + + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $repositories = [$repository]; + $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); + + try { + $providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($providerRepositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $owner = $github->getOwnerName($providerInstallationId); + $pullRequestResponse = $github->getPullRequest($owner, $providerRepositoryName, $providerPullRequestId); + + $providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? ''; + $providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? ''; + $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; + $providerBranchUrl = "$providerRepositoryUrl/tree/$providerBranch"; + $providerCommitHash = $pullRequestResponse['head']['sha'] ?? ''; + + $commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash); + $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; + $providerCommitUrl = $commitDetails["commitUrl"] ?? ''; + $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; + $providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? ''; + + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php new file mode 100644 index 0000000000..6d493e2cdb --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -0,0 +1,523 @@ +getId(); + $projectId = $repository->getAttribute('projectId'); + $resourceId = $repository->getAttribute('resourceId'); + $resourceType = $repository->getAttribute('resourceType'); + + $logBase = "vcs.github.event.repo.{$repositoryId}"; + Span::add("{$logBase}.projectId", $projectId); + Span::add("{$logBase}.resourceId", $resourceId); + Span::add("{$logBase}.resourceType", $resourceType); + + if ($resourceType !== "function" && $resourceType !== "site") { + continue; + } + + $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project'); + } + + try { + $dsn = new DSN($project->getAttribute('database')); + $databaseName = $dsn->getHost(); + } catch (\InvalidArgumentException) { + $databaseName = $project->getAttribute('database'); + } + + $databases = Config::getParam('pools-database', []); + $index = in_array($databaseName, $databases); + + if ($index === false) { + Console::error("Database: '{$databaseName}' is not part of region: " . System::getEnv('_APP_REGION')); + continue; + } + + $dbForProject = $getProjectDB($project); + $resourceCollection = $resourceType === "function" ? 'functions' : 'sites'; + $resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); + $resourceInternalId = $resource->getSequence(); + + $deploymentId = ID::unique(); + $repositoryId = $repository->getId(); + $repositoryInternalId = $repository->getSequence(); + $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); + $installationId = $repository->getAttribute('installationId'); + $installationInternalId = $repository->getAttribute('installationInternalId'); + $productionBranch = $resource->getAttribute('providerBranch'); + $activate = false; + + if ($providerBranch == $productionBranch && $external === false) { + $activate = true; + } + + $owner = $github->getOwnerName($providerInstallationId) ?? ''; + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $isAuthorized = !$external; + + if (!$isAuthorized && !empty($providerPullRequestId)) { + if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) { + $isAuthorized = true; + } + } + + Span::add("{$logBase}.authorized", $isAuthorized); + + $commentStatus = 'waiting'; + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; + $hostname = $platform['consoleHostname'] ?? ''; + + $authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}"; + + $action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl]; + + $latestCommentId = ''; + + if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) { + $latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [ + Query::equal('providerRepositoryId', [$providerRepositoryId]), + Query::equal('providerPullRequestId', [$providerPullRequestId]), + Query::orderDesc('$createdAt'), + ])); + + if (!$latestComment->isEmpty()) { + $latestCommentId = $latestComment->getAttribute('providerCommentId', ''); + + $retries = 0; + $lockAcquired = false; + + while ($retries < 9) { + $retries++; + + try { + $dbForPlatform->createDocument('vcsCommentLocks', new Document([ + '$id' => $latestCommentId + ])); + $lockAcquired = true; + break; + } catch (\Throwable $err) { + if ($retries >= 9) { + Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage()); + } + + \sleep(1); + } + } + + if ($lockAcquired) { + // Wrap in try/finally to ensure lock file gets deleted + try { + $comment = new Comment($platform); + $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); + $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); + + $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); + } finally { + $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); + } + } + } else { + $comment = new Comment($platform); + $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); + $latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment())); + + if (!empty($latestCommentId)) { + $teamId = $project->getAttribute('teamId', ''); + + $latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'installationInternalId' => $installationInternalId, + 'installationId' => $installationId, + 'projectInternalId' => $project->getSequence(), + 'projectId' => $project->getId(), + 'providerRepositoryId' => $providerRepositoryId, + 'providerBranch' => $providerBranch, + 'providerPullRequestId' => $providerPullRequestId, + 'providerCommentId' => $latestCommentId + ]))); + } + } + } elseif (!empty($providerBranch)) { + $latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [ + Query::equal('providerRepositoryId', [$providerRepositoryId]), + Query::equal('providerBranch', [$providerBranch]), + Query::orderDesc('$createdAt'), + ])); + + foreach ($latestComments as $comment) { + $latestCommentId = $comment->getAttribute('providerCommentId', ''); + + $retries = 0; + $lockAcquired = false; + + while ($retries < 9) { + $retries++; + + try { + $dbForPlatform->createDocument('vcsCommentLocks', new Document([ + '$id' => $latestCommentId + ])); + $lockAcquired = true; + break; + } catch (\Throwable $err) { + if ($retries >= 9) { + Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage()); + } + + \sleep(1); + } + } + + if ($lockAcquired) { + // Wrap in try/finally to ensure lock file gets deleted + try { + $comment = new Comment($platform); + $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); + $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); + + $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); + } finally { + $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); + } + } + } + } + + if (!$isAuthorized) { + $resourceName = $resource->getAttribute('name'); + $projectName = $project->getAttribute('name'); + $name = "{$resourceName} ({$projectName})"; + $message = 'Authorization required for external contributor.'; + + $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + $owner = $github->getOwnerName($providerInstallationId); + $github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $authorizeUrl, $name); + continue; + } + + $commands = []; + if (!empty($resource->getAttribute('installCommand', ''))) { + $commands[] = $resource->getAttribute('installCommand', ''); + } + if (!empty($resource->getAttribute('buildCommand', ''))) { + $commands[] = $resource->getAttribute('buildCommand', ''); + } + if (!empty($resource->getAttribute('commands', ''))) { + $commands[] = $resource->getAttribute('commands', ''); + } + + $deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $resourceId, + 'resourceInternalId' => $resourceInternalId, + 'resourceType' => $resourceCollection, + 'entrypoint' => $resource->getAttribute('entrypoint', ''), + 'buildCommands' => \implode(' && ', $commands), + 'startCommand' => $resource->getAttribute('startCommand', ''), + 'buildOutput' => $resource->getAttribute('outputDirectory', ''), + 'adapter' => $resource->getAttribute('adapter', ''), + 'fallbackFile' => $resource->getAttribute('fallbackFile', ''), + 'type' => 'vcs', + 'installationId' => $installationId, + 'installationInternalId' => $installationInternalId, + 'providerRepositoryId' => $providerRepositoryId, + 'repositoryId' => $repositoryId, + 'repositoryInternalId' => $repositoryInternalId, + 'providerBranchUrl' => $providerBranchUrl, + 'providerRepositoryName' => $providerRepositoryName, + 'providerRepositoryOwner' => $providerRepositoryOwner, + 'providerRepositoryUrl' => $providerRepositoryUrl, + 'providerCommitHash' => $providerCommitHash, + 'providerCommitAuthorUrl' => $providerCommitAuthorUrl, + 'providerCommitAuthor' => $providerCommitAuthor, + 'providerCommitMessage' => mb_strimwidth($providerCommitMessage, 0, 255, '...'), + 'providerCommitUrl' => $providerCommitUrl, + 'providerCommentId' => \strval($latestCommentId), + 'providerBranch' => $providerBranch, + 'activate' => $activate, + ]))); + + $resource = $resource + ->setAttribute('latestDeploymentId', $deployment->getId()) + ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) + ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) + ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); + $authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource)); + + if ($resource->getCollection() === 'sites') { + $projectId = $project->getId(); + + // Deployment preview + $sitesDomain = $platform['sitesDomain']; + $domain = ID::unique() . "." . $sitesDomain; + $ruleId = md5($domain); + $previewRuleId = $ruleId; + $authorization->skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getSequence(), + 'domain' => $domain, + 'type' => 'deployment', + 'trigger' => 'deployment', + 'deploymentId' => $deployment->getId(), + 'deploymentInternalId' => $deployment->getSequence(), + 'deploymentResourceType' => 'site', + 'deploymentResourceId' => $resourceId, + 'deploymentResourceInternalId' => $resourceInternalId, + 'deploymentVcsProviderBranch' => $providerBranch, + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + 'owner' => 'Appwrite', + 'region' => $project->getAttribute('region') + ])) + ); + + // VCS branch preview + if (!empty($providerBranch)) { + $domain = (new BranchDomainFilter())->apply([ + 'branch' => $providerBranch, + 'resourceId' => $resource->getId(), + 'projectId' => $project->getId(), + 'sitesDomain' => $sitesDomain, + ]); + $ruleId = md5($domain); + try { + $authorization->skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getSequence(), + 'domain' => $domain, + 'type' => 'deployment', + 'trigger' => 'deployment', + 'deploymentId' => $deployment->getId(), + 'deploymentInternalId' => $deployment->getSequence(), + 'deploymentResourceType' => 'site', + 'deploymentResourceId' => $resourceId, + 'deploymentResourceInternalId' => $resourceInternalId, + 'deploymentVcsProviderBranch' => $providerBranch, + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + 'owner' => 'Appwrite', + 'region' => $project->getAttribute('region') + ])) + ); + } catch (Duplicate $err) { + // Ignore, rule already exists; will be updated by builds worker + } + } + + // VCS commit preview + if (!empty($providerCommitHash)) { + $domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}"; + $ruleId = md5($domain); + try { + $authorization->skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getSequence(), + 'domain' => $domain, + 'type' => 'deployment', + 'trigger' => 'deployment', + 'deploymentId' => $deployment->getId(), + 'deploymentInternalId' => $deployment->getSequence(), + 'deploymentResourceType' => 'site', + 'deploymentResourceId' => $resourceId, + 'deploymentResourceInternalId' => $resourceInternalId, + 'deploymentVcsProviderBranch' => $providerBranch, + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + 'owner' => 'Appwrite', + 'region' => $project->getAttribute('region') + ])) + ); + } catch (Duplicate $err) { + // Ignore, rule already exists; will be updated by builds worker + } + } + } + + if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) { + $retries = 0; + $lockAcquired = false; + + while ($retries < 9) { + $retries++; + + try { + $dbForPlatform->createDocument('vcsCommentLocks', new Document([ + '$id' => $latestCommentId + ])); + $lockAcquired = true; + break; + } catch (\Throwable $err) { + if ($retries >= 9) { + Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage()); + } + + \sleep(1); + } + } + + if ($lockAcquired) { + // Wrap in try/finally to ensure lock file gets deleted + try { + $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId)); + + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; + $previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : ''; + + if (!empty($previewUrl)) { + $comment = new Comment($platform); + $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); + $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl); + $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()); + } + } finally { + $authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId)); + } + } + } + + if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) { + $resourceName = $resource->getAttribute('name'); + $projectName = $project->getAttribute('name'); + $region = $project->getAttribute('region', 'default'); + $name = "{$resourceName} ({$projectName})"; + $message = 'Starting...'; + + $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + $owner = $github->getOwnerName($providerInstallationId); + + $providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId"; + $github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name); + } + + $queueName = $this->getBuildQueueName($project, $dbForPlatform, $authorization); + + $queueForBuilds + ->setQueue($queueName) + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($resource) + ->setDeployment($deployment) + ->setProject($project); // set the project because it won't be set for git deployments + + $queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site + + Span::add("{$logBase}.build.triggered", 'true'); + //TODO: Add event? + } catch (\Throwable $e) { + Span::add("{$logBase}.error", $e->getMessage()); + $errors[] = $e->getMessage(); + } + } + + $queueForBuilds->reset(); // prevent shutdown hook from triggering again + + if (!empty($errors)) { + throw new Exception(Exception::GENERAL_UNKNOWN, \implode("\n", $errors)); + } + } + + protected function getBuildQueueName(Document $project, Database $dbForPlatform, Authorization $authorization): string + { + return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php new file mode 100644 index 0000000000..de6750fe64 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -0,0 +1,244 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/vcs/github/events') + ->desc('Create event') + ->groups(['api', 'vcs']) + ->label('scope', 'public') + ->inject('gitHub') + ->inject('request') + ->inject('response') + ->inject('dbForPlatform') + ->inject('authorization') + ->inject('getProjectDB') + ->inject('queueForBuilds') + ->inject('platform') + ->callback($this->action(...)); + } + + public function action( + GitHub $github, + Request $request, + Response $response, + Database $dbForPlatform, + Authorization $authorization, + callable $getProjectDB, + Build $queueForBuilds, + array $platform + ) { + $this->preprocessEvent($request); + + $event = $request->getHeader('x-github-event', ''); + Span::add('vcs.github.event.name', $event); + + $payload = $request->getRawPayload(); + $signature = $request->getHeader('x-hub-signature-256', ''); + $secretKey = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); + + $valid = empty($signature) ? true : $github->validateWebhookEvent($payload, $signature, $secretKey); + Span::add('vcs.github.event.signature.valid', $valid); + + if (!$valid) { + throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, "Invalid webhook payload signature. Please make sure the webhook secret has same value in your GitHub app and in the _APP_VCS_GITHUB_WEBHOOK_SECRET environment variable"); + } + + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $parsedPayload = $github->getEvent($event, $payload); + + match ($event) { + $github::EVENT_INSTALLATION => $this->handleInstallationEvent($parsedPayload, $dbForPlatform, $authorization), + $github::EVENT_PUSH => $this->handlePushEvent($parsedPayload, $githubAppId, $privateKey, $github, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform), + $github::EVENT_PULL_REQUEST => $this->handlePullRequestEvent($parsedPayload, $privateKey, $githubAppId, $github, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform), + default => null, + }; + + return $response->json($parsedPayload); + } + + protected function preprocessEvent(Request $request) + { + return; + } + + private function handleInstallationEvent( + array $parsedPayload, + Database $dbForPlatform, + Authorization $authorization, + ) { + if ($parsedPayload["action"] !== "deleted") { + return; + } + + // TODO: Use worker for this job instead (update function/site as well) + $providerInstallationId = $parsedPayload["installationId"]; + + $installations = $dbForPlatform->find('installations', [ + Query::equal('providerInstallationId', [$providerInstallationId]), + Query::limit(1000) + ]); + + foreach ($installations as $installation) { + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ + Query::equal('installationInternalId', [$installation->getSequence()]), + Query::limit(1000) + ])); + + foreach ($repositories as $repository) { + $authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId())); + } + + $authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId())); + } + } + + private function handlePushEvent( + array $parsedPayload, + string $githubAppId, + string $privateKey, + GitHub $github, + Database $dbForPlatform, + Authorization $authorization, + Build $queueForBuilds, + callable $getProjectDB, + array $platform, + ) { + $providerBranchCreated = $parsedPayload["branchCreated"] ?? false; + $providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false; + $providerBranch = $parsedPayload["branch"] ?? ''; + $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; + $providerRepositoryId = $parsedPayload["repositoryId"] ?? ''; + $providerRepositoryName = $parsedPayload["repositoryName"] ?? ''; + $providerInstallationId = $parsedPayload["installationId"] ?? ''; + $providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? ''; + $providerCommitHash = $parsedPayload["commitHash"] ?? ''; + $providerRepositoryOwner = $parsedPayload["owner"] ?? ''; + $providerCommitAuthorName = $parsedPayload["headCommitAuthorName"] ?? ''; + $providerCommitAuthorEmail = $parsedPayload["headCommitAuthorEmail"] ?? ''; + $providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? ''; + $providerCommitMessage = $parsedPayload["headCommitMessage"] ?? ''; + $providerCommitUrl = $parsedPayload["headCommitUrl"] ?? ''; + + Span::add('vcs.github.event.repo.id', $providerRepositoryId); + Span::add('vcs.github.event.repo.name', $providerRepositoryName); + Span::add('vcs.github.event.branch', $providerBranch); + Span::add('vcs.github.event.installation.id', $providerInstallationId); + + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + // Find associated repositories + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ + Query::equal('providerRepositoryId', [$providerRepositoryId]), + Query::limit(100), + ])); + + // Create new deployment only on push (not committed by us) and not when branch is created or deleted + if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) { + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + } + } + + private function handlePullRequestEvent( + array $parsedPayload, + string $privateKey, + string $githubAppId, + GitHub $github, + Database $dbForPlatform, + Authorization $authorization, + Build $queueForBuilds, + callable $getProjectDB, + array $platform, + ) { + $action = $parsedPayload["action"] ?? ''; + + if ($action == "opened" || $action == "reopened" || $action == "synchronize") { + $providerBranch = $parsedPayload["branch"] ?? ''; + $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; + $providerRepositoryId = $parsedPayload["repositoryId"] ?? ''; + $providerRepositoryName = $parsedPayload["repositoryName"] ?? ''; + $providerInstallationId = $parsedPayload["installationId"] ?? ''; + $providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? ''; + $providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? ''; + $providerCommitHash = $parsedPayload["commitHash"] ?? ''; + $providerRepositoryOwner = $parsedPayload["owner"] ?? ''; + $external = $parsedPayload["external"] ?? true; + $providerCommitUrl = $parsedPayload["headCommitUrl"] ?? ''; + $providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? ''; + + Span::add('vcs.github.event.repo.id', $providerRepositoryId); + Span::add('vcs.github.event.repo.name', $providerRepositoryName); + Span::add('vcs.github.event.branch', $providerBranch); + Span::add('vcs.github.event.installation.id', $providerInstallationId); + + // Ignore sync for non-external. We handle it in push webhook + if (!$external && $parsedPayload["action"] == "synchronize") { + return; + } + + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash); + $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; + $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; + + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ + Query::equal('providerRepositoryId', [$providerRepositoryId]), + Query::orderDesc('$createdAt') + ])); + + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); + } elseif ($action == "closed") { + // Allowed external contributions cleanup + + $providerRepositoryId = $parsedPayload["repositoryId"] ?? ''; + $providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? ''; + $external = $parsedPayload["external"] ?? true; + + if ($external) { + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ + Query::equal('providerRepositoryId', [$providerRepositoryId]), + Query::orderDesc('$createdAt') + ])); + + foreach ($repositories as $repository) { + $providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []); + + if (\in_array($providerPullRequestId, $providerPullRequestIds)) { + $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); + $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + } + } + } + } + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Services/Http.php b/src/Appwrite/Platform/Modules/VCS/Services/Http.php index 8bd2314f9e..ae766ad00e 100644 --- a/src/Appwrite/Platform/Modules/VCS/Services/Http.php +++ b/src/Appwrite/Platform/Modules/VCS/Services/Http.php @@ -2,8 +2,10 @@ namespace Appwrite\Platform\Modules\VCS\Services; +use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\External\Update as UpdateExternalDeployment; use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\Get as GetGitHubAuthorize; use Appwrite\Platform\Modules\VCS\Http\GitHub\Callback\Get as GetGitHubCallback; +use Appwrite\Platform\Modules\VCS\Http\GitHub\Events\Create as CreateGitHubEvent; use Appwrite\Platform\Modules\VCS\Http\Installations\Delete as DeleteInstallation; use Appwrite\Platform\Modules\VCS\Http\Installations\Get as GetInstallation; use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Branches\XList as ListRepositoryBranches; @@ -24,6 +26,7 @@ class Http extends Service // GitHub Authorization & Callback $this->addAction(GetGitHubAuthorize::getName(), new GetGitHubAuthorize()); $this->addAction(GetGitHubCallback::getName(), new GetGitHubCallback()); + $this->addAction(UpdateExternalDeployment::getName(), new UpdateExternalDeployment()); // Installations $this->addAction(GetInstallation::getName(), new GetInstallation()); @@ -37,5 +40,8 @@ class Http extends Service $this->addAction(ListRepositoryBranches::getName(), new ListRepositoryBranches()); $this->addAction(GetRepositoryContents::getName(), new GetRepositoryContents()); $this->addAction(CreateRepositoryDetections::getName(), new CreateRepositoryDetections()); + + // Events + $this->addAction(CreateGitHubEvent::getName(), new CreateGitHubEvent()); } }