mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge pull request #11442 from appwrite/fix/sdk-beta-versioning
refactor: improve SDK generation task reliability and maintainability
This commit is contained in:
+1
-1
@@ -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,
|
||||
|
||||
@@ -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.*"
|
||||
},
|
||||
|
||||
Generated
+66
-2
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,29 +42,6 @@ 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'
|
||||
];
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'sdks';
|
||||
@@ -74,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_values(\array_map(
|
||||
fn ($platform) => \array_column($platform['sdks'], 'key'),
|
||||
Config::getParam('sdks')
|
||||
))));
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
@@ -100,8 +91,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);
|
||||
@@ -117,9 +109,6 @@ class SDKs extends Action
|
||||
|
||||
$prUrls = [];
|
||||
|
||||
if ($git) {
|
||||
$message ??= Console::confirm('Please enter your commit message:');
|
||||
}
|
||||
} elseif ($examplesOnly) {
|
||||
$git = false;
|
||||
$prUrls = [];
|
||||
@@ -337,7 +326,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)) {
|
||||
@@ -368,7 +357,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";
|
||||
@@ -396,11 +385,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 = [];
|
||||
@@ -491,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
|
||||
@@ -499,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);
|
||||
@@ -527,159 +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)) {
|
||||
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 "' . $message . '" --quiet && \
|
||||
git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true
|
||||
', $gitOutput, $gitReturnCode);
|
||||
|
||||
if ($gitReturnCode !== 0) {
|
||||
Console::warning("Git operations completed with warnings (exit code: {$gitReturnCode})");
|
||||
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::success("Pushed {$language['name']} SDK to {$gitUrl}");
|
||||
if ($git) {
|
||||
$prTitle = "feat: {$language['name']} SDK update for version {$language['version']}";
|
||||
$pushSuccess = $this->pushToGit($language, $target, $result, $gitUrl, $gitBranch, $repoBranch, $commitMessage);
|
||||
|
||||
// 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 "' . $repoName . '" \
|
||||
--title "' . $prTitle . '" \
|
||||
--body "' . $prBody . '" \
|
||||
--base "' . $repoBranch . '" \
|
||||
--head "' . $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 "' . $repoName . '" \
|
||||
--head "' . $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
|
||||
$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 . '" \
|
||||
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 "' . $repoName . '" \
|
||||
--head "' . $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);
|
||||
}
|
||||
}
|
||||
if ($pushSuccess) {
|
||||
$this->createPullRequest($language, $target, $gitBranch, $repoBranch, $aiChangelog, $prUrls);
|
||||
}
|
||||
|
||||
\exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target);
|
||||
Console::success("Remove temp directory '{$target}' for {$language['name']} SDK");
|
||||
$this->cleanupTarget($target, $language['name']);
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
\exec(
|
||||
'mkdir -p ' . $resultExamples . $languagePath . ' && \
|
||||
cp -r ' . $examplesSource . ' ' . $resultExamples
|
||||
);
|
||||
Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}");
|
||||
}
|
||||
$this->copyExamples($language, $version, $result, $resultExamples);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,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
|
||||
*/
|
||||
@@ -780,20 +807,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 = <<<PROMPT
|
||||
You are a technical writer generating a changelog for the {$language['name']} SDK release.
|
||||
|
||||
|
||||
Analyze the git diff below and return a JSON response with the version bump type, new version number, and changelog.
|
||||
|
||||
|
||||
## Versioning
|
||||
|
||||
|
||||
Current version: {$language['version']}
|
||||
|
||||
|
||||
Determine the semantic version bump:
|
||||
- `major`: Breaking changes (removed/renamed public APIs, changed method signatures, dropped support)
|
||||
- `minor`: New features that are backward-compatible (new methods, new optional parameters, new classes)
|
||||
- `patch`: Bug fixes, documentation updates, refactors with no API surface change
|
||||
|
||||
{$betaNote}
|
||||
When multiple change types are present, use the highest severity bump.
|
||||
|
||||
## Changelog guidelines
|
||||
@@ -810,6 +842,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
- Only include changes visible to SDK users (public API, behavior, docs, examples, CLI)
|
||||
- Ignore: CI/CD pipelines (.github/), internal tooling, code formatting, test infrastructure
|
||||
- Consolidate related changes into one entry (e.g., "Added `timeout`, `retries`, and `baseUrl` options" not three separate lines)
|
||||
- Wrap all method names, parameter names, class names, and code identifiers in backticks (e.g., `listDocuments`, `ttl`)
|
||||
- If the diff contains zero user-facing changes, return a single entry: "No user-facing SDK changes"
|
||||
- Do not speculate — only document what the diff explicitly shows
|
||||
|
||||
@@ -856,7 +889,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;
|
||||
}
|
||||
|
||||
@@ -866,7 +898,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;
|
||||
}
|
||||
|
||||
@@ -875,6 +906,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 ['skip' => true];
|
||||
}
|
||||
|
||||
Console::success("✓ Analysis complete");
|
||||
Console::log(" Version: {$language['version']} → {$parsed['version']} ({$parsed['versionBump']} bump)");
|
||||
Console::log(" Changelog:");
|
||||
@@ -891,21 +928,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
|
||||
*
|
||||
@@ -920,7 +946,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;
|
||||
}
|
||||
|
||||
@@ -936,11 +961,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;
|
||||
}
|
||||
}
|
||||
@@ -956,11 +979,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;
|
||||
}
|
||||
}
|
||||
@@ -1019,12 +1040,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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user