Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
15 changes: 14 additions & 1 deletion src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -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,
),
]);
}

Expand Down
35 changes: 28 additions & 7 deletions src/Repository/GitHub/GitHubRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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());
}

/**
Expand Down
31 changes: 27 additions & 4 deletions tests/Unit/CommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
}
20 changes: 18 additions & 2 deletions tests/Unit/Repository/GitHub/GitHubRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading