diff --git a/docs/references/documentsdb/create-operations.md b/docs/references/documentsdb/create-operations.md new file mode 100644 index 0000000000..a737b95a55 --- /dev/null +++ b/docs/references/documentsdb/create-operations.md @@ -0,0 +1 @@ +Create multiple operations in a single transaction. \ No newline at end of file diff --git a/docs/references/documentsdb/create-transaction.md b/docs/references/documentsdb/create-transaction.md new file mode 100644 index 0000000000..fdf369a789 --- /dev/null +++ b/docs/references/documentsdb/create-transaction.md @@ -0,0 +1 @@ +Create a new transaction. \ No newline at end of file diff --git a/docs/references/documentsdb/delete-transaction.md b/docs/references/documentsdb/delete-transaction.md new file mode 100644 index 0000000000..f1395c228f --- /dev/null +++ b/docs/references/documentsdb/delete-transaction.md @@ -0,0 +1 @@ +Delete a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/documentsdb/get-transaction.md b/docs/references/documentsdb/get-transaction.md new file mode 100644 index 0000000000..41900f7468 --- /dev/null +++ b/docs/references/documentsdb/get-transaction.md @@ -0,0 +1 @@ +Get a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/documentsdb/list-transactions.md b/docs/references/documentsdb/list-transactions.md new file mode 100644 index 0000000000..9a63d9f04a --- /dev/null +++ b/docs/references/documentsdb/list-transactions.md @@ -0,0 +1 @@ +List transactions across all databases. \ No newline at end of file diff --git a/docs/references/documentsdb/update-transaction.md b/docs/references/documentsdb/update-transaction.md new file mode 100644 index 0000000000..d9d5f45439 --- /dev/null +++ b/docs/references/documentsdb/update-transaction.md @@ -0,0 +1 @@ +Update a transaction, to either commit or roll back its operations. \ No newline at end of file diff --git a/docs/references/vectorsdb/create-collection.md b/docs/references/vectorsdb/create-collection.md new file mode 100644 index 0000000000..c6293a4c38 --- /dev/null +++ b/docs/references/vectorsdb/create-collection.md @@ -0,0 +1 @@ +Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console. \ No newline at end of file diff --git a/docs/references/vectorsdb/create-document.md b/docs/references/vectorsdb/create-document.md new file mode 100644 index 0000000000..197744b4a0 --- /dev/null +++ b/docs/references/vectorsdb/create-document.md @@ -0,0 +1 @@ +Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console. \ No newline at end of file diff --git a/docs/references/vectorsdb/create-documents.md b/docs/references/vectorsdb/create-documents.md new file mode 100644 index 0000000000..9f4a4a1396 --- /dev/null +++ b/docs/references/vectorsdb/create-documents.md @@ -0,0 +1 @@ +Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console. \ No newline at end of file diff --git a/docs/references/vectorsdb/create-index.md b/docs/references/vectorsdb/create-index.md new file mode 100644 index 0000000000..164b754161 --- /dev/null +++ b/docs/references/vectorsdb/create-index.md @@ -0,0 +1,2 @@ +Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request. +Attributes can be `key`, `fulltext`, and `unique`. \ No newline at end of file diff --git a/docs/references/vectorsdb/create-operations.md b/docs/references/vectorsdb/create-operations.md new file mode 100644 index 0000000000..a737b95a55 --- /dev/null +++ b/docs/references/vectorsdb/create-operations.md @@ -0,0 +1 @@ +Create multiple operations in a single transaction. \ No newline at end of file diff --git a/docs/references/vectorsdb/create-transaction.md b/docs/references/vectorsdb/create-transaction.md new file mode 100644 index 0000000000..fdf369a789 --- /dev/null +++ b/docs/references/vectorsdb/create-transaction.md @@ -0,0 +1 @@ +Create a new transaction. \ No newline at end of file diff --git a/docs/references/vectorsdb/create.md b/docs/references/vectorsdb/create.md new file mode 100644 index 0000000000..b608485341 --- /dev/null +++ b/docs/references/vectorsdb/create.md @@ -0,0 +1 @@ +Create a new Database. diff --git a/docs/references/vectorsdb/decrement-document-attribute.md b/docs/references/vectorsdb/decrement-document-attribute.md new file mode 100644 index 0000000000..b7b32d6148 --- /dev/null +++ b/docs/references/vectorsdb/decrement-document-attribute.md @@ -0,0 +1 @@ +Decrement a specific column of a row by a given value. \ No newline at end of file diff --git a/docs/references/vectorsdb/delete-collection.md b/docs/references/vectorsdb/delete-collection.md new file mode 100644 index 0000000000..90f7aa6aa5 --- /dev/null +++ b/docs/references/vectorsdb/delete-collection.md @@ -0,0 +1 @@ +Delete a collection by its unique ID. Only users with write permissions have access to delete this resource. \ No newline at end of file diff --git a/docs/references/vectorsdb/delete-document.md b/docs/references/vectorsdb/delete-document.md new file mode 100644 index 0000000000..36fbf6802d --- /dev/null +++ b/docs/references/vectorsdb/delete-document.md @@ -0,0 +1 @@ +Delete a document by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/delete-documents.md b/docs/references/vectorsdb/delete-documents.md new file mode 100644 index 0000000000..a7b05503de --- /dev/null +++ b/docs/references/vectorsdb/delete-documents.md @@ -0,0 +1 @@ +Bulk delete documents using queries, if no queries are passed then all documents are deleted. \ No newline at end of file diff --git a/docs/references/vectorsdb/delete-index.md b/docs/references/vectorsdb/delete-index.md new file mode 100644 index 0000000000..c5b8f49e5f --- /dev/null +++ b/docs/references/vectorsdb/delete-index.md @@ -0,0 +1 @@ +Delete an index. \ No newline at end of file diff --git a/docs/references/vectorsdb/delete-transaction.md b/docs/references/vectorsdb/delete-transaction.md new file mode 100644 index 0000000000..f1395c228f --- /dev/null +++ b/docs/references/vectorsdb/delete-transaction.md @@ -0,0 +1 @@ +Delete a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/delete.md b/docs/references/vectorsdb/delete.md new file mode 100644 index 0000000000..605fa290d3 --- /dev/null +++ b/docs/references/vectorsdb/delete.md @@ -0,0 +1 @@ +Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-collection-logs.md b/docs/references/vectorsdb/get-collection-logs.md new file mode 100644 index 0000000000..8578cef03c --- /dev/null +++ b/docs/references/vectorsdb/get-collection-logs.md @@ -0,0 +1 @@ +Get the collection activity logs list by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-collection-usage.md b/docs/references/vectorsdb/get-collection-usage.md new file mode 100644 index 0000000000..48682a075f --- /dev/null +++ b/docs/references/vectorsdb/get-collection-usage.md @@ -0,0 +1 @@ +Get usage metrics and statistics for a collection. Returning the total number of documents. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-collection.md b/docs/references/vectorsdb/get-collection.md new file mode 100644 index 0000000000..97b39e8474 --- /dev/null +++ b/docs/references/vectorsdb/get-collection.md @@ -0,0 +1 @@ +Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-database-usage.md b/docs/references/vectorsdb/get-database-usage.md new file mode 100644 index 0000000000..2c2628a464 --- /dev/null +++ b/docs/references/vectorsdb/get-database-usage.md @@ -0,0 +1 @@ +Get usage metrics and statistics for a database. You can view the total number of collections, documents, and storage usage. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-document-logs.md b/docs/references/vectorsdb/get-document-logs.md new file mode 100644 index 0000000000..9b96df5ad4 --- /dev/null +++ b/docs/references/vectorsdb/get-document-logs.md @@ -0,0 +1 @@ +Get the document activity logs list by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-document.md b/docs/references/vectorsdb/get-document.md new file mode 100644 index 0000000000..4e4d76bec0 --- /dev/null +++ b/docs/references/vectorsdb/get-document.md @@ -0,0 +1 @@ +Get a document by its unique ID. This endpoint response returns a JSON object with the document data. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-index.md b/docs/references/vectorsdb/get-index.md new file mode 100644 index 0000000000..cdea5b4f27 --- /dev/null +++ b/docs/references/vectorsdb/get-index.md @@ -0,0 +1 @@ +Get index by ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-logs.md b/docs/references/vectorsdb/get-logs.md new file mode 100644 index 0000000000..8e49da4603 --- /dev/null +++ b/docs/references/vectorsdb/get-logs.md @@ -0,0 +1 @@ +Get the database activity logs list by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/get-transaction.md b/docs/references/vectorsdb/get-transaction.md new file mode 100644 index 0000000000..41900f7468 --- /dev/null +++ b/docs/references/vectorsdb/get-transaction.md @@ -0,0 +1 @@ +Get a transaction by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/get.md b/docs/references/vectorsdb/get.md new file mode 100644 index 0000000000..24183f6f6b --- /dev/null +++ b/docs/references/vectorsdb/get.md @@ -0,0 +1 @@ +Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata. \ No newline at end of file diff --git a/docs/references/vectorsdb/increment-document-attribute.md b/docs/references/vectorsdb/increment-document-attribute.md new file mode 100644 index 0000000000..7a19b3fbc7 --- /dev/null +++ b/docs/references/vectorsdb/increment-document-attribute.md @@ -0,0 +1 @@ +Increment a specific column of a row by a given value. \ No newline at end of file diff --git a/docs/references/vectorsdb/list-attributes.md b/docs/references/vectorsdb/list-attributes.md new file mode 100644 index 0000000000..72ad6d727f --- /dev/null +++ b/docs/references/vectorsdb/list-attributes.md @@ -0,0 +1 @@ +List attributes in the collection. \ No newline at end of file diff --git a/docs/references/vectorsdb/list-collections.md b/docs/references/vectorsdb/list-collections.md new file mode 100644 index 0000000000..e437674915 --- /dev/null +++ b/docs/references/vectorsdb/list-collections.md @@ -0,0 +1 @@ +Get a list of all collections that belong to the provided databaseId. You can use the search parameter to filter your results. \ No newline at end of file diff --git a/docs/references/vectorsdb/list-documents.md b/docs/references/vectorsdb/list-documents.md new file mode 100644 index 0000000000..4e2ae91792 --- /dev/null +++ b/docs/references/vectorsdb/list-documents.md @@ -0,0 +1 @@ +Get a list of all the user's documents in a given collection. You can use the query params to filter your results. \ No newline at end of file diff --git a/docs/references/vectorsdb/list-indexes.md b/docs/references/vectorsdb/list-indexes.md new file mode 100644 index 0000000000..a8c687fb2b --- /dev/null +++ b/docs/references/vectorsdb/list-indexes.md @@ -0,0 +1 @@ +List indexes in the collection. \ No newline at end of file diff --git a/docs/references/vectorsdb/list-transactions.md b/docs/references/vectorsdb/list-transactions.md new file mode 100644 index 0000000000..9a63d9f04a --- /dev/null +++ b/docs/references/vectorsdb/list-transactions.md @@ -0,0 +1 @@ +List transactions across all databases. \ No newline at end of file diff --git a/docs/references/vectorsdb/list-usage.md b/docs/references/vectorsdb/list-usage.md new file mode 100644 index 0000000000..a88e76680e --- /dev/null +++ b/docs/references/vectorsdb/list-usage.md @@ -0,0 +1 @@ +List usage metrics and statistics for all databases in the project. You can view the total number of databases, collections, documents, and storage usage. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days. \ No newline at end of file diff --git a/docs/references/vectorsdb/list.md b/docs/references/vectorsdb/list.md new file mode 100644 index 0000000000..d93fb9d7a8 --- /dev/null +++ b/docs/references/vectorsdb/list.md @@ -0,0 +1 @@ +Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results. \ No newline at end of file diff --git a/docs/references/vectorsdb/update-collection.md b/docs/references/vectorsdb/update-collection.md new file mode 100644 index 0000000000..b8f6bef997 --- /dev/null +++ b/docs/references/vectorsdb/update-collection.md @@ -0,0 +1 @@ +Update a collection by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/update-document.md b/docs/references/vectorsdb/update-document.md new file mode 100644 index 0000000000..526f3971d1 --- /dev/null +++ b/docs/references/vectorsdb/update-document.md @@ -0,0 +1 @@ +Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated. \ No newline at end of file diff --git a/docs/references/vectorsdb/update-documents.md b/docs/references/vectorsdb/update-documents.md new file mode 100644 index 0000000000..5f560c6435 --- /dev/null +++ b/docs/references/vectorsdb/update-documents.md @@ -0,0 +1 @@ +Update all documents that match your queries, if no queries are submitted then all documents are updated. You can pass only specific fields to be updated. \ No newline at end of file diff --git a/docs/references/vectorsdb/update-transaction.md b/docs/references/vectorsdb/update-transaction.md new file mode 100644 index 0000000000..d9d5f45439 --- /dev/null +++ b/docs/references/vectorsdb/update-transaction.md @@ -0,0 +1 @@ +Update a transaction, to either commit or roll back its operations. \ No newline at end of file diff --git a/docs/references/vectorsdb/update.md b/docs/references/vectorsdb/update.md new file mode 100644 index 0000000000..4e99bf2e07 --- /dev/null +++ b/docs/references/vectorsdb/update.md @@ -0,0 +1 @@ +Update a database by its unique ID. \ No newline at end of file diff --git a/docs/references/vectorsdb/upsert-document.md b/docs/references/vectorsdb/upsert-document.md new file mode 100644 index 0000000000..f1b68d13d5 --- /dev/null +++ b/docs/references/vectorsdb/upsert-document.md @@ -0,0 +1 @@ +Create or update a Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console. \ No newline at end of file diff --git a/docs/references/vectorsdb/upsert-documents.md b/docs/references/vectorsdb/upsert-documents.md new file mode 100644 index 0000000000..4feb473076 --- /dev/null +++ b/docs/references/vectorsdb/upsert-documents.md @@ -0,0 +1 @@ +Create or update Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console. diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php index 050227b4b9..5a5ebf48ee 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php @@ -58,7 +58,7 @@ class Upsert extends Action group: $this->getSDKGroup(), name: self::getName(), description: '/docs/references/databases/upsert-documents.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], + auth: [AuthType::ADMIN, AuthType::KEY], responses: [ new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 638ceab59e..04e2dbd406 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -127,7 +127,6 @@ trait Deployment Span::add("{$logBase}.authorized", $isAuthorized); - $commentStatus = 'waiting'; $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $hostname = $platform['consoleHostname'] ?? ''; @@ -135,6 +134,34 @@ trait Deployment $action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl]; + $commentStatus = 'waiting'; + $commentPreviewUrl = ''; + + // If this action was triggered by pull request, use most up to date details in comment + if (!empty($providerPullRequestId)) { + $existingDeployment = $authorization->skip(fn () => $dbForProject->findOne('deployments', [ + Query::equal('resourceInternalId', [$resource->getSequence()]), + Query::equal('resourceType', [$resourceCollection]), + Query::equal('providerCommitHash', [$providerCommitHash]), + Query::equal('providerBranch', [$providerBranch]), + Query::orderDesc('$createdAt') + ])); + + $commentStatus = $existingDeployment->getAttribute('status', 'waiting'); + + if ($resource->getCollection() === 'sites') { + $previewRule = $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ + Query::equal('projectInternalId', [$project->getSequence()]), + Query::equal('type', ['deployment']), // Not redirect + Query::equal('trigger', ['deployment']), // Preview - Not manual + Query::equal('deploymentResourceType', ['site']), // Not function + Query::equal('deploymentInternalId', [$existingDeployment->getSequence()]), + ])); + + $commentPreviewUrl = !$previewRule->isEmpty() ? ("{$protocol}://" . $previewRule->getAttribute('domain', '')) : ''; + } + } + $latestCommentId = ''; if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) { @@ -173,7 +200,7 @@ trait Deployment try { $comment = new Comment($platform); $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId)); - $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); + $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $commentPreviewUrl); $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment())); } finally { @@ -182,7 +209,7 @@ trait Deployment } } else { $comment = new Comment($platform); - $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, ''); + $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $commentPreviewUrl); $latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment())); if (!empty($latestCommentId)) { @@ -274,6 +301,19 @@ trait Deployment continue; } + if (!empty($providerPullRequestId)) { + // Update comment ID so running build can update comment + $authorization->skip(fn () => $dbForProject->updateDocuments('deployments', new Document([ + 'providerCommentId' => \strval($latestCommentId) + ]), [ + Query::equal('providerCommitHash', [$providerCommitHash]), + Query::equal('providerBranch', [$providerBranch]), + ])); + + // Skip rest - prevent double deployments (previous one was made by push) + continue; + } + $commands = []; if (!empty($resource->getAttribute('installCommand', ''))) { $commands[] = $resource->getAttribute('installCommand', ''); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php index c614c80041..a2fa44c613 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -65,7 +65,7 @@ class Create extends Action $signature = $request->getHeader('x-hub-signature-256', ''); $secretKey = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''); - $valid = empty($signature) ? true : $github->validateWebhookEvent($payload, $signature, $secretKey); + $valid = empty($secretKey) ? true : $github->validateWebhookEvent($payload, $signature, $secretKey); Span::add('vcs.github.event.signature.valid', $valid); if (!$valid) { @@ -162,8 +162,8 @@ class Create extends Action Query::limit(100), ])); - // Create new deployment only on push (not committed by us) and not when branch is created or deleted - if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) { + // Create new deployment only on push (not committed by us) and not when branch is deleted + if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchDeleted) { $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform); } } diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 528084f4ea..70d5e6ded5 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -89,7 +89,7 @@ class SDKs extends Action $selectedSDK = $sdk; if (! $sdks) { - $selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '" or "*" for all):'); + $selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '", comma-separated, or "*" for all):'); $selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):')); $supportedSDKs = $this->getSupportedSDKs(); if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $supportedSDKs)) { @@ -134,14 +134,26 @@ class SDKs extends Action '1.6.x', '1.7.x', '1.8.x', + '1.9.x', 'latest', ])) { throw new \Exception('Unknown version given'); } + $selectedPlatforms = ($selectedPlatform === '*' || $selectedPlatform === null) ? null : \array_map('trim', \explode(',', $selectedPlatform)); + + if ($selectedPlatforms !== null) { + $validPlatforms = static::getPlatforms(); + foreach ($selectedPlatforms as $p) { + if (! \in_array($p, $validPlatforms)) { + throw new \Exception('Unknown platform "' . $p . '". Options are: ' . implode(', ', $validPlatforms)); + } + } + } + $platforms = Config::getParam('sdks'); foreach ($platforms as $key => $platform) { - if ($selectedPlatform !== $key && $selectedPlatform !== '*' && ($sdks === null)) { + if ($selectedPlatforms !== null && ! \in_array($key, $selectedPlatforms) && ($sdks === null)) { continue; } @@ -151,12 +163,14 @@ class SDKs extends Action } if (! $language['enabled']) { - Console::warning($language['name'] . ' for ' . $platform['name'] . ' is disabled'); + Console::warning("{$language['name']} for {$platform['name']} is disabled"); continue; } - Console::info('Fetching API Spec for ' . $language['name'] . ' for ' . $platform['name'] . ' (version: ' . $version . ')'); + Console::log(''); + Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$version}) ━━━"); + Console::log(' Fetching API spec...'); $specPath = __DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json'; @@ -320,7 +334,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $releaseTarget = $language['repoBranch'] ?? 'main'; if ($repoName === '/') { - Console::warning("{$language['name']} SDK is not an SDK, skipping release"); + Console::warning(' Not a releasable SDK, skipping'); continue; } @@ -330,8 +344,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? ''); if (! empty($existingReleaseUrl)) { - Console::warning("Release {$releaseVersion} already exists for {$language['name']} SDK, skipping..."); - Console::info("Existing release: {$existingReleaseUrl}"); + Console::warning(" Release {$releaseVersion} already exists, skipping"); + Console::log(" {$existingReleaseUrl}"); continue; } @@ -349,7 +363,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? ''); 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..."); + Console::warning(" Latest commit already released ({$latestReleaseTag}), skipping"); continue; } @@ -370,17 +384,16 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } 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(''); + Console::info(' [DRY RUN] Would create release:'); + 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)); } else { - Console::info("Creating release {$releaseVersion} for {$language['name']} SDK..."); + Console::log(" Creating release {$releaseVersion}..."); $tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_'); \file_put_contents($tempNotesFile, $formattedNotes); @@ -408,22 +421,22 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } } - Console::success("Successfully created release {$releaseVersion} for {$language['name']} SDK"); + Console::success(" Release {$releaseVersion} created"); if (! empty($releaseUrl)) { - Console::info("Release URL: {$releaseUrl}"); + Console::log(" {$releaseUrl}"); } } else { $errorMessage = implode("\n", $releaseOutput); - Console::error("Failed to create release for {$language['name']} SDK: " . $errorMessage); + Console::error(" Failed to create release: " . $errorMessage); } } continue; } - Console::info($examplesOnly - ? "Generating examples for {$language['name']} SDK..." - : "Generating {$language['name']} SDK..."); + Console::log($examplesOnly + ? ' Generating examples...' + : ' Generating SDK...'); $sdk = new SDK($config, new Swagger2($spec)); @@ -477,11 +490,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $aiChangelog = ''; // Track AI-generated changelog for PR description if (! empty($apiKey) && ! $examplesOnly) { - Console::info("Analyzing SDK changes with AI..."); + Console::log(' Analyzing changes with AI...'); $aiResult = $this->generateVersionAndChangelog($language, $result); if (!empty($aiResult['skip'])) { - Console::warning("Skipping {$language['name']} SDK generation"); + Console::warning(' Skipping (no relevant changes)'); continue; } elseif ($aiResult !== null) { $newVersion = $aiResult['version']; @@ -509,7 +522,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::error($exception->getMessage()); } } else { - Console::warning('AI analysis failed, using existing version'); + Console::warning(' AI analysis failed, using existing version'); } } @@ -530,10 +543,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $pushSuccess = $this->pushToGit($language, $target, $result, $gitUrl, $gitBranch, $repoBranch, $commitMessage); if ($pushSuccess) { - $this->createPullRequest($language, $target, $gitBranch, $repoBranch, $aiChangelog, $prUrls); + $this->createPullRequest($language, $platform['name'], $target, $gitBranch, $repoBranch, $aiChangelog, $prUrls); } - $this->cleanupTarget($target, $language['name']); + \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); + Console::log(' Cleaned up temp directory'); } $this->copyExamples($language, $version, $result, $resultExamples); @@ -542,9 +556,13 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (! empty($prUrls)) { Console::log(''); - Console::log('Pull Request Summary'); - foreach ($prUrls as $sdkName => $url) { - Console::log("{$sdkName}: {$url}"); + Console::info('━━━ Pull Request Summary ━━━'); + foreach ($prUrls as $platformName => $sdks) { + Console::log(''); + Console::info(" {$platformName}:"); + foreach ($sdks as $sdkName => $url) { + Console::log(" {$sdkName}: {$url}"); + } } Console::log(''); } @@ -552,7 +570,7 @@ 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..."); + Console::log(' Preparing git repository...'); try { // Init fresh repo @@ -635,18 +653,26 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Stage, commit, push $repo->addAllChanges(); - $repo->commit($commitMessage); + + try { + $repo->commit($commitMessage); + } catch (\Throwable $e) { + // Exit code 1 (256 in PHP) = nothing to commit + Console::log(' No changes to commit, SDK is up to date'); + return true; + } + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); } catch (\Throwable $e) { - Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + Console::warning(" Git push failed: " . $e->getMessage()); return false; } - Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); + Console::success(" Pushed to {$gitUrl}"); return true; } - private function createPullRequest(array $language, string $target, string $gitBranch, string $repoBranch, string $aiChangelog, array &$prUrls): void + private function createPullRequest(array $language, string $platformName, 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']}."; @@ -655,7 +681,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; - Console::info("Creating pull request for {$language['name']} SDK..."); + Console::log(' Creating pull request...'); $prCommand = 'cd ' . $target . ' && \ gh pr create \ @@ -671,29 +697,32 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND \exec($prCommand, $prOutput, $prReturnCode); if ($prReturnCode === 0) { - Console::success("Successfully created pull request for {$language['name']} SDK"); + Console::success(" Pull request created"); foreach ($prOutput as $line) { if (\str_starts_with(trim($line), 'https://')) { - $prUrls[$language['name']] = trim($line); + $prUrls[$platformName][$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); + Console::error(" Failed to create pull request: " . $errorMessage); } else { - $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); + // Extract PR URL from the error output (gh includes it in "already exists" messages) + $existingPrUrl = ''; + foreach ($prOutput as $line) { + if (\preg_match('#(https://github\.com/[^\s]+/pull/\d+)#', $line, $urlMatch)) { + $existingPrUrl = $urlMatch[1]; + break; + } + } + + $this->updateExistingPr($repoName, $gitBranch, $prTitle, $prBody, $platformName, $language['name'], $prUrls, $existingPrUrl); } } } - 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'] ?? ['']; @@ -707,7 +736,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $examplesSource = $result . '/docs/examples' . $languagePath; if (! \is_dir($examplesSource)) { - Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy."); + Console::warning(" No code examples found at: {$examplesSource}"); continue; } @@ -716,7 +745,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 'mkdir -p ' . $resultExamples . $languagePath . ' && \ cp -r ' . $examplesSource . ' ' . $resultExamples ); - Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}"); + $label = \is_string($languageTitle) ? " ({$languageTitle})" : ''; + Console::success(" Examples{$label} copied to {$resultExamples}{$languagePath}"); } } @@ -772,13 +802,13 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $repoBranch = $language['repoBranch'] ?? 'main'; if (empty($gitUrl)) { - Console::warning("No git URL for {$language['name']} SDK, skipping AI analysis"); + Console::warning(' No git URL, skipping AI analysis'); return null; } $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'); + Console::warning(' _APP_ASSISTANT_OPENAI_API_KEY not set, skipping AI analysis'); return null; } @@ -870,7 +900,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ->setMaxDiffLines(500) ->setUserId('sdk-analyst'); - Console::info("Running DiffCheck for {$language['name']} SDK..."); + Console::log(' Running DiffCheck...'); $result = (new DiffCheck())->run( runner: $adapter, @@ -881,43 +911,42 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ); if (!$result['hasChanges']) { - Console::info("✓ No changes detected - SDK is up to date"); + Console::success(' No changes detected, SDK is up to date'); return null; } $responseContent = $result['response']; if (empty(trim($responseContent))) { - Console::warning('AI returned empty response'); + Console::warning(' AI returned empty response'); return null; } $parsed = json_decode($responseContent, true); if (json_last_error() !== JSON_ERROR_NONE) { - Console::warning('Failed to parse AI response as JSON: ' . json_last_error_msg()); - Console::log('Raw response:'); - Console::log($responseContent); + Console::warning(' Failed to parse AI response: ' . json_last_error_msg()); + Console::log(' Raw response: ' . $responseContent); return null; } if (empty($parsed['version']) || empty($parsed['changelog']) || empty($parsed['versionBump'])) { - Console::warning('AI response missing required fields'); + Console::warning(' AI response missing required fields'); 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"); + Console::warning(" Beta SDK cannot bump to {$parsed['version']}, skipping"); return ['skip' => true]; } - Console::success("✓ Analysis complete"); - Console::log(" Version: {$language['version']} → {$parsed['version']} ({$parsed['versionBump']} bump)"); - Console::log(" Changelog:"); + Console::success(" AI analysis complete"); + Console::log(" Version: {$language['version']} → {$parsed['version']} ({$parsed['versionBump']})"); + Console::log(" Changelog:"); foreach (explode("\n", $parsed['changelog']) as $line) { if (trim($line)) { - Console::log(" {$line}"); + Console::log(" {$line}"); } } @@ -927,7 +956,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 'versionBump' => $parsed['versionBump'], ]; } catch (\Throwable $e) { - Console::error('Error generating version and changelog: ' . $e->getMessage()); + Console::error(' AI error: ' . $e->getMessage()); return null; } } @@ -945,7 +974,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $configPath = $this->getSdkConfigPath(); if (! file_exists($configPath)) { - Console::error("Config file not found: {$configPath}"); + Console::error(" Config file not found: {$configPath}"); return false; } @@ -960,10 +989,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $newContent = preg_replace($inlinePattern, '${1}' . $newVersion . '${3}', $content); if (file_put_contents($configPath, $newContent) !== false) { - Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); + Console::success(" Config updated: {$sdkKey} {$oldVersion} → {$newVersion}"); return true; } else { - Console::error('Failed to write config file'); + Console::error(' Failed to write config file'); return false; } } @@ -971,24 +1000,45 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Second, try to find version in array format (pattern 2) // Pattern matches: 'nodejs' => '22.1.2', or "nodejs" => "22.1.2", // Also handles extra whitespace: 'nodejs' => '22.1.2', - $arrayPattern = '/([\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"],)/m'; + // Scoped to the correct $Versions array block to avoid + // updating duplicate keys that appear under a different platform. + $blockPattern = '/(\$' . preg_quote($platform, '/') . 'Versions\s*=\s*\[)([\s\S]*?)(\];)/m'; + $entryPattern = '/([\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"],?)/m'; - if (preg_match($arrayPattern, $content, $matches)) { - $oldVersion = $matches[2]; - $newContent = preg_replace($arrayPattern, '${1}' . $newVersion . '${3}', $content); - - if (file_put_contents($configPath, $newContent) !== false) { - Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); - return true; - } else { - Console::error('Failed to write config file'); - return false; - } + if (! preg_match($blockPattern, $content)) { + Console::warning(" Could not find \${$platform}Versions block in config file"); + return false; } - Console::warning("Could not find version entry for {$sdkKey} in config"); + $updated = false; + $oldVersion = ''; + $newContent = preg_replace_callback($blockPattern, function ($blockMatch) use ($entryPattern, $newVersion, &$updated, &$oldVersion) { + $blockContent = $blockMatch[2]; + if (preg_match($entryPattern, $blockContent, $entryMatch)) { + $oldVersion = $entryMatch[2]; + $blockContent = preg_replace($entryPattern, '${1}' . $newVersion . '${3}', $blockContent); + $updated = true; + } + return $blockMatch[1] . $blockContent . $blockMatch[3]; + }, $content); - return false; + if ($newContent === null) { + Console::error(' preg_replace_callback failed while updating config'); + return false; + } + + if (! $updated) { + Console::warning(" Could not find version entry for {$sdkKey} in \${$platform}Versions block"); + return false; + } + + if (file_put_contents($configPath, $newContent) === false) { + Console::error(' Failed to write config file'); + return false; + } + + Console::success(" Config updated: {$sdkKey} {$oldVersion} → {$newVersion}"); + return true; } /** @@ -1002,7 +1052,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND private function updateChangelogFile(string $changelogPath, string $version, string $notes): bool { if (empty($changelogPath) || ! file_exists($changelogPath)) { - Console::warning("Changelog file not found: {$changelogPath}"); + Console::warning(" Changelog file not found: {$changelogPath}"); return false; } @@ -1011,7 +1061,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Check if version already exists if (strpos($content, "## {$version}") !== false) { - Console::warning("Version {$version} already exists in changelog, skipping update"); + Console::warning(" Version {$version} already in changelog, skipping"); return false; } @@ -1039,72 +1089,74 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $newContent = implode("\n", $newLines); if (file_put_contents($changelogPath, $newContent) !== false) { - Console::success("Updated changelog at {$changelogPath} with version {$version}"); + Console::success(" Changelog updated with version {$version}"); return true; } else { - Console::error('Failed to write changelog file'); + 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 + private function updateExistingPr(string $repoName, string $gitBranch, string $prTitle, string $prBody, string $platformName, string $sdkName, array &$prUrls, string $existingPrUrl = ''): void { - Console::warning("Pull request already exists for {$sdkName} SDK, updating title and body..."); + Console::log(' Pull request already exists, updating...'); - $prNumberCommand = 'cd ' . $target . ' && \ - gh pr list \ - --repo ' . \escapeshellarg($repoName) . ' \ - --head ' . \escapeshellarg($gitBranch) . ' \ - --json number \ - --jq ".[0].number" \ - 2>&1'; + $prNumber = ''; + $prUrl = ''; - $prNumberOutput = []; - $prNumberReturnCode = 0; - \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); + // Try extracting from the gh pr create error output first (free, no API call) + if (! empty($existingPrUrl) && \preg_match('#/pull/(\d+)#', $existingPrUrl, $matches)) { + $prNumber = $matches[1]; + $prUrl = $existingPrUrl; + } - if ($prNumberReturnCode !== 0 || empty($prNumberOutput[0])) { - Console::error("Failed to get PR number for {$sdkName} SDK"); + // Otherwise, look it up via gh pr list + if (empty($prNumber)) { + $prListCommand = 'gh pr list' + . ' --repo ' . \escapeshellarg($repoName) + . ' --head ' . \escapeshellarg($gitBranch) + . ' --json number,url' + . ' --jq ".[0] | (.number|tostring) + \" \" + .url"' + . ' 2>&1'; + + $prListOutput = []; + \exec($prListCommand, $prListOutput); + + if (! empty($prListOutput[0])) { + $parts = \explode(' ', trim($prListOutput[0]), 2); + $prNumber = $parts[0] ?? ''; + $prUrl = $parts[1] ?? ''; + } + } + + if (empty($prNumber)) { + Console::error(" Failed to find existing PR for branch {$gitBranch}"); 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'; + $updateCommand = '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)); + Console::error(" Failed to update pull request: " . implode("\n", $updateOutput)); return; } - Console::success("Successfully updated pull request for {$sdkName} SDK"); + Console::success(" Pull request updated"); - $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]); + if (! empty($prUrl)) { + $prUrls[$platformName][$sdkName] = $prUrl; } } }