mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
619 lines
28 KiB
PHP
619 lines
28 KiB
PHP
<?php
|
|
|
|
namespace Appwrite\Platform\Tasks;
|
|
|
|
use Appwrite\SDK\AuthType;
|
|
use Appwrite\SDK\Method;
|
|
use Appwrite\SDK\Specification\Format\OpenAPI3;
|
|
use Appwrite\SDK\Specification\Format\Swagger2;
|
|
use Appwrite\SDK\Specification\Specification;
|
|
use Appwrite\Utopia\Request as AppwriteRequest;
|
|
use Appwrite\Utopia\Response as AppwriteResponse;
|
|
use Exception;
|
|
use Swoole\Http\Request as SwooleRequest;
|
|
use Swoole\Http\Response as SwooleResponse;
|
|
use Utopia\Cache\Adapter\None;
|
|
use Utopia\Cache\Cache;
|
|
use Utopia\Config\Config;
|
|
use Utopia\Console;
|
|
use Utopia\Database\Adapter\MySQL;
|
|
use Utopia\Database\Database;
|
|
use Utopia\Http\Http;
|
|
use Utopia\Http\Request as UtopiaRequest;
|
|
use Utopia\Http\Response as UtopiaResponse;
|
|
use Utopia\Platform\Action;
|
|
use Utopia\System\System;
|
|
use Utopia\Validator\Nullable;
|
|
use Utopia\Validator\Text;
|
|
use Utopia\Validator\WhiteList;
|
|
|
|
class Specs extends Action
|
|
{
|
|
public function __construct()
|
|
{
|
|
$this
|
|
->desc('Generate Appwrite API specifications')
|
|
->param('version', 'latest', new Text(16), 'Spec version', true)
|
|
->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true)
|
|
->param('git', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we push to the specs repo?', optional: true)
|
|
->param('message', null, new Nullable(new Text(256)), 'Commit Message', optional: true)
|
|
->param('branch', null, new Nullable(new Text(256)), 'Target branch for PR (defaults to main)', optional: true)
|
|
->callback($this->action(...));
|
|
}
|
|
|
|
public static function getName(): string
|
|
{
|
|
return 'specs';
|
|
}
|
|
|
|
public function getRequest(): UtopiaRequest
|
|
{
|
|
return new AppwriteRequest(new SwooleRequest());
|
|
}
|
|
|
|
public function getResponse(): UtopiaResponse
|
|
{
|
|
return new AppwriteResponse(new SwooleResponse());
|
|
}
|
|
|
|
protected function getFormatInstance(string $format, array $arguments)
|
|
{
|
|
return match ($format) {
|
|
'swagger2' => new Swagger2(...$arguments),
|
|
'open-api3' => new OpenAPI3(...$arguments),
|
|
default => throw new Exception('Format not found: ' . $format)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Platforms
|
|
*
|
|
* @return array<string>
|
|
*/
|
|
public static function getPlatforms(): array
|
|
{
|
|
return [
|
|
APP_SDK_PLATFORM_CLIENT,
|
|
APP_SDK_PLATFORM_SERVER,
|
|
APP_SDK_PLATFORM_CONSOLE,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Platforms to include in PR creation.
|
|
* Override in a subclass to exclude specific platforms.
|
|
*
|
|
* @return array<string>
|
|
*/
|
|
public static function getPlatformsForPR(): array
|
|
{
|
|
return static::getPlatforms();
|
|
}
|
|
|
|
/**
|
|
* Build the CLI command used to regenerate SDK examples.
|
|
* Override in a subclass to customise flags (platform, sdk, mode, etc.).
|
|
*
|
|
* @param string $version Spec version being generated
|
|
* @return string Shell command string (including 2>&1 redirect)
|
|
*/
|
|
protected function getSdksCommand(string $version): string
|
|
{
|
|
$cli = \realpath(__DIR__ . '/../../../../app') . '/cli.php';
|
|
|
|
return 'php ' . \escapeshellarg($cli)
|
|
. ' sdks --platform=* --sdk=* --version=' . \escapeshellarg($version)
|
|
. ' --git=no --mode=examples 2>&1';
|
|
}
|
|
|
|
/**
|
|
* Number of authentication methods supported by each platform
|
|
* client: 1 (Session or JWT), server: 2 (Key and JWT), console: 1 (Admin)
|
|
*
|
|
* @return array{client: int, console: int, server: int}
|
|
*/
|
|
protected function getAuthCounts(): array
|
|
{
|
|
return [
|
|
'client' => 1,
|
|
'server' => 2,
|
|
'console' => 1,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Keys for each platform
|
|
*
|
|
* @return array{client: array, server: array, console: array}
|
|
*/
|
|
protected function getKeys(): array
|
|
{
|
|
return [
|
|
APP_SDK_PLATFORM_CLIENT => [
|
|
'Project' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Project',
|
|
'description' => 'Your project ID',
|
|
'in' => 'header',
|
|
],
|
|
'JWT' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-JWT',
|
|
'description' => 'Your secret JSON Web Token',
|
|
'in' => 'header',
|
|
],
|
|
'Locale' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Locale',
|
|
'description' => '',
|
|
'in' => 'header',
|
|
],
|
|
'Session' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Session',
|
|
'description' => 'The user session to authenticate with',
|
|
'in' => 'header',
|
|
],
|
|
'DevKey' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Dev-Key',
|
|
'description' => 'Your secret dev API key',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserId' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Id',
|
|
'description' => 'Impersonate a user by ID on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserEmail' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Email',
|
|
'description' => 'Impersonate a user by email on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserPhone' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Phone',
|
|
'description' => 'Impersonate a user by phone on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
],
|
|
APP_SDK_PLATFORM_SERVER => [
|
|
'Project' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Project',
|
|
'description' => 'Your project ID',
|
|
'in' => 'header',
|
|
],
|
|
'Key' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Key',
|
|
'description' => 'Your secret API key',
|
|
'in' => 'header',
|
|
],
|
|
'JWT' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-JWT',
|
|
'description' => 'Your secret JSON Web Token',
|
|
'in' => 'header',
|
|
],
|
|
'Locale' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Locale',
|
|
'description' => '',
|
|
'in' => 'header',
|
|
],
|
|
'Session' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Session',
|
|
'description' => 'The user session to authenticate with',
|
|
'in' => 'header',
|
|
],
|
|
'ForwardedUserAgent' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Forwarded-User-Agent',
|
|
'description' => 'The user agent string of the client that made the request',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserId' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Id',
|
|
'description' => 'Impersonate a user by ID on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserEmail' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Email',
|
|
'description' => 'Impersonate a user by email on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserPhone' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Phone',
|
|
'description' => 'Impersonate a user by phone on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
],
|
|
APP_SDK_PLATFORM_CONSOLE => [
|
|
'Project' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Project',
|
|
'description' => 'Your project ID',
|
|
'in' => 'header',
|
|
],
|
|
'Key' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Key',
|
|
'description' => 'Your secret API key',
|
|
'in' => 'header',
|
|
],
|
|
'JWT' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-JWT',
|
|
'description' => 'Your secret JSON Web Token',
|
|
'in' => 'header',
|
|
],
|
|
'Locale' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Locale',
|
|
'description' => '',
|
|
'in' => 'header',
|
|
],
|
|
'Mode' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Mode',
|
|
'description' => '',
|
|
'in' => 'header',
|
|
],
|
|
'Cookie' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'Cookie',
|
|
'description' => 'The user cookie to authenticate with',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserId' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Id',
|
|
'description' => 'Impersonate a user by ID on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserEmail' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Email',
|
|
'description' => 'Impersonate a user by email on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
'ImpersonateUserPhone' => [
|
|
'type' => 'apiKey',
|
|
'name' => 'X-Appwrite-Impersonate-User-Phone',
|
|
'description' => 'Impersonate a user by phone on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
|
'in' => 'header',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
public function getSDKPlatformsForRouteSecurity(array $routeSecurity): array
|
|
{
|
|
$sdkPlatforms = [];
|
|
foreach ($routeSecurity as $value) {
|
|
switch ($value) {
|
|
case AuthType::SESSION:
|
|
$sdkPlatforms[] = APP_SDK_PLATFORM_CLIENT;
|
|
break;
|
|
case AuthType::JWT:
|
|
case AuthType::KEY:
|
|
$sdkPlatforms[] = APP_SDK_PLATFORM_SERVER;
|
|
break;
|
|
case AuthType::ADMIN:
|
|
$sdkPlatforms[] = APP_SDK_PLATFORM_CONSOLE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $sdkPlatforms;
|
|
}
|
|
|
|
public function action(string $version, string $mode, ?string $git, ?string $message, ?string $branch): void
|
|
{
|
|
if (\is_null($git)) {
|
|
$git = Console::confirm('Should we push specs to the appwrite/specs repo? (yes/no)');
|
|
}
|
|
|
|
if ($git === 'yes' && \is_null($message)) {
|
|
$message = Console::confirm('Please enter your commit message:');
|
|
}
|
|
|
|
if (\is_null($branch)) {
|
|
$branch = 'main';
|
|
}
|
|
|
|
$appRoutes = Http::getRoutes();
|
|
|
|
/** @var AppwriteResponse $response */
|
|
$response = $this->getResponse();
|
|
|
|
$mocks = ($mode === 'mocks');
|
|
|
|
// Mock dependencies
|
|
Http::setResource('request', fn () => $this->getRequest());
|
|
Http::setResource('response', fn () => $response);
|
|
Http::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
|
|
Http::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
|
|
|
|
$platforms = static::getPlatforms();
|
|
$authCounts = $this->getAuthCounts();
|
|
$keys = $this->getKeys();
|
|
|
|
$generatedFiles = [];
|
|
$endpoint = System::getEnv('_APP_HOME', '[HOSTNAME]');
|
|
$email = System::getEnv('_APP_SYSTEM_TEAM_EMAIL', APP_EMAIL_TEAM);
|
|
$specsDir = __DIR__ . '/../../../../app/config/specs';
|
|
|
|
if (!is_dir($specsDir) && !@mkdir($specsDir, 0755, true) && !is_dir($specsDir)) {
|
|
throw new Exception('Failed to create specs directory: ' . $specsDir);
|
|
}
|
|
|
|
foreach ($platforms as $platform) {
|
|
$routes = [];
|
|
$models = [];
|
|
$services = [];
|
|
|
|
foreach ($appRoutes as $key => $method) {
|
|
foreach ($method as $route) {
|
|
$sdks = $route->getLabel('sdk', false);
|
|
|
|
if (empty($sdks)) {
|
|
continue;
|
|
}
|
|
|
|
if (!\is_array($sdks)) {
|
|
$sdks = [$sdks];
|
|
}
|
|
|
|
foreach ($sdks as $sdk) {
|
|
/** @var Method $sdk */
|
|
$hide = $sdk->isHidden();
|
|
|
|
if ($hide === true || (\is_array($hide) && \in_array($platform, $hide))) {
|
|
continue;
|
|
}
|
|
|
|
$routeSecurity = $sdk->getAuth();
|
|
$sdkPlatforms = $this->getSDKPlatformsForRouteSecurity($routeSecurity);
|
|
|
|
if (!$route->getLabel('docs', true)) {
|
|
continue;
|
|
}
|
|
|
|
if ($route->getLabel('mock', false) && !$mocks) {
|
|
continue;
|
|
}
|
|
|
|
if (!$route->getLabel('mock', false) && $mocks) {
|
|
continue;
|
|
}
|
|
|
|
if (empty($sdk->getNamespace())) {
|
|
continue;
|
|
}
|
|
|
|
if (!\in_array($platform, $sdkPlatforms)) {
|
|
continue;
|
|
}
|
|
|
|
$routes[] = $route;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (Config::getParam('services', []) as $service) {
|
|
if (
|
|
!isset($service['docs']) // Skip service if not part of the public API
|
|
|| !isset($service['sdk'])
|
|
|| !$service['docs']
|
|
|| !$service['sdk']
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Check if current platform is included in service's platforms
|
|
if (!\in_array($platform, $service['platforms'] ?? [])) {
|
|
continue;
|
|
}
|
|
|
|
$services[] = [
|
|
'name' => $service['key'] ?? '',
|
|
'description' => $service['subtitle'] ?? '',
|
|
];
|
|
}
|
|
|
|
$models = $response->getModels();
|
|
|
|
foreach ($models as $key => $value) {
|
|
if ($platform !== APP_SDK_PLATFORM_CONSOLE && !$value->isPublic()) {
|
|
unset($models[$key]);
|
|
}
|
|
}
|
|
|
|
$arguments = [
|
|
new Http('UTC'),
|
|
$services,
|
|
$routes,
|
|
$models,
|
|
$keys[$platform],
|
|
$authCounts[$platform] ?? 0,
|
|
$platform
|
|
];
|
|
|
|
foreach (['swagger2', 'open-api3'] as $format) {
|
|
$formatInstance = $this->getFormatInstance($format, $arguments);
|
|
$specs = new Specification($formatInstance);
|
|
|
|
$formatInstance
|
|
->setParam('name', APP_NAME)
|
|
->setParam('description', 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)')
|
|
->setParam('endpoint', 'https://cloud.appwrite.io/v1')
|
|
->setParam('endpoint.docs', 'https://<REGION>.cloud.appwrite.io/v1')
|
|
->setParam('version', APP_VERSION_STABLE)
|
|
->setParam('terms', $endpoint . '/policy/terms')
|
|
->setParam('support.email', $email)
|
|
->setParam('support.url', $endpoint . '/support')
|
|
->setParam('contact.name', APP_NAME . ' Team')
|
|
->setParam('contact.email', $email)
|
|
->setParam('contact.url', $endpoint . '/support')
|
|
->setParam('license.name', 'BSD-3-Clause')
|
|
->setParam('license.url', 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE')
|
|
->setParam('docs.description', 'Full API docs, specs and tutorials')
|
|
->setParam('docs.url', $endpoint . '/docs');
|
|
|
|
$path = $mocks
|
|
? $specsDir . '/' . $format . '-mocks-' . $platform . '.json'
|
|
: $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
|
|
|
|
$parsedSpecs = $specs->parse();
|
|
$encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT);
|
|
|
|
unset($parsedSpecs);
|
|
|
|
if ($encodedSpecs === false) {
|
|
throw new Exception('Failed to encode ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . \json_last_error_msg());
|
|
}
|
|
|
|
if (\file_put_contents($path, $encodedSpecs) === false) {
|
|
throw new Exception('Failed to save ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . $path);
|
|
}
|
|
|
|
$generatedFiles[] = realpath($path);
|
|
Console::success('Saved ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . realpath($path));
|
|
|
|
unset($encodedSpecs, $specs, $formatInstance);
|
|
}
|
|
|
|
unset($arguments, $models, $routes, $services);
|
|
}
|
|
|
|
if ($git === 'yes') {
|
|
$gitUrl = 'git@github.com:appwrite/specs.git';
|
|
$gitRepoName = 'appwrite/specs';
|
|
$gitBranch = 'feat-' . $version . '-specs';
|
|
$repoBranch = $branch;
|
|
$target = \realpath(__DIR__ . '/../../../../app') . '/sdks/git/specs/';
|
|
$examplesDir = \realpath(__DIR__ . '/../../../..') . '/docs/examples/' . $version;
|
|
|
|
Console::info("Cloning {$gitRepoName} into {$target}...");
|
|
|
|
\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 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
|
|
');
|
|
|
|
// Copy generated spec files into specs/{version}/ subdirectory
|
|
$prPlatforms = static::getPlatformsForPR();
|
|
$prFiles = \array_filter(
|
|
$generatedFiles,
|
|
fn (string $file) => \in_array(
|
|
\substr(\basename($file, '.json'), \strrpos(\basename($file, '.json'), '-') + 1),
|
|
$prPlatforms,
|
|
true
|
|
)
|
|
);
|
|
|
|
$specsSubDir = $mocks ? 'mocks' : $version;
|
|
\exec('mkdir -p ' . \escapeshellarg("{$target}/specs/{$specsSubDir}"));
|
|
foreach ($prFiles as $file) {
|
|
$fileName = \basename($file);
|
|
\exec('cp ' . \escapeshellarg($file) . ' ' . \escapeshellarg("{$target}/specs/{$specsSubDir}/{$fileName}"));
|
|
Console::success("Copied spec file to repo: specs/{$specsSubDir}/{$fileName}");
|
|
}
|
|
|
|
// Regenerate SDK examples for this version
|
|
Console::info("Regenerating SDK examples for version {$version}...");
|
|
$sdksCommand = $this->getSdksCommand($version);
|
|
\exec($sdksCommand, $sdksOutput, $sdksReturnCode);
|
|
|
|
if ($sdksReturnCode !== 0) {
|
|
Console::warning("SDK examples generation returned non-zero exit code: {$sdksReturnCode}");
|
|
Console::warning(\implode("\n", $sdksOutput));
|
|
} else {
|
|
Console::success("Regenerated SDK examples for version {$version}");
|
|
}
|
|
|
|
// Copy SDK examples for this version, filtered by PR platforms
|
|
if (\is_dir($examplesDir)) {
|
|
\exec('mkdir -p ' . \escapeshellarg("{$target}/examples/{$version}"));
|
|
$exampleFolders = \glob($examplesDir . '/*', GLOB_ONLYDIR) ?: [];
|
|
foreach ($exampleFolders as $folder) {
|
|
$folderName = \basename($folder);
|
|
$platform = \strstr($folderName, '-', true) ?: $folderName;
|
|
if (!\in_array($platform, $prPlatforms, true)) {
|
|
Console::info("Skipping SDK examples for platform '{$platform}': examples/{$version}/{$folderName}");
|
|
continue;
|
|
}
|
|
\exec('cp -r ' . \escapeshellarg($folder) . ' ' . \escapeshellarg("{$target}/examples/{$version}/"));
|
|
Console::success("Copied SDK examples to repo: examples/{$version}/{$folderName}");
|
|
}
|
|
} else {
|
|
Console::warning("No SDK examples found at: {$examplesDir}. Skipping examples copy.");
|
|
}
|
|
|
|
// Git add, commit, and push
|
|
\exec('cd ' . $target . ' && \
|
|
git add -A && \
|
|
git commit -m "' . \addslashes($message) . '" && \
|
|
git push -u origin ' . $gitBranch . '
|
|
');
|
|
|
|
Console::success("Pushed specs to {$gitRepoName} on branch {$gitBranch}");
|
|
|
|
// Create or update PR
|
|
$prTitle = "feat: API specs update for version {$version}";
|
|
$prBody = "This PR contains API specification updates and SDK examples for version {$version}.";
|
|
|
|
$prCommand = 'cd ' . $target . ' && \
|
|
gh pr create \
|
|
--repo "' . $gitRepoName . '" \
|
|
--title "' . $prTitle . '" \
|
|
--body "' . $prBody . '" \
|
|
--base "' . $repoBranch . '" \
|
|
--head "' . $gitBranch . '" \
|
|
2>&1';
|
|
|
|
$prUrl = '';
|
|
\exec($prCommand, $prOutput);
|
|
$prOutput = \implode("\n", $prOutput);
|
|
|
|
if (\str_contains($prOutput, 'already exists')) {
|
|
Console::warning("PR already exists for branch {$gitBranch}");
|
|
// Try to get the existing PR URL
|
|
\exec('cd ' . $target . ' && gh pr view --repo "' . $gitRepoName . '" --json url -q .url 2>&1', $existingPrOutput);
|
|
$prUrl = \trim(\implode("\n", $existingPrOutput));
|
|
} else {
|
|
$prUrl = \trim($prOutput);
|
|
}
|
|
|
|
// Clean up temp directory
|
|
\exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target);
|
|
Console::success("Removed temp directory '{$target}'");
|
|
|
|
if (!empty($prUrl)) {
|
|
Console::log('');
|
|
Console::log('Pull Request Summary');
|
|
Console::log("Specs PR: {$prUrl}");
|
|
Console::log('');
|
|
}
|
|
}
|
|
}
|
|
}
|