From 8d54ffead1a272ff0c486e2b70e5970dd7bfb627 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 10 Oct 2026 02:09:14 +0400 Subject: [PATCH] feat: read the GitHub API URL from RR_GITHUB_API_URL Releases can be fetched from GitHub Enterprise, a mirror or a mock server. The variable is not `GITHUB_API_URL` because GitHub Actions sets that one in every job. Releases are requested 100 per page, so listing them takes 2 requests instead of about 7. Assisted-By: Claude Opus 5.5 --- README.md | 2 +- src/Command.php | 15 +++++++- src/Repository/GitHub/GitHubRepository.php | 35 +++++++++++++++---- tests/Unit/CommandTest.php | 31 +++++++++++++--- .../GitHub/GitHubRepositoryTest.php | 20 +++++++++-- 5 files changed, 88 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fcf4bb9..3f2f1b2 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,4 @@ See the [RoadRunner documentation](https://docs.roadrunner.dev) for the configur ### Common options -The binary commands accept `--filter` (`-f`, version constraint), `--stability` (`-s`), `--os` (`-o`), `--arch` (`-a`) and `--location` (`-l`, target directory) options. Releases are fetched from the GitHub API; set the `GITHUB_TOKEN` environment variable to avoid its rate limits. +The binary commands accept `--filter` (`-f`, version constraint), `--stability` (`-s`), `--os` (`-o`), `--arch` (`-a`) and `--location` (`-l`, target directory) options. Releases are fetched from the GitHub API; set the `GITHUB_TOKEN` environment variable to avoid its rate limits, and `RR_GITHUB_API_URL` to use another API endpoint (GitHub Enterprise, a mirror; defaults to `https://api.github.com`). diff --git a/src/Command.php b/src/Command.php index ef3fb4b..27e726a 100644 --- a/src/Command.php +++ b/src/Command.php @@ -31,9 +31,17 @@ abstract class Command extends BaseCommand */ private const ENV_GITHUB_TOKEN = 'GITHUB_TOKEN'; + /** + * Not `GITHUB_API_URL`: GitHub Actions sets that one in every job. + * + * @var string + */ + private const ENV_GITHUB_API_URL = 'RR_GITHUB_API_URL'; + protected function getRepository(): RepositoryInterface { $token = Environment::get(self::ENV_GITHUB_TOKEN); + $apiUrl = Environment::get(self::ENV_GITHUB_API_URL); $client = HttpClient::create([ 'headers' => \array_filter([ @@ -42,7 +50,12 @@ protected function getRepository(): RepositoryInterface ]); return new RepositoriesCollection([ - GitHubRepository::create('roadrunner-server', 'roadrunner', $client), + GitHubRepository::create( + 'roadrunner-server', + 'roadrunner', + $client, + $apiUrl !== null && $apiUrl !== '' ? $apiUrl : GitHubRepository::DEFAULT_API_URL, + ), ]); } diff --git a/src/Repository/GitHub/GitHubRepository.php b/src/Repository/GitHub/GitHubRepository.php index 339eb81..6c79ee5 100644 --- a/src/Repository/GitHub/GitHubRepository.php +++ b/src/Repository/GitHub/GitHubRepository.php @@ -24,13 +24,21 @@ */ final class GitHubRepository implements RepositoryInterface { + public const DEFAULT_API_URL = 'https://api.github.com'; + /** * @var string */ - private const URL_RELEASES = 'https://api.github.com/repos/%s/releases'; + private const URL_RELEASES = '%s/repos/%s/releases'; + + /** + * The largest page size GitHub accepts. + */ + private const PER_PAGE = 100; private HttpClientInterface $client; private string $name; + private string $apiUrl; /** * @var array|string[] @@ -39,15 +47,27 @@ final class GitHubRepository implements RepositoryInterface 'accept' => 'application/vnd.github.v3+json', ]; - public function __construct(string $owner, string $repository, ?HttpClientInterface $client = null) - { + /** + * @param string $apiUrl Base URL of the GitHub REST API: GitHub Enterprise, a mirror or a mock server. + */ + public function __construct( + string $owner, + string $repository, + ?HttpClientInterface $client = null, + string $apiUrl = self::DEFAULT_API_URL, + ) { $this->name = $owner . '/' . $repository; $this->client = $client ?? HttpClient::create(); + $this->apiUrl = \rtrim($apiUrl, '/'); } - public static function create(string $owner, string $name, ?HttpClientInterface $client = null): GitHubRepository - { - return new GitHubRepository($owner, $name, $client); + public static function create( + string $owner, + string $name, + ?HttpClientInterface $client = null, + string $apiUrl = self::DEFAULT_API_URL, + ): GitHubRepository { + return new GitHubRepository($owner, $name, $client, $apiUrl); } /** @@ -98,13 +118,14 @@ private function releasesRequest(int $page): ResponseInterface return $this->request('GET', $this->uri(self::URL_RELEASES), [ 'query' => [ 'page' => $page, + 'per_page' => self::PER_PAGE, ], ]); } private function uri(string $pattern): string { - return \sprintf($pattern, $this->getName()); + return \sprintf($pattern, $this->apiUrl, $this->getName()); } /** diff --git a/tests/Unit/CommandTest.php b/tests/Unit/CommandTest.php index 1453bb7..6fcb39a 100644 --- a/tests/Unit/CommandTest.php +++ b/tests/Unit/CommandTest.php @@ -8,12 +8,38 @@ use Spiral\RoadRunner\Console\Repository\RepositoriesCollection; use Spiral\RoadRunner\Console\Repository\RepositoryInterface; use Testo\Assert; +use Testo\Expect; use Testo\Test; #[Test] final class CommandTest { + private const ENV_API_URL = 'RR_GITHUB_API_URL'; + public function repositoryAggregatesRoadRunnerReleases(): void + { + $repository = self::repository(); + + Assert::instanceOf($repository, RepositoriesCollection::class); + Assert::same($repository->getName(), 'unknown/unknown'); + } + + public function repositoryRequestsApiUrlFromEnvironment(): void + { + // The unsupported scheme makes the client fail before any connection, with the URL in the message. + $_SERVER[self::ENV_API_URL] = 'ftp://github.mock/api'; + + try { + Expect::exception(\InvalidArgumentException::class) + ->withMessageContaining('ftp://github.mock/api/repos/roadrunner-server/roadrunner/releases'); + + self::repository()->getReleases()->empty(); + } finally { + unset($_SERVER[self::ENV_API_URL]); + } + } + + private static function repository(): RepositoryInterface { $command = new class('test') extends Command { public function repository(): RepositoryInterface @@ -22,9 +48,6 @@ public function repository(): RepositoryInterface } }; - $repository = $command->repository(); - - Assert::instanceOf($repository, RepositoriesCollection::class); - Assert::same($repository->getName(), 'unknown/unknown'); + return $command->repository(); } } diff --git a/tests/Unit/Repository/GitHub/GitHubRepositoryTest.php b/tests/Unit/Repository/GitHub/GitHubRepositoryTest.php index b0a6989..f6fb8d5 100644 --- a/tests/Unit/Repository/GitHub/GitHubRepositoryTest.php +++ b/tests/Unit/Repository/GitHub/GitHubRepositoryTest.php @@ -40,11 +40,27 @@ static function (string $method, string $url, array $options) use (&$requests, $ Assert::same(Releases::versions($releases), ['v2024.2.0', 'v2024.1.0']); Assert::same($requests, [ - ['GET', 'https://api.github.com/repos/roadrunner-server/roadrunner/releases?page=1', 'accept: application/vnd.github.v3+json'], - ['GET', 'https://api.github.com/repos/roadrunner-server/roadrunner/releases?page=2', 'accept: application/vnd.github.v3+json'], + ['GET', 'https://api.github.com/repos/roadrunner-server/roadrunner/releases?page=1&per_page=100', 'accept: application/vnd.github.v3+json'], + ['GET', 'https://api.github.com/repos/roadrunner-server/roadrunner/releases?page=2&per_page=100', 'accept: application/vnd.github.v3+json'], ]); } + public function getReleasesUsesCustomApiUrl(): void + { + $urls = []; + $client = new MockHttpClient(static function (string $method, string $url) use (&$urls): MockResponse { + $urls[] = $url; + + return new MockResponse('[]'); + }); + + GitHubRepository::create('roadrunner-server', 'roadrunner', $client, 'http://127.0.0.1:8080/api/v3/') + ->getReleases() + ->empty(); + + Assert::same($urls, ['http://127.0.0.1:8080/api/v3/repos/roadrunner-server/roadrunner/releases?page=1&per_page=100']); + } + public function getReleasesStopsWithoutLinkHeader(): void { $calls = 0;