From a6d58a847d6a0262848f578e8c85857d7eb52cff Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 13:15:26 +0530 Subject: [PATCH 01/10] fix: handle beta SDK versioning in DiffCheck prompt Beta SDKs (version < 1.0.0) should not be bumped to 1.0.0. The prompt now checks the beta flag from sdk config and instructs the AI to use minor bumps for both breaking changes and new features, and patch for bug fixes only. Also adds a rule to wrap code identifiers in backticks for better changelog rendering. --- src/Appwrite/Platform/Tasks/SDKs.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 949dbb3e6b..f2103893a9 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -780,20 +780,25 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND required: $object->getNames() ); + $isBeta = !empty($language['beta']); + $betaNote = $isBeta + ? "\n Note: This SDK is in beta (version < 1.0.0). Do NOT bump to 1.0.0. Use `minor` for both breaking changes and new features, `patch` for bug fixes only." + : ''; + $prompt = << Date: Wed, 4 Mar 2026 13:16:55 +0530 Subject: [PATCH 02/10] refactor: derive supported SDKs from config instead of hardcoded array Replace the static supportedSDKS array with a getSupportedSDKs() method that reads SDK keys from the sdks.php config file. This eliminates the need to maintain the list in two places. --- src/Appwrite/Platform/Tasks/SDKs.php | 38 ++++++++++------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index f2103893a9..27095e6ec2 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -41,28 +41,17 @@ use Utopia\Validator\WhiteList; class SDKs extends Action { - protected array $supportedSDKS = [ - 'web', - 'cli', - 'php', - 'nodejs', - 'deno', - 'python', - 'ruby', - 'flutter', - 'react-native', - 'dart', - 'go', - 'swift', - 'apple', - 'dotnet', - 'android', - 'graphql', - 'rest', - 'markdown', - 'agent-skills', - 'cursor-plugin' - ]; + protected function getSupportedSDKs(): array + { + $keys = []; + $platforms = Config::getParam('sdks'); + foreach ($platforms as $platform) { + foreach ($platform['sdks'] as $sdk) { + $keys[] = $sdk['key']; + } + } + return \array_unique($keys); + } public static function getName(): string { @@ -100,8 +89,9 @@ class SDKs extends Action if (! $sdks) { $selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '" or "*" for all):'); $selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):')); - if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $this->supportedSDKS)) { - throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $this->supportedSDKS)); + $supportedSDKs = $this->getSupportedSDKs(); + if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $supportedSDKs)) { + throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $supportedSDKs)); } } else { $sdks = explode(',', $sdks); From fc88d4b4ab9039aed465fb7e5d581f70f4aff2b1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 13:19:48 +0530 Subject: [PATCH 03/10] feat: auto-generate commit messages for SDK pushes Remove the manual commit message prompt. When AI is available and produces a changelog, use it as the commit message. Otherwise fall back to a descriptive message based on SDK name and version. A manually provided --message flag still takes priority. --- src/Appwrite/Platform/Tasks/SDKs.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 27095e6ec2..5aa5042739 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -107,9 +107,6 @@ class SDKs extends Action $prUrls = []; - if ($git) { - $message ??= Console::confirm('Please enter your commit message:'); - } } elseif ($examplesOnly) { $git = false; $prUrls = []; @@ -518,6 +515,16 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $repoBranch = $language['repoBranch'] ?? 'main'; if ($git && ! empty($gitUrl)) { + // Generate commit message: use provided message, AI changelog, or fallback + if (! empty($message)) { + $commitMessage = $message; + } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; + } else { + $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; + } + $escapedCommitMessage = \addcslashes($commitMessage, '"\\`$'); + Console::info("Preparing {$language['name']} SDK repository..."); \exec('rm -rf ' . $target . ' && \ @@ -540,7 +547,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND cp -r ' . $result . '/. ' . $target . '/ && \ (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 . '" --quiet && \ + git commit -m "' . $escapedCommitMessage . '" --quiet && \ git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true ', $gitOutput, $gitReturnCode); From f989ccde579f6796300f2737a2df5546afbde1f0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 18:44:29 +0530 Subject: [PATCH 04/10] fix: add post-parse guard for beta SDK version bumps Add a programmatic guard after parsing the AI response that rejects major bumps or versions >= 1.0.0 for beta SDKs. When triggered, the SDK is skipped with a warning instead of proceeding with an invalid version. --- src/Appwrite/Platform/Tasks/SDKs.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 5aa5042739..715d322892 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -878,6 +878,12 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND return null; } + // Guard: beta SDKs must not be bumped to >= 1.0.0 + if ($isBeta && ($parsed['versionBump'] === 'major' || \version_compare($parsed['version'], '1.0.0', '>='))) { + Console::warning("Beta SDK {$language['name']} cannot have a major bump or version >= 1.0.0 (AI suggested {$parsed['version']}), skipping"); + return null; + } + Console::success("✓ Analysis complete"); Console::log(" Version: {$language['version']} → {$parsed['version']} ({$parsed['versionBump']} bump)"); Console::log(" Changelog:"); From 300aaeb25136fce0dcae9d2848eab9b8d13111ae Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 18:46:49 +0530 Subject: [PATCH 05/10] fix: escape shell arguments in SDK git and gh CLI commands Replace raw string interpolation with escapeshellarg() for all arguments passed to exec/shell_exec calls that build git commit, gh pr create, gh api, and gh release commands. This prevents shell injection from AI-generated changelog text or any other dynamically constructed values. --- src/Appwrite/Platform/Tasks/SDKs.php | 43 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 715d322892..a7e9d2c861 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -324,7 +324,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } // Check if release already exists - $checkReleaseCommand = 'gh release view "' . $releaseVersion . '" --repo "' . $repoName . '" --json url --jq ".url" 2>/dev/null'; + $checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null'; $existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? ''); if (! empty($existingReleaseUrl)) { @@ -355,7 +355,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } $previousVersion = ''; - $tagListCommand = 'gh release list --repo "' . $repoName . '" --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; + $tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; $previousVersion = trim(\shell_exec($tagListCommand) ?? ''); $formattedNotes = "## What's Changed\n\n"; @@ -383,11 +383,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_'); \file_put_contents($tempNotesFile, $formattedNotes); - $releaseCommand = 'gh release create "' . $releaseVersion . '" \ - --repo "' . $repoName . '" \ - --title "' . $releaseTitle . '" \ - --notes-file "' . $tempNotesFile . '" \ - --target "' . $releaseTarget . '" \ + $releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($releaseTitle) . ' \ + --notes-file ' . \escapeshellarg($tempNotesFile) . ' \ + --target ' . \escapeshellarg($releaseTarget) . ' \ 2>&1'; $releaseOutput = []; @@ -523,8 +523,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } else { $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; } - $escapedCommitMessage = \addcslashes($commitMessage, '"\\`$'); - Console::info("Preparing {$language['name']} SDK repository..."); \exec('rm -rf ' . $target . ' && \ @@ -547,7 +545,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND cp -r ' . $result . '/. ' . $target . '/ && \ (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 "' . $escapedCommitMessage . '" --quiet && \ + git commit -m ' . \escapeshellarg($commitMessage) . ' --quiet && \ git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true ', $gitOutput, $gitReturnCode); @@ -570,11 +568,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $prCommand = 'cd ' . $target . ' && \ gh pr create \ - --repo "' . $repoName . '" \ - --title "' . $prTitle . '" \ - --body "' . $prBody . '" \ - --base "' . $repoBranch . '" \ - --head "' . $gitBranch . '" \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($prTitle) . ' \ + --body ' . \escapeshellarg($prBody) . ' \ + --base ' . \escapeshellarg($repoBranch) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ 2>&1'; $prOutput = []; @@ -592,8 +590,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body..."); $prNumberCommand = 'cd ' . $target . ' && \ gh pr list \ - --repo "' . $repoName . '" \ - --head "' . $gitBranch . '" \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ --json number \ --jq ".[0].number" \ 2>&1'; @@ -606,14 +604,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $prNumber = trim($prNumberOutput[0]); // Use API directly to update PR to avoid deprecated projectCards field + $apiPath = "/repos/{$repoName}/pulls/{$prNumber}"; $updateCommand = 'cd ' . $target . ' && \ gh api \ --method PATCH \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/' . $repoName . '/pulls/' . $prNumber . ' \ - -f title="' . $prTitle . '" \ - -f body="' . $prBody . '" \ + ' . \escapeshellarg($apiPath) . ' \ + -f title=' . \escapeshellarg($prTitle) . ' \ + -f body=' . \escapeshellarg($prBody) . ' \ 2>&1'; $updateOutput = []; @@ -625,8 +624,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $prUrlCommand = 'cd ' . $target . ' && \ gh pr list \ - --repo "' . $repoName . '" \ - --head "' . $gitBranch . '" \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ --json url \ --jq ".[0].url" \ 2>&1'; From 198f9a64a30afb6648edab3d641201cd77a4d187 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:03:21 +0530 Subject: [PATCH 06/10] refactoring --- composer.json | 1 + composer.lock | 68 ++++- docker-compose.yml | 18 -- src/Appwrite/Platform/Tasks/SDKs.php | 388 +++++++++++++++------------ 4 files changed, 285 insertions(+), 190 deletions(-) diff --git a/composer.json b/composer.json index 1422bd5d0a..d7f0ae66b5 100644 --- a/composer.json +++ b/composer.json @@ -100,6 +100,7 @@ "swoole/ide-helper": "6.*", "phpstan/phpstan": "1.12.*", "textalk/websocket": "1.5.*", + "czproject/git-php": "4.*", "laravel/pint": "1.*", "phpbench/phpbench": "1.*" }, diff --git a/composer.lock b/composer.lock index ccf03ce835..6097b8ecd9 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": "1fb043a556550f62ab27a0aad0b9332a", + "content-hash": "1cc64e07484256225f56bd525674c3b8", "packages": [ { "name": "adhocore/jwt", @@ -5580,6 +5580,70 @@ ], "time": "2026-02-25T14:53:45+00:00" }, + { + "name": "czproject/git-php", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/czproject/git-php.git", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.5" + }, + "require-dev": { + "nette/tester": "^2.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jan Pecha", + "email": "janpecha@email.cz" + } + ], + "description": "Library for work with Git repository in PHP.", + "keywords": [ + "git" + ], + "support": { + "issues": "https://github.com/czproject/git-php/issues", + "source": "https://github.com/czproject/git-php/tree/v4.6.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/janpecha", + "type": "github" + }, + { + "url": "https://www.janpecha.cz/donate/git-php/", + "type": "other" + }, + { + "url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA", + "type": "stripe" + }, + { + "url": "https://thanks.dev/u/gh/czproject", + "type": "thanks.dev" + } + ], + "time": "2025-11-10T07:24:07+00:00" + }, { "name": "doctrine/annotations", "version": "2.0.2", @@ -9043,7 +9107,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index bdf77a46b4..4a38757737 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -152,7 +152,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_SMTP_HOST - _APP_SMTP_PORT - _APP_SMTP_SECURE @@ -305,7 +304,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_USAGE_STATS - _APP_LOGGING_CONFIG - _APP_LOGGING_CONFIG_REALTIME @@ -340,7 +338,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_DATABASE_SHARED_TABLES @@ -371,7 +368,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -414,7 +410,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_STORAGE_DEVICE - _APP_STORAGE_S3_ACCESS_KEY - _APP_STORAGE_S3_SECRET @@ -474,7 +469,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_WORKERS_NUM - _APP_QUEUE_NAME @@ -513,7 +507,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_VCS_GITHUB_APP_NAME - _APP_VCS_GITHUB_PRIVATE_KEY @@ -658,7 +651,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_DATABASE_SHARED_TABLES @@ -722,7 +714,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_FUNCTIONS_TIMEOUT - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT @@ -808,7 +799,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_SMS_FROM - _APP_SMS_PROVIDER @@ -875,7 +865,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_MIGRATIONS_FIREBASE_CLIENT_ID - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET @@ -918,7 +907,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_CACHE @@ -994,7 +982,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1028,7 +1015,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1062,7 +1048,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1100,7 +1085,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_DATABASE_SHARED_TABLES appwrite-task-scheduler-executions: @@ -1131,7 +1115,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER appwrite-task-scheduler-messages: entrypoint: schedule-messages @@ -1161,7 +1144,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_DATABASE_SHARED_TABLES appwrite-assistant: diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index a7e9d2c861..c318e34686 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -25,6 +25,7 @@ use Appwrite\SDK\Language\Swift; use Appwrite\SDK\Language\Web; use Appwrite\SDK\SDK; use Appwrite\Spec\Swagger2; +use CzProject\GitPhp\Git; use Utopia\Agents\Adapters\OpenAI; use Utopia\Agents\DiffCheck\DiffCheck; use Utopia\Agents\DiffCheck\Options as DiffCheckOptions; @@ -41,18 +42,6 @@ use Utopia\Validator\WhiteList; class SDKs extends Action { - protected function getSupportedSDKs(): array - { - $keys = []; - $platforms = Config::getParam('sdks'); - foreach ($platforms as $platform) { - foreach ($platform['sdks'] as $sdk) { - $keys[] = $sdk['key']; - } - } - return \array_unique($keys); - } - public static function getName(): string { return 'sdks'; @@ -63,6 +52,19 @@ class SDKs extends Action return Specs::getPlatforms(); } + protected function getSdkConfigPath(): string + { + return __DIR__ . '/../../../../app/config/sdks.php'; + } + + protected function getSupportedSDKs(): array + { + return \array_unique(\array_merge(...\array_map( + fn ($platform) => \array_column($platform['sdks'], 'key'), + Config::getParam('sdks') + ))); + } + public function __construct() { $this @@ -514,146 +516,151 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $gitBranch = $language['gitBranch']; $repoBranch = $language['repoBranch'] ?? 'main'; - if ($git && ! empty($gitUrl)) { - // Generate commit message: use provided message, AI changelog, or fallback - if (! empty($message)) { - $commitMessage = $message; - } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { - $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; - } else { - $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; - } - Console::info("Preparing {$language['name']} SDK repository..."); - - \exec('rm -rf ' . $target . ' && \ - mkdir -p ' . $target . ' && \ - cd ' . $target . ' && \ - 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 --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 . ' --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 . ' --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 && \ - (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 . '/ && \ - (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 ' . \escapeshellarg($commitMessage) . ' --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']}"; - - // 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..."); - - $prCommand = 'cd ' . $target . ' && \ - gh pr create \ - --repo ' . \escapeshellarg($repoName) . ' \ - --title ' . \escapeshellarg($prTitle) . ' \ - --body ' . \escapeshellarg($prBody) . ' \ - --base ' . \escapeshellarg($repoBranch) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - 2>&1'; - - $prOutput = []; - $prReturnCode = 0; - \exec($prCommand, $prOutput, $prReturnCode); - - if ($prReturnCode === 0) { - Console::success("Successfully created pull request for {$language['name']} SDK"); - if (! empty($prOutput)) { - $prUrls[$language['name']] = end($prOutput); - } - } else { - $errorMessage = implode("\n", $prOutput); - if (strpos($errorMessage, 'already exists') !== false) { - Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body..."); - $prNumberCommand = 'cd ' . $target . ' && \ - gh pr list \ - --repo ' . \escapeshellarg($repoName) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - --json number \ - --jq ".[0].number" \ - 2>&1'; - - $prNumberOutput = []; - $prNumberReturnCode = 0; - \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); - - if ($prNumberReturnCode === 0 && ! empty($prNumberOutput[0])) { - $prNumber = trim($prNumberOutput[0]); - - // Use API directly to update PR to avoid deprecated projectCards field - $apiPath = "/repos/{$repoName}/pulls/{$prNumber}"; - $updateCommand = 'cd ' . $target . ' && \ - gh api \ - --method PATCH \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - ' . \escapeshellarg($apiPath) . ' \ - -f title=' . \escapeshellarg($prTitle) . ' \ - -f body=' . \escapeshellarg($prBody) . ' \ - 2>&1'; - - $updateOutput = []; - $updateReturnCode = 0; - \exec($updateCommand, $updateOutput, $updateReturnCode); - - if ($updateReturnCode === 0) { - Console::success("Successfully updated pull request for {$language['name']} SDK"); - - $prUrlCommand = 'cd ' . $target . ' && \ - gh pr list \ - --repo ' . \escapeshellarg($repoName) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - --json url \ - --jq ".[0].url" \ - 2>&1'; - - $prUrlOutput = []; - $prUrlReturnCode = 0; - \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); - - if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) { - $prUrls[$language['name']] = trim($prUrlOutput[0]); - } - } else { - $updateErrorMessage = implode("\n", $updateOutput); - Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage); - } - } else { - Console::error("Failed to get PR number for {$language['name']} SDK"); - } - } else { - Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); - } - } - } - - \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); - Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); + if (! $git || empty($gitUrl)) { + goto copyExamples; } + // Generate commit message: use provided message, AI changelog, or fallback + if (! empty($message)) { + $commitMessage = $message; + } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; + } else { + $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; + } + Console::info("Preparing {$language['name']} SDK repository..."); + + try { + // Init fresh repo + \exec('rm -rf ' . \escapeshellarg($target)); + \mkdir($target, 0755, true); + + $gitClient = new Git(); + $repo = $gitClient->init($target); + + $repo->execute('config', 'core.ignorecase', 'false'); + $repo->execute('config', 'pull.rebase', 'false'); + $repo->execute('config', 'advice.defaultBranchName', 'false'); + $repo->addRemote('origin', $gitUrl); + + // Fetch and checkout base branch (or create if new repo) + try { + $repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch); + try { + $repo->execute('checkout', '-f', $repoBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + + try { + $repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags'); + } catch (\Throwable) { + } + + // Checkout dev branch (or create if it doesn't exist) + try { + $repo->execute('checkout', '-f', $gitBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $gitBranch); + } + + // Fetch dev branch, or push to create it on remote + try { + $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); + } catch (\Throwable) { + try { + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable) { + } + } + + // Sync with remote dev branch + try { + $repo->execute('reset', '--hard', "origin/{$gitBranch}"); + } catch (\Throwable) { + } + + // Backup .github before cleaning working tree + $githubDir = $target . '/.github'; + $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); + $hasGithubDir = \is_dir($githubDir); + if ($hasGithubDir) { + \exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup)); + } + + // Clean working tree + try { + $repo->execute('rm', '-rf', '--cached', '.'); + } catch (\Throwable) { + } + try { + $repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github'); + } catch (\Throwable) { + } + + // Copy generated SDK and restore .github + \exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/')); + + if ($hasGithubDir && \is_dir($githubBackup)) { + \exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null'); + \exec('rm -rf ' . \escapeshellarg($githubBackup)); + } + + // Stage, commit, push + $repo->addAllChanges(); + $repo->commit($commitMessage); + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable $e) { + Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + } + + Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); + + // Create or update pull request + $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; + $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..."); + + $prCommand = 'cd ' . $target . ' && \ + gh pr create \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($prTitle) . ' \ + --body ' . \escapeshellarg($prBody) . ' \ + --base ' . \escapeshellarg($repoBranch) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + 2>&1'; + + $prOutput = []; + $prReturnCode = 0; + \exec($prCommand, $prOutput, $prReturnCode); + + if ($prReturnCode === 0) { + Console::success("Successfully created pull request for {$language['name']} SDK"); + if (! empty($prOutput)) { + $prUrls[$language['name']] = end($prOutput); + } + } else { + $errorMessage = implode("\n", $prOutput); + if (strpos($errorMessage, 'already exists') === false) { + Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); + } else { + $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); + } + } + + \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); + Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); + + copyExamples: + $docDirectories = $language['docDirectories'] ?? ['']; if ($version === 'latest') { @@ -858,7 +865,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (empty(trim($responseContent))) { Console::warning('AI returned empty response'); - return null; } @@ -868,7 +874,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::warning('Failed to parse AI response as JSON: ' . json_last_error_msg()); Console::log('Raw response:'); Console::log($responseContent); - return null; } @@ -899,21 +904,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ]; } catch (\Throwable $e) { Console::error('Error generating version and changelog: ' . $e->getMessage()); - return null; } } - /** - * 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 * @@ -928,7 +922,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (! file_exists($configPath)) { Console::error("Config file not found: {$configPath}"); - return false; } @@ -944,11 +937,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 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; } } @@ -964,11 +955,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 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; } } @@ -1027,12 +1016,71 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (file_put_contents($changelogPath, $newContent) !== false) { Console::success("Updated changelog at {$changelogPath} with version {$version}"); - return true; } else { Console::error('Failed to write changelog file'); - return false; } } + + private function updateExistingPr(string $target, string $repoName, string $gitBranch, string $prTitle, string $prBody, string $sdkName, array &$prUrls): void + { + Console::warning("Pull request already exists for {$sdkName} SDK, updating title and body..."); + + $prNumberCommand = 'cd ' . $target . ' && \ + gh pr list \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + --json number \ + --jq ".[0].number" \ + 2>&1'; + + $prNumberOutput = []; + $prNumberReturnCode = 0; + \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); + + if ($prNumberReturnCode !== 0 || empty($prNumberOutput[0])) { + Console::error("Failed to get PR number for {$sdkName} SDK"); + return; + } + + $prNumber = trim($prNumberOutput[0]); + $apiPath = "/repos/{$repoName}/pulls/{$prNumber}"; + $updateCommand = 'cd ' . $target . ' && \ + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + ' . \escapeshellarg($apiPath) . ' \ + -f title=' . \escapeshellarg($prTitle) . ' \ + -f body=' . \escapeshellarg($prBody) . ' \ + 2>&1'; + + $updateOutput = []; + $updateReturnCode = 0; + \exec($updateCommand, $updateOutput, $updateReturnCode); + + if ($updateReturnCode !== 0) { + Console::error("Failed to update pull request for {$sdkName} SDK: " . implode("\n", $updateOutput)); + return; + } + + Console::success("Successfully updated pull request for {$sdkName} SDK"); + + $prUrlCommand = 'cd ' . $target . ' && \ + gh pr list \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + --json url \ + --jq ".[0].url" \ + 2>&1'; + + $prUrlOutput = []; + $prUrlReturnCode = 0; + \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); + + if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) { + $prUrls[$sdkName] = trim($prUrlOutput[0]); + } + } } From 50f9c6786212441eca886a90d63c7fa6afea9a62 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:10:39 +0530 Subject: [PATCH 07/10] fixes --- app/config/sdks.php | 2 +- src/Appwrite/Platform/Tasks/SDKs.php | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/config/sdks.php b/app/config/sdks.php index 77db161b5c..1a808aa10a 100644 --- a/app/config/sdks.php +++ b/app/config/sdks.php @@ -231,7 +231,7 @@ return [ 'url' => 'https://github.com/appwrite/sdk-for-cli', 'package' => 'https://www.npmjs.com/package/appwrite-cli', 'enabled' => true, - 'beta' => true, + 'beta' => false, 'dev' => false, 'hidden' => false, 'family' => APP_SDK_PLATFORM_CONSOLE, diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index c318e34686..ceb07d25cb 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -59,10 +59,10 @@ class SDKs extends Action protected function getSupportedSDKs(): array { - return \array_unique(\array_merge(...\array_map( + return \array_unique(\array_merge(...\array_values(\array_map( fn ($platform) => \array_column($platform['sdks'], 'key'), Config::getParam('sdks') - ))); + )))); } public function __construct() @@ -480,7 +480,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::info("Analyzing SDK changes with AI..."); $aiResult = $this->generateVersionAndChangelog($language, $result); - if ($aiResult !== null) { + if (!empty($aiResult['skip'])) { + Console::warning("Skipping {$language['name']} SDK generation"); + continue; + } elseif ($aiResult !== null) { $newVersion = $aiResult['version']; $newChangelog = $aiResult['changelog']; $aiChangelog = $newChangelog; // Store for PR description @@ -885,7 +888,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Guard: beta SDKs must not be bumped to >= 1.0.0 if ($isBeta && ($parsed['versionBump'] === 'major' || \version_compare($parsed['version'], '1.0.0', '>='))) { Console::warning("Beta SDK {$language['name']} cannot have a major bump or version >= 1.0.0 (AI suggested {$parsed['version']}), skipping"); - return null; + return ['skip' => true]; } Console::success("✓ Analysis complete"); From c497a686133738405315d78592b0330297db8167 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:33:41 +0530 Subject: [PATCH 08/10] fixes --- src/Appwrite/Platform/Tasks/SDKs.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index ceb07d25cb..40874c4879 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -491,19 +491,17 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Update the version in the config $this->updateSdkVersion($key, $language['key'], $newVersion); - // Update the changelog file + // Update the source 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); - } + // Re-read updated changelog so regeneration includes the new entry + $updatedChangelog = \file_get_contents($language['changelog']); + $sdk->setChangelog($updatedChangelog); // Reload the language config with updated values $language['version'] = $newVersion; - // Regenerate SDK with new version + // Regenerate SDK with new version and updated changelog $sdk->setVersion($newVersion); try { $sdk->generate($result); @@ -647,8 +645,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if ($prReturnCode === 0) { Console::success("Successfully created pull request for {$language['name']} SDK"); - if (! empty($prOutput)) { - $prUrls[$language['name']] = end($prOutput); + foreach ($prOutput as $line) { + if (\str_starts_with(trim($line), 'https://')) { + $prUrls[$language['name']] = trim($line); + break; + } } } else { $errorMessage = implode("\n", $prOutput); From 161c5af66f8b49f946eb8629c59d1532ac341e5b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 4 Mar 2026 19:35:30 +0530 Subject: [PATCH 09/10] fixes --- src/Appwrite/Platform/Tasks/SDKs.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 40874c4879..c9875e1194 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -616,6 +616,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); } catch (\Throwable $e) { Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + goto cleanupTarget; } Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); @@ -660,6 +661,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } } + cleanupTarget: \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); From 152de6c58488d4678ef4a243f79d6238086865cf Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 5 Mar 2026 09:32:07 +0530 Subject: [PATCH 10/10] remove goto --- src/Appwrite/Platform/Tasks/SDKs.php | 354 ++++++++++++++------------- 1 file changed, 186 insertions(+), 168 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index c9875e1194..528084f4ea 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -517,178 +517,26 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $gitBranch = $language['gitBranch']; $repoBranch = $language['repoBranch'] ?? 'main'; - if (! $git || empty($gitUrl)) { - goto copyExamples; - } - - // Generate commit message: use provided message, AI changelog, or fallback - if (! empty($message)) { - $commitMessage = $message; - } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { - $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; - } else { - $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; - } - Console::info("Preparing {$language['name']} SDK repository..."); - - try { - // Init fresh repo - \exec('rm -rf ' . \escapeshellarg($target)); - \mkdir($target, 0755, true); - - $gitClient = new Git(); - $repo = $gitClient->init($target); - - $repo->execute('config', 'core.ignorecase', 'false'); - $repo->execute('config', 'pull.rebase', 'false'); - $repo->execute('config', 'advice.defaultBranchName', 'false'); - $repo->addRemote('origin', $gitUrl); - - // Fetch and checkout base branch (or create if new repo) - try { - $repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch); - try { - $repo->execute('checkout', '-f', $repoBranch); - } catch (\Throwable) { - $repo->execute('checkout', '-b', $repoBranch); - } - } catch (\Throwable) { - $repo->execute('checkout', '-b', $repoBranch); - } - - try { - $repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags'); - } catch (\Throwable) { - } - - // Checkout dev branch (or create if it doesn't exist) - try { - $repo->execute('checkout', '-f', $gitBranch); - } catch (\Throwable) { - $repo->execute('checkout', '-b', $gitBranch); - } - - // Fetch dev branch, or push to create it on remote - try { - $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); - } catch (\Throwable) { - try { - $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); - } catch (\Throwable) { - } - } - - // Sync with remote dev branch - try { - $repo->execute('reset', '--hard', "origin/{$gitBranch}"); - } catch (\Throwable) { - } - - // Backup .github before cleaning working tree - $githubDir = $target . '/.github'; - $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); - $hasGithubDir = \is_dir($githubDir); - if ($hasGithubDir) { - \exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup)); - } - - // Clean working tree - try { - $repo->execute('rm', '-rf', '--cached', '.'); - } catch (\Throwable) { - } - try { - $repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github'); - } catch (\Throwable) { - } - - // Copy generated SDK and restore .github - \exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/')); - - if ($hasGithubDir && \is_dir($githubBackup)) { - \exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null'); - \exec('rm -rf ' . \escapeshellarg($githubBackup)); - } - - // Stage, commit, push - $repo->addAllChanges(); - $repo->commit($commitMessage); - $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); - } catch (\Throwable $e) { - Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); - goto cleanupTarget; - } - - Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); - - // Create or update pull request - $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; - $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..."); - - $prCommand = 'cd ' . $target . ' && \ - gh pr create \ - --repo ' . \escapeshellarg($repoName) . ' \ - --title ' . \escapeshellarg($prTitle) . ' \ - --body ' . \escapeshellarg($prBody) . ' \ - --base ' . \escapeshellarg($repoBranch) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - 2>&1'; - - $prOutput = []; - $prReturnCode = 0; - \exec($prCommand, $prOutput, $prReturnCode); - - if ($prReturnCode === 0) { - Console::success("Successfully created pull request for {$language['name']} SDK"); - foreach ($prOutput as $line) { - if (\str_starts_with(trim($line), 'https://')) { - $prUrls[$language['name']] = trim($line); - break; - } - } - } else { - $errorMessage = implode("\n", $prOutput); - if (strpos($errorMessage, 'already exists') === false) { - Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); + if ($git && !empty($gitUrl)) { + // Generate commit message: use provided message, AI changelog, or fallback + if (! empty($message)) { + $commitMessage = $message; + } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; } else { - $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); - } - } - - cleanupTarget: - \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); - Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); - - copyExamples: - - $docDirectories = $language['docDirectories'] ?? ['']; - - if ($version === 'latest') { - continue; - } - - foreach ($docDirectories as $languageTitle => $path) { - $languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : ''); - $examplesSource = $result . '/docs/examples' . $languagePath; - - if (! \is_dir($examplesSource)) { - Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy."); - - continue; + $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; } - \exec( - 'mkdir -p ' . $resultExamples . $languagePath . ' && \ - cp -r ' . $examplesSource . ' ' . $resultExamples - ); - Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}"); + $pushSuccess = $this->pushToGit($language, $target, $result, $gitUrl, $gitBranch, $repoBranch, $commitMessage); + + if ($pushSuccess) { + $this->createPullRequest($language, $target, $gitBranch, $repoBranch, $aiChangelog, $prUrls); + } + + $this->cleanupTarget($target, $language['name']); } + + $this->copyExamples($language, $version, $result, $resultExamples); } } @@ -702,6 +550,176 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } } + private function pushToGit(array $language, string $target, string $result, string $gitUrl, string $gitBranch, string $repoBranch, string $commitMessage): bool + { + Console::info("Preparing {$language['name']} SDK repository..."); + + try { + // Init fresh repo + \exec('rm -rf ' . \escapeshellarg($target)); + \mkdir($target, 0755, true); + + $gitClient = new Git(); + $repo = $gitClient->init($target); + + $repo->execute('config', 'core.ignorecase', 'false'); + $repo->execute('config', 'pull.rebase', 'false'); + $repo->execute('config', 'advice.defaultBranchName', 'false'); + $repo->addRemote('origin', $gitUrl); + + // Fetch and checkout base branch (or create if new repo) + try { + $repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch); + try { + $repo->execute('checkout', '-f', $repoBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + + try { + $repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags'); + } catch (\Throwable) { + } + + // Checkout dev branch (or create if it doesn't exist) + try { + $repo->execute('checkout', '-f', $gitBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $gitBranch); + } + + // Fetch dev branch, or push to create it on remote + try { + $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); + } catch (\Throwable) { + try { + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable) { + } + } + + // Sync with remote dev branch + try { + $repo->execute('reset', '--hard', "origin/{$gitBranch}"); + } catch (\Throwable) { + } + + // Backup .github before cleaning working tree + $githubDir = $target . '/.github'; + $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); + $hasGithubDir = \is_dir($githubDir); + if ($hasGithubDir) { + \exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup)); + } + + // Clean working tree + try { + $repo->execute('rm', '-rf', '--cached', '.'); + } catch (\Throwable) { + } + try { + $repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github'); + } catch (\Throwable) { + } + + // Copy generated SDK and restore .github + \exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/')); + + if ($hasGithubDir && \is_dir($githubBackup)) { + \exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null'); + \exec('rm -rf ' . \escapeshellarg($githubBackup)); + } + + // Stage, commit, push + $repo->addAllChanges(); + $repo->commit($commitMessage); + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable $e) { + Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + return false; + } + + Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); + return true; + } + + private function createPullRequest(array $language, string $target, string $gitBranch, string $repoBranch, string $aiChangelog, array &$prUrls): void + { + $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; + $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..."); + + $prCommand = 'cd ' . $target . ' && \ + gh pr create \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($prTitle) . ' \ + --body ' . \escapeshellarg($prBody) . ' \ + --base ' . \escapeshellarg($repoBranch) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + 2>&1'; + + $prOutput = []; + $prReturnCode = 0; + \exec($prCommand, $prOutput, $prReturnCode); + + if ($prReturnCode === 0) { + Console::success("Successfully created pull request for {$language['name']} SDK"); + foreach ($prOutput as $line) { + if (\str_starts_with(trim($line), 'https://')) { + $prUrls[$language['name']] = trim($line); + break; + } + } + } else { + $errorMessage = implode("\n", $prOutput); + if (strpos($errorMessage, 'already exists') === false) { + Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); + } else { + $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); + } + } + } + + private function cleanupTarget(string $target, string $languageName): void + { + \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); + Console::success("Remove temp directory '{$target}' for {$languageName} SDK"); + } + + private function copyExamples(array $language, string $version, string $result, string $resultExamples): void + { + $docDirectories = $language['docDirectories'] ?? ['']; + + if ($version === 'latest') { + return; + } + + foreach ($docDirectories as $languageTitle => $path) { + $languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : ''); + $examplesSource = $result . '/docs/examples' . $languagePath; + + if (! \is_dir($examplesSource)) { + Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy."); + + continue; + } + + \exec( + 'mkdir -p ' . $resultExamples . $languagePath . ' && \ + cp -r ' . $examplesSource . ' ' . $resultExamples + ); + Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}"); + } + } + /** * Extract release notes from changelog for a specific version */