feat(sdks): automate sdk version/changelog with assistant

This commit is contained in:
Chirag Aggarwal
2026-02-19 15:39:11 +05:30
parent f0574f5079
commit fe48882e6c
8 changed files with 561 additions and 124 deletions
+1 -1
View File
@@ -227,7 +227,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '13.4.0',
'version' => '13.5.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
+1
View File
@@ -46,6 +46,7 @@
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/agents": "1.1.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
Generated
+74 -20
View File
@@ -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": "dd9887a50fb434887461a99413007573",
"content-hash": "9ca31842e86c899e310497c38abb72c2",
"packages": [
{
"name": "adhocore/jwt",
@@ -3401,6 +3401,60 @@
},
"time": "2026-02-02T10:43:10+00:00"
},
{
"name": "utopia-php/agents",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/agents.git",
"reference": "77ec73c02f91a571aabee3f72ac5cd49a1d1f39c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/agents/zipball/77ec73c02f91a571aabee3f72ac5cd49a1d1f39c",
"reference": "77ec73c02f91a571aabee3f72ac5cd49a1d1f39c",
"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.1.1"
},
"time": "2026-02-19T08:42:23+00:00"
},
{
"name": "utopia-php/analytics",
"version": "0.15.2",
@@ -3797,16 +3851,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.1",
"version": "5.3.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "e3305f6ca64eb5dd715e30212fd32d5d834a3a46"
"reference": "19305fc487df4f7b990350463914c1113cc6dab1"
},
"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/19305fc487df4f7b990350463914c1113cc6dab1",
"reference": "19305fc487df4f7b990350463914c1113cc6dab1",
"shasum": ""
},
"require": {
@@ -3849,22 +3903,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.2"
},
"time": "2026-02-18T07:15:40+00:00"
"time": "2026-02-18T11:59:15+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 +3948,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",
@@ -5385,16 +5439,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.9.3",
"version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "5a8a304e404c84539f3ac6562c736f8c7ef2bef2"
"reference": "b3c878d618c87f21f959c13612e17efddce3c98d"
},
"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/b3c878d618c87f21f959c13612e17efddce3c98d",
"reference": "b3c878d618c87f21f959c13612e17efddce3c98d",
"shasum": ""
},
"require": {
@@ -5430,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.9.3"
"source": "https://github.com/appwrite/sdk-generator/tree/1.9.4"
},
"time": "2026-02-17T11:48:05+00:00"
"time": "2026-02-19T06:54:24+00:00"
},
{
"name": "doctrine/annotations",
@@ -8885,7 +8939,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -0,0 +1,7 @@
```bash
appwrite projects create-schedule \
--project-id <PROJECT_ID> \
--resource-type function \
--resource-id <RESOURCE_ID> \
--schedule ''
```
@@ -0,0 +1,5 @@
```bash
appwrite projects get-schedule \
--project-id <PROJECT_ID> \
--schedule-id <SCHEDULE_ID>
```
@@ -0,0 +1,4 @@
```bash
appwrite projects list-schedules \
--project-id <PROJECT_ID>
```
+5
View File
@@ -1,5 +1,10 @@
# Change Log
## 13.5.0
* New: Added CLI commands under projects: `list-schedules`, `create-schedule`, and `get-schedule` for managing project schedules.
* Packaging/build: Updated packaging to support explicit ESM/CJS outputs with exports. main now points to dist/index.cjs, module to dist/index.js, and exports include type and entry-point mappings. This enables better compatibility for both ESM and CommonJS users.
## 13.4.0
* Add `--queries` option to `list-keys` command
+464 -103
View File
@@ -24,9 +24,17 @@ use Appwrite\SDK\Language\Swift;
use Appwrite\SDK\Language\Web;
use Appwrite\SDK\SDK;
use Appwrite\Spec\Swagger2;
use Utopia\Agents\Adapters\OpenAI;
use Utopia\Agents\Agent;
use Utopia\Agents\Conversation;
use Utopia\Agents\Messages\Text as AgentText;
use Utopia\Agents\Roles\User;
use Utopia\Agents\Schema;
use Utopia\Agents\Schema\SchemaObject;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Platform\Action;
use Utopia\System\System;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -52,7 +60,7 @@ class SDKs extends Action
'graphql',
'rest',
'markdown',
'agent-skills'
'agent-skills',
];
public static function getName(): string
@@ -78,20 +86,21 @@ class SDKs extends Action
->param('commit', null, new Nullable(new WhiteList(['yes', 'no'])), 'Actually create releases (yes) or dry-run (no)?', optional: true)
->param('sdks', null, new Nullable(new Text(256)), 'Selected SDKs', optional: true)
->param('mode', 'full', new WhiteList(['full', 'examples']), 'Generation mode: full (default) or examples (only generate and copy examples)', optional: true)
->param('ai', 'yes', new Nullable(new WhiteList(['yes', 'no'])), 'Use AI to generate changelog (yes/no, default: yes if _APP_ASSISTANT_OPENAI_API_KEY is set)', optional: true)
->callback($this->action(...));
}
public function action(?string $platform, ?string $sdk, ?string $version, ?string $git, ?string $message, ?string $release, ?string $commit, ?string $sdks, string $mode): void
public function action(?string $platform, ?string $sdk, ?string $version, ?string $git, ?string $message, ?string $release, ?string $commit, ?string $sdks, string $mode, ?string $ai): void
{
$examplesOnly = ($mode === 'examples');
$selectedPlatform = $platform;
$selectedSDK = $sdk;
if (!$sdks) {
$selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '" or "*" for all):');
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));
if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $this->supportedSDKS)) {
throw new \Exception('Unknown SDK "'.$selectedSDK.'" given. Options are: '.implode(', ', $this->supportedSDKS));
}
} else {
$sdks = explode(',', $sdks);
@@ -99,9 +108,9 @@ class SDKs extends Action
$version ??= Console::confirm('Choose an Appwrite version');
$createRelease = ($release === 'yes');
$commitRelease = ($commit === 'yes');
$commitRelease = ($commit === 'yes');
if (!$createRelease && !$examplesOnly) {
if (! $createRelease && ! $examplesOnly) {
$git ??= Console::confirm('Should we use git push? (yes/no)');
$git = ($git === 'yes');
@@ -115,7 +124,7 @@ class SDKs extends Action
$prUrls = [];
}
if (!\in_array($version, [
if (! \in_array($version, [
'0.6.x',
'0.7.x',
'0.8.x',
@@ -135,7 +144,7 @@ class SDKs extends Action
'1.6.x',
'1.7.x',
'1.8.x',
'latest'
'latest',
])) {
throw new \Exception('Unknown version given');
}
@@ -147,34 +156,35 @@ class SDKs extends Action
}
foreach ($platform['sdks'] as $language) {
if ($selectedSDK !== $language['key'] && $selectedSDK !== '*' && ($sdks === null || !\in_array($language['key'], $sdks))) {
if ($selectedSDK !== $language['key'] && $selectedSDK !== '*' && ($sdks === null || ! \in_array($language['key'], $sdks))) {
continue;
}
if (!$language['enabled']) {
Console::warning($language['name'] . ' for ' . $platform['name'] . ' is disabled');
if (! $language['enabled']) {
Console::warning($language['name'].' for '.$platform['name'].' is disabled');
continue;
}
Console::info('Fetching API Spec for ' . $language['name'] . ' for ' . $platform['name'] . ' (version: ' . $version . ')');
Console::info('Fetching API Spec for '.$language['name'].' for '.$platform['name'].' (version: '.$version.')');
$spec = file_get_contents(__DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json');
$spec = file_get_contents(__DIR__.'/../../../../app/config/specs/swagger2-'.$version.'-'.$language['family'].'.json');
$cover = 'https://github.com/appwrite/appwrite/raw/main/public/images/github.png';
$result = \realpath(__DIR__ . '/../../../../app') . '/sdks/' . $key . '-' . $language['key'];
$resultExamples = \realpath(__DIR__ . '/../../../..') . '/docs/examples/' . $version . '/' . $key . '-' . $language['key'];
$target = \realpath(__DIR__ . '/../../../../app') . '/sdks/git/' . $language['key'] . '/';
$readme = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/README.md');
$result = \realpath(__DIR__.'/../../../../app').'/sdks/'.$key.'-'.$language['key'];
$resultExamples = \realpath(__DIR__.'/../../../..').'/docs/examples/'.$version.'/'.$key.'-'.$language['key'];
$target = \realpath(__DIR__.'/../../../../app').'/sdks/git/'.$language['key'].'/';
$readme = \realpath(__DIR__.'/../../../../docs/sdks/'.$language['key'].'/README.md');
$readme = ($readme) ? \file_get_contents($readme) : '';
$gettingStarted = $language['gettingStarted'] ?? \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/GETTING_STARTED.md');
$gettingStarted = $language['gettingStarted'] ?? \realpath(__DIR__.'/../../../../docs/sdks/'.$language['key'].'/GETTING_STARTED.md');
$gettingStarted = ($gettingStarted) ? \file_get_contents($gettingStarted) : '';
$examples = \realpath(__DIR__ . '/../../../../docs/sdks/' . $language['key'] . '/EXAMPLES.md');
$examples = \realpath(__DIR__.'/../../../../docs/sdks/'.$language['key'].'/EXAMPLES.md');
$examples = ($examples) ? \file_get_contents($examples) : '';
$changelog = $language['changelog'] ?? '';
$changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log';
$warning = '**This SDK is compatible with Appwrite server version ' . $version . '. For older versions, please check [previous releases](' . $language['url'] . '/releases).**';
$warning = '**This SDK is compatible with Appwrite server version '.$version.'. For older versions, please check [previous releases]('.$language['url'].'/releases).**';
$license = 'BSD-3-Clause';
$licenseContent = 'Copyright (c) ' . date('Y') . ' Appwrite (https://appwrite.io) and individual contributors.
$licenseContent = 'Copyright (c) '.date('Y').' Appwrite (https://appwrite.io) and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
@@ -228,7 +238,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$config = new Node();
$config->setNPMPackage('node-appwrite');
$config->setBowerPackage('appwrite');
$warning = $warning . "\n\n > This is the Node.js SDK for integrating with Appwrite from your Node.js server-side code.
$warning = $warning."\n\n > This is the Node.js SDK for integrating with Appwrite from your Node.js server-side code.
If you're looking to integrate from the browser, you should check [appwrite/sdk-for-web](https://github.com/appwrite/sdk-for-web)";
break;
case 'deno':
@@ -258,14 +268,14 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
case 'dart':
$config = new Dart();
$config->setPackageName('dart_appwrite');
$warning = $warning . "\n\n > This is the Dart SDK for integrating with Appwrite from your Dart server-side code. If you're looking for the Flutter SDK you should check [appwrite/sdk-for-flutter](https://github.com/appwrite/sdk-for-flutter)";
$warning = $warning."\n\n > This is the Dart SDK for integrating with Appwrite from your Dart server-side code. If you're looking for the Flutter SDK you should check [appwrite/sdk-for-flutter](https://github.com/appwrite/sdk-for-flutter)";
break;
case 'go':
$config = new Go();
break;
case 'swift':
$config = new Swift();
$warning = $warning . "\n\n > This is the Swift SDK for integrating with Appwrite from your Swift server-side code. If you're looking for the Apple SDK you should check [appwrite/sdk-for-apple](https://github.com/appwrite/sdk-for-apple)";
$warning = $warning."\n\n > This is the Swift SDK for integrating with Appwrite from your Swift server-side code. If you're looking for the Apple SDK you should check [appwrite/sdk-for-apple](https://github.com/appwrite/sdk-for-apple)";
break;
case 'apple':
$config = new Apple();
@@ -279,7 +289,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
break;
case 'kotlin':
$config = new Kotlin();
$warning = $warning . "\n\n > This is the Kotlin SDK for integrating with Appwrite from your Kotlin server-side code. If you're looking for the Android SDK you should check [appwrite/sdk-for-android](https://github.com/appwrite/sdk-for-android)";
$warning = $warning."\n\n > This is the Kotlin SDK for integrating with Appwrite from your Kotlin server-side code. If you're looking for the Android SDK you should check [appwrite/sdk-for-android](https://github.com/appwrite/sdk-for-android)";
break;
case 'graphql':
$config = new GraphQL();
@@ -295,14 +305,14 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$config = new AgentSkills();
break;
default:
throw new \Exception('Language "' . $language['key'] . '" not supported');
throw new \Exception('Language "'.$language['key'].'" not supported');
}
if ($createRelease && !$examplesOnly) {
if ($createRelease && ! $examplesOnly) {
$repoName = $language['gitUserName'].'/'.$language['gitRepoName'];
$releaseVersion = $language['version'];
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
$releaseNotes = $this->extractReleaseNotes($changelog, $releaseVersion);
if (empty($releaseNotes)) {
$releaseNotes = "Release version {$releaseVersion}";
}
@@ -312,60 +322,63 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
if ($repoName === '/') {
Console::warning("{$language['name']} SDK is not an SDK, skipping release");
continue;
}
// Check if release already exists
$checkReleaseCommand = 'gh release view "' . $releaseVersion . '" --repo "' . $repoName . '" --json url --jq ".url" 2>/dev/null';
$checkReleaseCommand = 'gh release view "'.$releaseVersion.'" --repo "'.$repoName.'" --json url --jq ".url" 2>/dev/null';
$existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? '');
if (!empty($existingReleaseUrl)) {
if (! empty($existingReleaseUrl)) {
Console::warning("Release {$releaseVersion} already exists for {$language['name']} SDK, skipping...");
Console::info("Existing release: {$existingReleaseUrl}");
continue;
}
// Check if the latest commit on the target branch already has a release
$latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null';
$latestCommitCommand = 'gh api repos/'.$repoName.'/commits/'.$releaseTarget.' --jq ".sha" 2>/dev/null';
$latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? '');
if (!empty($latestCommitSha)) {
$latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null';
if (! empty($latestCommitSha)) {
$latestReleaseTagCommand = 'gh api repos/'.$repoName.'/releases --jq ".[0] | .tag_name" 2>/dev/null';
$latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? '');
if (!empty($latestReleaseTag)) {
$tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null';
if (! empty($latestReleaseTag)) {
$tagCommitCommand = 'gh api repos/'.$repoName.'/git/ref/tags/'.$latestReleaseTag.' --jq ".object.sha" 2>/dev/null';
$tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? '');
if (!empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) {
if (! empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) {
Console::warning("Latest commit on {$releaseTarget} already has a release ({$latestReleaseTag}) for {$language['name']} SDK, skipping to avoid empty release...");
continue;
}
}
}
$previousVersion = '';
$tagListCommand = 'gh release list --repo "' . $repoName . '" --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
$tagListCommand = 'gh release list --repo "'.$repoName.'" --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
$previousVersion = trim(\shell_exec($tagListCommand) ?? '');
$formattedNotes = "## What's Changed\n\n";
$formattedNotes .= $releaseNotes . "\n\n";
$formattedNotes .= $releaseNotes."\n\n";
if (!empty($previousVersion)) {
$formattedNotes .= "**Full Changelog**: https://github.com/" . $repoName . "/compare/" . $previousVersion . "..." . $releaseVersion;
if (! empty($previousVersion)) {
$formattedNotes .= '**Full Changelog**: https://github.com/'.$repoName.'/compare/'.$previousVersion.'...'.$releaseVersion;
} else {
$formattedNotes .= "**Full Changelog**: https://github.com/" . $repoName . "/releases/tag/" . $releaseVersion;
$formattedNotes .= '**Full Changelog**: https://github.com/'.$repoName.'/releases/tag/'.$releaseVersion;
}
if (!$commitRelease) {
if (! $commitRelease) {
Console::info("[DRY RUN] Would create release for {$language['name']} SDK:");
Console::log(" Repository: {$repoName}");
Console::log(" Version: {$releaseVersion}");
Console::log(" Title: {$releaseTitle}");
Console::log(" Target Branch: {$releaseTarget}");
Console::log(" Previous Version: " . ($previousVersion ?: 'N/A'));
Console::log(" Release Notes:");
Console::log(" " . str_replace("\n", "\n ", $formattedNotes));
Console::log(' Previous Version: '.($previousVersion ?: 'N/A'));
Console::log(' Release Notes:');
Console::log(' '.str_replace("\n", "\n ", $formattedNotes));
Console::log('');
} else {
Console::info("Creating release {$releaseVersion} for {$language['name']} SDK...");
@@ -373,11 +386,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 "'.$releaseVersion.'" \
--repo "'.$repoName.'" \
--title "'.$releaseTitle.'" \
--notes-file "'.$tempNotesFile.'" \
--target "'.$releaseTarget.'" \
2>&1';
$releaseOutput = [];
@@ -397,14 +410,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
Console::success("Successfully created release {$releaseVersion} for {$language['name']} SDK");
if (!empty($releaseUrl)) {
if (! empty($releaseUrl)) {
Console::info("Release URL: {$releaseUrl}");
}
} else {
$errorMessage = implode("\n", $releaseOutput);
Console::error("Failed to create release for {$language['name']} SDK: " . $errorMessage);
Console::error("Failed to create release for {$language['name']} SDK: ".$errorMessage);
}
}
continue;
}
@@ -449,7 +463,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// Make sure we have a clean slate.
// Otherwise, all files in this dir will be pushed,
// regardless of whether they were just generated or not.
\exec('chmod -R u+w ' . $result . ' 2>/dev/null; rm -rf ' . $result);
\exec('chmod -R u+w '.$result.' 2>/dev/null; rm -rf '.$result);
try {
$sdk->generate($result);
@@ -457,49 +471,87 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
Console::error($exception->getMessage());
}
// Use AI to determine version bump and changelog if _APP_ASSISTANT_OPENAI_API_KEY is set and --ai is not no
// This uses the already generated SDK to compare with the remote repo
$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...");
$aiResult = $this->generateVersionAndChangelog($language, $result);
if ($aiResult !== null) {
$newVersion = $aiResult['version'];
$newChangelog = $aiResult['changelog'];
$aiChangelog = $newChangelog; // Store for PR description
// Update the version in the config
$this->updateSdkVersion($key, $language['key'], $newVersion);
// Update the changelog file
$this->updateChangelogFile($language['changelog'], $newVersion, $newChangelog);
// Reload the language config with updated values
$language['version'] = $newVersion;
// Regenerate SDK with new version
$sdk->setVersion($newVersion);
try {
$sdk->generate($result);
} 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');
}
}
$gitUrl = $language['gitUrl'];
$gitBranch = $language['gitBranch'];
$repoBranch = $language['repoBranch'] ?? 'main';
if ($git && !empty($gitUrl)) {
\exec('rm -rf ' . $target . ' && \
mkdir -p ' . $target . ' && \
cd ' . $target . ' && \
if ($git && ! empty($gitUrl)) {
\exec('rm -rf '.$target.' && \
mkdir -p '.$target.' && \
cd '.$target.' && \
git init && \
git config core.ignorecase false && \
git config pull.rebase false && \
git remote add origin ' . $gitUrl . ' && \
git remote add origin '.$gitUrl.' && \
git fetch origin && \
(git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \
git pull origin ' . $repoBranch . ' && \
(git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \
(git fetch origin ' . $gitBranch . ' 2>/dev/null || git push -u origin ' . $gitBranch . ') && \
git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \
(git checkout -f '.$repoBranch.' 2>/dev/null || git checkout -b '.$repoBranch.') && \
git pull origin '.$repoBranch.' && \
(git checkout -f '.$gitBranch.' 2>/dev/null || git checkout -b '.$gitBranch.') && \
(git fetch origin '.$gitBranch.' 2>/dev/null || git push -u origin '.$gitBranch.') && \
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 && \
cp -r ' . $result . '/. ' . $target . '/ && \
cp -r '.$result.'/. '.$target.'/ && \
(test -d /tmp/.github-backup-$$ && cp -rn /tmp/.github-backup-$$/.github . && rm -rf /tmp/.github-backup-$$ || true) && \
git add -A && \
git commit -m "' . $message . '" && \
git push -u origin ' . $gitBranch . '
git commit -m "'.$message.'" && \
git push -u origin '.$gitBranch.'
');
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']}.";
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
$repoName = $language['gitUserName'].'/'.$language['gitRepoName'];
Console::info("Creating pull request for {$language['name']} SDK...");
$prCommand = 'cd ' . $target . ' && \
$prCommand = 'cd '.$target.' && \
gh pr create \
--repo "' . $repoName . '" \
--title "' . $prTitle . '" \
--body "' . $prBody . '" \
--base "' . $repoBranch . '" \
--head "' . $gitBranch . '" \
--repo "'.$repoName.'" \
--title "'.$prTitle.'" \
--body "'.$prBody.'" \
--base "'.$repoBranch.'" \
--head "'.$gitBranch.'" \
2>&1';
$prOutput = [];
@@ -508,17 +560,17 @@ 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)) {
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 . ' && \
$prNumberCommand = 'cd '.$target.' && \
gh pr list \
--repo "' . $repoName . '" \
--head "' . $gitBranch . '" \
--repo "'.$repoName.'" \
--head "'.$gitBranch.'" \
--json number \
--jq ".[0].number" \
2>&1';
@@ -527,18 +579,18 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$prNumberReturnCode = 0;
\exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode);
if ($prNumberReturnCode === 0 && !empty($prNumberOutput[0])) {
if ($prNumberReturnCode === 0 && ! empty($prNumberOutput[0])) {
$prNumber = trim($prNumberOutput[0]);
// Use API directly to update PR to avoid deprecated projectCards field
$updateCommand = 'cd ' . $target . ' && \
$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 . '" \
/repos/'.$repoName.'/pulls/'.$prNumber.' \
-f title="'.$prTitle.'" \
-f body="'.$prBody.'" \
2>&1';
$updateOutput = [];
@@ -548,10 +600,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
if ($updateReturnCode === 0) {
Console::success("Successfully updated pull request for {$language['name']} SDK");
$prUrlCommand = 'cd ' . $target . ' && \
$prUrlCommand = 'cd '.$target.' && \
gh pr list \
--repo "' . $repoName . '" \
--head "' . $gitBranch . '" \
--repo "'.$repoName.'" \
--head "'.$gitBranch.'" \
--json url \
--jq ".[0].url" \
2>&1';
@@ -560,23 +612,23 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$prUrlReturnCode = 0;
\exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode);
if ($prUrlReturnCode === 0 && !empty($prUrlOutput)) {
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);
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);
Console::error("Failed to create pull request for {$language['name']} SDK: ".$errorMessage);
}
}
}
\exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target);
\exec('chmod -R u+w '.$target.' && rm -rf '.$target);
Console::success("Remove temp directory '{$target}' for {$language['name']} SDK");
}
@@ -587,24 +639,25 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
foreach ($docDirectories as $languageTitle => $path) {
$languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : '');
$examplesSource = $result . '/docs/examples' . $languagePath;
$languagePath = strtolower($languageTitle !== 0 ? '/'.$languageTitle : '');
$examplesSource = $result.'/docs/examples'.$languagePath;
if (!\is_dir($examplesSource)) {
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
'mkdir -p '.$resultExamples.$languagePath.' && \
cp -r '.$examplesSource.' '.$resultExamples
);
Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}");
}
}
}
if (!empty($prUrls)) {
if (! empty($prUrls)) {
Console::log('');
Console::log('Pull Request Summary');
foreach ($prUrls as $sdkName => $url) {
@@ -616,10 +669,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
/**
* Extract release notes from changelog for a specific version
*
* @param string $changelog
* @param string $version
* @return string
*/
private function extractReleaseNotes(string $changelog, string $version): string
{
@@ -628,7 +677,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
// Changelog version header pattern: ## 0.14.0
$pattern = '/^##\s+' . preg_quote($version, '/') . '\s*$/m';
$pattern = '/^##\s+'.preg_quote($version, '/').'\s*$/m';
$startPos = false;
if (preg_match($pattern, $changelog, $matches, PREG_OFFSET_CAPTURE)) {
$startPos = $matches[0][1];
@@ -656,4 +705,316 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
return trim($notes);
}
/**
* Compare generated SDK with remote repo and use AI to determine version bump and changelog
*
* @param array $language SDK language configuration
* @param string $generatedSdkPath Path to the already generated SDK
* @return array|null ['version' => string, 'changelog' => string] or null on failure
*/
private function generateVersionAndChangelog(array $language, string $generatedSdkPath): ?array
{
$tempDir = sys_get_temp_dir();
$repoClonePath = $tempDir.'/appwrite-sdk-repo-'.$language['key'].'-'.uniqid();
try {
// Clone the existing SDK repo
$repoBranch = $language['repoBranch'] ?? 'main';
$gitUrl = $language['gitUrl'];
if (empty($gitUrl)) {
Console::warning("No git URL for {$language['name']} SDK, skipping AI analysis");
return null;
}
Console::info("Cloning {$language['name']} SDK repository...");
$cloneCommand = "git clone --depth 1 --branch {$repoBranch} {$gitUrl} {$repoClonePath} 2>&1";
exec($cloneCommand, $cloneOutput, $cloneReturnCode);
if ($cloneReturnCode !== 0) {
Console::error("Failed to clone {$language['name']} SDK repository: ".implode("\n", $cloneOutput));
return null;
}
// Copy generated SDK into the cloned repo to see actual changes
Console::info('Copying generated SDK into cloned repo...');
// Remove all files except .git directory, then copy generated files
exec("cd {$repoClonePath} && find . -not -path './.git/*' -not -path './.git' -delete 2>/dev/null");
exec("cp -r {$generatedSdkPath}/* {$repoClonePath}/ 2>&1", $copyOutput, $copyReturnCode);
if ($copyReturnCode !== 0) {
Console::error('Failed to copy generated SDK: '.implode("\n", $copyOutput));
exec("rm -rf {$repoClonePath}");
return null;
}
// Generate diff using git diff within the repo
Console::info("Generating diff for {$language['name']} SDK...");
$diffCommand = "cd {$repoClonePath} && git diff --ignore-all-space --ignore-blank-lines 2>&1 | head -n 500";
exec($diffCommand, $diffOutput, $diffReturnCode);
$diff = implode("\n", $diffOutput);
// Log diff size for debugging
$diffSize = strlen($diff);
$diffLines = count($diffOutput);
Console::log("Diff size: {$diffSize} bytes, {$diffLines} lines");
// Clean up temp repo clone (don't remove generatedSdkPath as it's the main result dir)
exec("rm -rf {$repoClonePath}");
if (empty($diff)) {
Console::warning("No changes detected for {$language['name']} SDK");
return null;
}
// Use AI to analyze the diff and determine version/changelog
return $this->analyzeDiffWithAI($diff, $language['version'], $language['name']);
} catch (\Throwable $e) {
Console::error('Error generating version and changelog: '.$e->getMessage());
// Clean up on error (don't remove generatedSdkPath as it's the main result dir)
exec("rm -rf {$repoClonePath}");
return null;
}
}
/**
* Use AI to analyze diff and determine version bump and changelog
*
* @param string $diff Git diff between current SDK and generated SDK
* @param string $currentVersion Current SDK version
* @param string $sdkName SDK name
* @return array|null ['version' => string, 'changelog' => string] or null on failure
*/
private function analyzeDiffWithAI(string $diff, string $currentVersion, string $sdkName): ?array
{
$apiKey = System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', '');
if (empty($apiKey)) {
Console::warning('_APP_ASSISTANT_OPENAI_API_KEY not set, cannot use AI for version analysis');
return null;
}
try {
// Use larger maxTokens to handle large diffs
$adapter = new OpenAI($apiKey, OpenAI::MODEL_GPT_5_NANO, maxTokens: 8192);
$agent = new Agent($adapter);
// Define schema for structured output
$object = new SchemaObject();
$object->addProperty('version', [
'type' => SchemaObject::TYPE_STRING,
'description' => 'The new version number following semantic versioning (e.g., 1.2.3)',
]);
$object->addProperty('versionBump', [
'type' => SchemaObject::TYPE_STRING,
'description' => 'The type of version bump: major, minor, or patch',
'enum' => ['major', 'minor', 'patch'],
]);
$object->addProperty('changelog', [
'type' => SchemaObject::TYPE_STRING,
'description' => 'Changelog entries as bullet points, one per line, starting with *',
]);
$schema = new Schema(
name: 'sdk_release_analysis',
description: 'Analyze SDK changes and determine version bump and changelog',
object: $object,
required: $object->getNames()
);
$agent->setSchema($schema);
$agent->setInstructions([
'description' => 'You are an expert software engineer analyzing SDK code changes to determine semantic versioning and generate changelogs.',
'tone' => 'professional and technical',
]);
$prompt = <<<PROMPT
Analyze the following git diff for the {$sdkName} SDK and determine:
1. The appropriate version bump (major, minor, or patch) based on semantic versioning rules:
- MAJOR: Breaking changes that are not backward compatible
- MINOR: New features that are backward compatible
- PATCH: Bug fixes and minor improvements that are backward compatible
2. The new version number (current version is {$currentVersion})
3. A changelog describing the changes in a clear, user-friendly format
Git diff (truncated to 500 lines):
```diff
{$diff}
```
Provide your analysis in the requested JSON format.
PROMPT;
$user = new User('sdk-analyst');
$conversation = new Conversation($agent);
$conversation->message($user, new AgentText($prompt));
// Log prompt size for debugging
$promptSize = strlen($prompt);
Console::log("Sending prompt to AI (size: {$promptSize} bytes)...");
try {
$response = $conversation->send();
} catch (\Throwable $e) {
Console::error('AI API call failed: '.$e->getMessage());
Console::log('Exception trace: '.$e->getTraceAsString());
return null;
}
if (empty($response)) {
Console::warning('AI returned empty response');
return null;
}
// Get the content from the Message object and parse the response
$responseContent = $response->getContent();
Console::log('AI raw response:');
Console::log($responseContent);
Console::log('--- End of AI response ---');
$result = 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($responseContent);
return null;
}
if (empty($result['version']) || empty($result['changelog'])) {
Console::warning('AI response missing required fields');
return null;
}
Console::info("AI analysis complete - Version bump: {$result['versionBump']}, New version: {$result['version']}");
return [
'version' => $result['version'],
'changelog' => $result['changelog'],
'versionBump' => $result['versionBump'],
];
} catch (\Throwable $e) {
Console::error('AI analysis failed: '.$e->getMessage());
return null;
}
}
/**
* Update SDK version in the config file
*
* @param string $platform Platform key
* @param string $sdkKey SDK key
* @param string $newVersion New version number
* @return bool Success status
*/
private function updateSdkVersion(string $platform, string $sdkKey, string $newVersion): bool
{
$configPath = __DIR__.'/../../../../app/config/sdks.php';
if (! file_exists($configPath)) {
Console::error("Config file not found: {$configPath}");
return false;
}
$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';
if (preg_match($pattern, $content, $matches)) {
$oldVersion = $matches[2];
$newContent = preg_replace($pattern, '${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;
}
} else {
Console::warning("Could not find version entry for {$sdkKey} in config");
return false;
}
}
/**
* Update changelog file with new version entry
*
* @param string $changelogPath Path to changelog file
* @param string $version New version number
* @param string $notes Changelog notes
* @return bool Success status
*/
private function updateChangelogFile(string $changelogPath, string $version, string $notes): bool
{
if (empty($changelogPath) || ! file_exists($changelogPath)) {
Console::warning("Changelog file not found: {$changelogPath}");
return false;
}
$content = file_get_contents($changelogPath);
// Check if version already exists
if (strpos($content, "## {$version}") !== false) {
Console::warning("Version {$version} already exists in changelog, skipping update");
return false;
}
// Prepare new entry - trim notes to avoid extra newlines
$notes = rtrim($notes);
$newEntry = "## {$version}\n\n{$notes}";
// Insert after the header (first line)
$lines = explode("\n", $content);
$newLines = [];
$headerAdded = false;
foreach ($lines as $line) {
$newLines[] = $line;
// Add the new entry after the "# Change Log" header
if (! $headerAdded && strpos($line, '# Change Log') !== false) {
$newLines[] = '';
$newLines[] = $newEntry;
$headerAdded = true;
}
}
$newContent = implode("\n", $newLines);
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;
}
}
}