From 8fddfbea97c52469448fa7b9f2b9ccfeb3b32103 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 15 Dec 2025 17:50:24 +0100 Subject: [PATCH 1/6] add support for Gitlab --- README.md | 11 +- src/Bootstrap.php | 4 +- src/Module/Config/Schema/GitLab.php | 21 +++ .../Repository/Internal/GitLab/Api/Client.php | 71 ++++++++ .../Internal/GitLab/Api/RepositoryApi.php | 165 ++++++++++++++++++ .../GitLab/Api/Response/AssetInfo.php | 38 ++++ .../GitLab/Api/Response/ReleaseInfo.php | 58 ++++++ .../GitLab/Api/Response/RepositoryInfo.php | 53 ++++++ .../Exception/GitLabRateLimitException.php | 29 +++ .../Repository/Internal/GitLab/Factory.php | 59 +++++++ .../Internal/GitLab/GitLabAsset.php | 80 +++++++++ .../Internal/GitLab/GitLabRelease.php | 58 ++++++ .../Internal/GitLab/GitLabRepository.php | 105 +++++++++++ 13 files changed, 750 insertions(+), 2 deletions(-) create mode 100644 src/Module/Config/Schema/GitLab.php create mode 100644 src/Module/Repository/Internal/GitLab/Api/Client.php create mode 100644 src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php create mode 100644 src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php create mode 100644 src/Module/Repository/Internal/GitLab/Api/Response/ReleaseInfo.php create mode 100644 src/Module/Repository/Internal/GitLab/Api/Response/RepositoryInfo.php create mode 100644 src/Module/Repository/Internal/GitLab/Exception/GitLabRateLimitException.php create mode 100644 src/Module/Repository/Internal/GitLab/Factory.php create mode 100644 src/Module/Repository/Internal/GitLab/GitLabAsset.php create mode 100644 src/Module/Repository/Internal/GitLab/GitLabRelease.php create mode 100644 src/Module/Repository/Internal/GitLab/GitLabRepository.php diff --git a/README.md b/README.md index 07929d5..b1d7ee6 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,14 @@ This ensures consistent Velox versions across different environments and team me + + + + + + ``` @@ -553,12 +561,13 @@ Each developer gets the correct binaries for their system: ``` -## GitHub API Rate Limits +## API Rate Limits Use a personal access token to avoid rate limits: ```bash GITHUB_TOKEN=your_token_here ./vendor/bin/dload get +GITLAB_TOKEN=your_token_here ./vendor/bin/dload get ``` Add to CI/CD environment variables for automated downloads. diff --git a/src/Bootstrap.php b/src/Bootstrap.php index a85759c..a4bc5ff 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -14,6 +14,7 @@ use Internal\DLoad\Module\HttpClient\Factory; use Internal\DLoad\Module\HttpClient\Internal\NyholmFactoryImpl; use Internal\DLoad\Module\Repository\Internal\GitHub\Factory as GithubRepositoryFactory; +use Internal\DLoad\Module\Repository\Internal\GitLab\Factory as GitLabRepositoryFactory; use Internal\DLoad\Module\Repository\RepositoryProvider; use Internal\DLoad\Module\Velox\ApiClient; use Internal\DLoad\Module\Velox\Builder; @@ -106,7 +107,8 @@ public function withConfig( $this->container->bind( RepositoryProvider::class, static fn(Container $container): RepositoryProvider => (new RepositoryProvider()) - ->addRepositoryFactory($container->get(GithubRepositoryFactory::class)), + ->addRepositoryFactory($container->get(GithubRepositoryFactory::class)) + ->addRepositoryFactory($container->get(GitLabRepositoryFactory::class)), ); $this->container->bind(BinaryProvider::class, BinaryProviderImpl::class); $this->container->bind(Factory::class, NyholmFactoryImpl::class); diff --git a/src/Module/Config/Schema/GitLab.php b/src/Module/Config/Schema/GitLab.php new file mode 100644 index 0000000..e049139 --- /dev/null +++ b/src/Module/Config/Schema/GitLab.php @@ -0,0 +1,21 @@ + + */ + private array $defaultHeaders = [ + 'accept' => 'application/json', + ]; + + public function __construct( + private readonly HttpFactory $httpFactory, + private readonly ClientInterface $client, + private readonly GitLab $gitLabConfig, + ) { + // Add authorization header if token is available + $this->gitLabConfig->token !== null and $this->defaultHeaders['authorization'] = 'Bearer ' . $this->gitLabConfig->token; + } + + /** + * @param Method|non-empty-string $method + * @param array $headers + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + public function request(Method|string $method, string|UriInterface $uri, array $headers = []): ResponseInterface + { + $request = $this->httpFactory->request($method, $uri, $headers + $this->defaultHeaders); + + return $this->sendRequest($request); + } + + /** + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + $response = $this->client->sendRequest($request); + + if ($response->getStatusCode() === 403) { + throw GitLabRateLimitException::fromApiResponse(); + } + + return $response; + } +} diff --git a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php new file mode 100644 index 0000000..cf41ebb --- /dev/null +++ b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php @@ -0,0 +1,165 @@ +repositoryPath = $projectPath; + } + + /** + * @param Method|non-empty-string $method + * @param array $headers + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + public function request(Method|string $method, string|UriInterface $uri, array $headers = []): ResponseInterface + { + return $this->client->request($method, $uri, $headers); + } + + /** + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + public function getRepository(): RepositoryInfo + { + $response = $this->request(Method::Get, \sprintf(self::URL_REPOSITORY, urlencode($this->repositoryPath))); + + /** @var array{ + * name: string, + * name_with_namespace: string, + * description: string|null, + * web_url: string, + * visibility: bool, + * created_at: string, + * updated_at: string + * } $data */ + $data = \json_decode($response->getBody()->__toString(), true, 512, JSON_THROW_ON_ERROR); + + return RepositoryInfo::fromApiResponse($data); + } + + /** + * @param int<1, max> $page + * @return Paginator + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + public function getReleases(int $page = 1): Paginator + { + $pageLoader = function () use ($page): \Generator { + $currentPage = $page; + + do { + try { + $response = $this->releasesRequest($currentPage); + + /** @var array + * }, + * upcoming_release: bool + * }> $data */ + $data = \json_decode($response->getBody()->__toString(), true, 512, JSON_THROW_ON_ERROR); + + // If empty response, no more pages + if ($data === []) { + return; + } + + $releases = []; + foreach ($data as $releaseData) { + try { + $releases[] = ReleaseInfo::fromApiResponse($releaseData); + } catch (\Throwable) { + // Skip invalid releases + continue; + } + } + + yield $releases; + + // Check if there are more pages + $hasMorePages = $this->hasNextPage($response); + $currentPage++; + } catch (ClientExceptionInterface) { + return; + } + } while ($hasMorePages); + }; + + return Paginator::createFromGenerator($pageLoader(), null); + } + + /** + * @param positive-int $page + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + private function releasesRequest(int $page): ResponseInterface + { + return $this->request( + Method::Get, + $this->httpFactory->uri( + \sprintf(self::URL_RELEASES, urlencode($this->repositoryPath)), + ['page' => $page], + ), + ); + } + + private function hasNextPage(ResponseInterface $response): bool + { + $headers = $response->getHeaders(); + $link = $headers['link'] ?? []; + + if (!isset($link[0])) { + return false; + } + + return \str_contains($link[0], 'rel="next"'); + } +} diff --git a/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php b/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php new file mode 100644 index 0000000..3478da3 --- /dev/null +++ b/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php @@ -0,0 +1,38 @@ + $assets + */ + public function __construct( + public readonly string $name, + public readonly string $tagName, + public readonly \DateTimeImmutable $publishedAt, + public readonly array $assets, + public readonly bool $prerelease, + ) {} + + /** + * @param array{ + * name: string|null, + * tag_name: string, + * released_at: string, + * assets: array{ + * links: list + * }, + * upcoming_release: bool + * } $data + */ + public static function fromApiResponse(array $data): self + { + $assets = []; + foreach ($data['assets']['links'] as $assetData) { + $assets[] = AssetInfo::fromApiResponse($assetData); + } + + return new self( + name: $data['name'] ?? $data['tag_name'], + tagName: $data['tag_name'], + publishedAt: new \DateTimeImmutable($data['released_at']), + assets: $assets, + prerelease: $data['upcoming_release'], + ); + } +} diff --git a/src/Module/Repository/Internal/GitLab/Api/Response/RepositoryInfo.php b/src/Module/Repository/Internal/GitLab/Api/Response/RepositoryInfo.php new file mode 100644 index 0000000..a0b2f32 --- /dev/null +++ b/src/Module/Repository/Internal/GitLab/Api/Response/RepositoryInfo.php @@ -0,0 +1,53 @@ +gitLabClient = new Client( + $httpFactory, + $httpFactory->client(), + $gitLabConfig, + ); + } + + public function supports(RepositoryConfig $config): bool + { + return \strtolower($config->type) === 'gitlab'; + } + + public function create(RepositoryConfig $config): GitLabRepository + { + $uri = \parse_url($config->uri, PHP_URL_PATH) ?? $config->uri; + $api = $this->createRepositoryApi($uri); + + return new GitLabRepository($api, $uri); + } + + /** + * @param non-empty-string $projectPath + */ + private function createRepositoryApi(string $projectPath): RepositoryApi + { + return new RepositoryApi($this->gitLabClient, $this->httpFactory, $projectPath); + } +} diff --git a/src/Module/Repository/Internal/GitLab/GitLabAsset.php b/src/Module/Repository/Internal/GitLab/GitLabAsset.php new file mode 100644 index 0000000..97601ed --- /dev/null +++ b/src/Module/Repository/Internal/GitLab/GitLabAsset.php @@ -0,0 +1,80 @@ +name, $dto->downloadUrl); + } + + /** + * @param null|\Closure(int $dlNow, int|null $dlSize, array $info): mixed $progress + * throwing any exceptions MUST abort the request; + * it MUST be called on DNS resolution, on arrival of headers and on completion; + * it SHOULD be called on upload/download of data and at least 1/s + * + * @return \Generator + * @throws ClientExceptionInterface + */ + public function download(?\Closure $progress = null): \Generator + { + $response = $this->api->request(Method::Get, $this->getUri()); + + $body = $response->getBody(); + $size = $body->getSize(); + $loaded = 0; + + while (!$body->eof()) { + $chunk = $body->read(8192); + $loaded += \strlen($chunk); + $progress === null or $progress($loaded, $size, []); + yield $chunk; + } + } + + public function destroy(): void + { + unset($this->release); + } +} diff --git a/src/Module/Repository/Internal/GitLab/GitLabRelease.php b/src/Module/Repository/Internal/GitLab/GitLabRelease.php new file mode 100644 index 0000000..d3f655b --- /dev/null +++ b/src/Module/Repository/Internal/GitLab/GitLabRelease.php @@ -0,0 +1,58 @@ +tagName); + $result = new self($repository, $dto->name, $version); + + $result->assets = AssetsCollection::create(static function () use ($api, $result, $dto): \Generator { + foreach ($dto->assets as $assetDTO) { + yield GitLabAsset::fromDTO($api, $result, $assetDTO); + } + }); + + return $result; + } + + public function destroy(): void + { + $this->assets === null or $this->assets->map( + static fn(object $asset) => $asset instanceof Destroyable and $asset->destroy(), + ); + + unset($this->assets, $this->repository); + } +} diff --git a/src/Module/Repository/Internal/GitLab/GitLabRepository.php b/src/Module/Repository/Internal/GitLab/GitLabRepository.php new file mode 100644 index 0000000..8e1c9c4 --- /dev/null +++ b/src/Module/Repository/Internal/GitLab/GitLabRepository.php @@ -0,0 +1,105 @@ +name = $projectPath; + } + + /** + * Returns a lazily loaded collection of repository releases. + * Pages are loaded only when needed during iteration or filtering. + */ + public function getReleases(): ReleasesCollection + { + if ($this->releases !== null) { + return $this->releases; + } + + // Create a generator function for lazy loading release pages + $pageLoader = function (): \Generator { + $page = 0; + + do { + try { + // to avoid first eager loading because of generator + yield []; + + $paginator = $this->api->getReleases(++$page); + $releases = $paginator->getPageItems(); + + $toYield = []; + foreach ($releases as $releaseDTO) { + try { + $toYield[] = GitLabRelease::fromDTO($this->api, $this, $releaseDTO); + } catch (\Throwable) { + // Skip invalid releases + continue; + } + } + yield $toYield; + + // Check if there are more pages by getting next page + $hasMorePages = $paginator->getNextPage() !== null; + } catch (GitLabRateLimitException $e) { + throw $e; + } catch (\Throwable) { + return; + } + } while ($hasMorePages); + }; + + // Create paginator + $paginator = \Internal\DLoad\Module\Repository\Internal\Paginator::createFromGenerator($pageLoader(), null); + + // Create a collection with the paginator + $this->releases = ReleasesCollection::create($paginator); + + return $this->releases; + } + + public function getName(): string + { + return $this->name; + } + + public function destroy(): void + { + $this->releases === null or $this->releases->map( + static fn(object $release) => $release instanceof Destroyable and $release->destroy(), + ); + + unset($this->releases); + } +} From 2bebd7a35cfdab6630ec9eff5bd16b6489d9d5b6 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 15 Dec 2025 20:48:10 +0100 Subject: [PATCH 2/6] fix: download --- README.md | 35 +++++++++++++++++++ .../Repository/Internal/GitLab/Api/Client.php | 18 ++++++++++ .../Internal/GitLab/Api/RepositoryApi.php | 13 +++++++ .../GitLab/Api/Response/AssetInfo.php | 2 +- .../Internal/GitLab/GitLabAsset.php | 2 +- 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b1d7ee6..1e203d6 100644 --- a/README.md +++ b/README.md @@ -572,6 +572,41 @@ GITLAB_TOKEN=your_token_here ./vendor/bin/dload get Add to CI/CD environment variables for automated downloads. +## Gitlab CI configuration + +When you make a release in Gitlab, make sure to upload your assets to the release page via +package manager. This can easily be done via Gitlab CLI and the `glab release upload --use-package-registry` +command. + +``` +# .gitlab-ci.yml + +Build artifacts: + stage: push + script: + - mkdir bin + - echo "Mock binary for darwin arm" > bin/cool-darwin-arm64 + - echo "Mock binary for darwin amd" > bin/cool-darwin-amd64 + - echo "Mock binary for linux arm" > bin/cool-linux-arm64 + - echo "Mock binary for linux amd" > bin/cool-linux-amd64 + artifacts: + expire_in: 2 hours + paths: + - $CI_PROJECT_DIR/bin/cool-* + rules: + - if: $CI_COMMIT_TAG + +Release artifacts: + stage: deploy + image: gitlab/glab:latest + needs: [ "Build artifacts" ] + script: + - glab auth login --token $GL_TOKEN --hostname $CI_SERVER_HOST + - glab release upload --use-package-registry "$CI_COMMIT_TAG" ./bin/* + rules: + - if: $CI_COMMIT_TAG +``` + ## Contributing Contributions welcome! Submit Pull Requests to: diff --git a/src/Module/Repository/Internal/GitLab/Api/Client.php b/src/Module/Repository/Internal/GitLab/Api/Client.php index 56a6f98..6efd64a 100644 --- a/src/Module/Repository/Internal/GitLab/Api/Client.php +++ b/src/Module/Repository/Internal/GitLab/Api/Client.php @@ -41,6 +41,24 @@ public function __construct( $this->gitLabConfig->token !== null and $this->defaultHeaders['authorization'] = 'Bearer ' . $this->gitLabConfig->token; } + /** + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + public function downloadArtifact(string|UriInterface $uri): ResponseInterface + { + $headers = []; + if ($this->gitLabConfig->token !== null) { + $headers = [ + 'PRIVATE-TOKEN' => $this->gitLabConfig->token + ]; + } + + $request = $this->httpFactory->request(Method::Get, $uri, $headers); + + return $this->sendRequest($request); + } + /** * @param Method|non-empty-string $method * @param array $headers diff --git a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php index cf41ebb..36d2277 100644 --- a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php +++ b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php @@ -26,6 +26,7 @@ final class RepositoryApi { private const URL_REPOSITORY = 'https://gitlab.com/api/v4/projects/%s'; private const URL_RELEASES = 'https://gitlab.com/api/v4/projects/%s/releases'; + private const URL_RELEASE_ASSET = 'https://gitlab.com/api/v4/projects/%s/releases/%s/downloads/%s'; /** * @var non-empty-string @@ -43,6 +44,18 @@ public function __construct( $this->repositoryPath = $projectPath; } + /** + * @param non-empty-string $repositoryPath + * @param non-empty-string $releaseName + * @param non-empty-string $fileName + * @throws GitLabRateLimitException + * @throws ClientExceptionInterface + */ + public function downloadArtifact(string $repositoryPath, string $releaseName, string $fileName): ResponseInterface + { + $url = sprintf(self::URL_RELEASE_ASSET, urlencode($repositoryPath), $releaseName, $fileName); + return $this->client->downloadArtifact($url); + } /** * @param Method|non-empty-string $method * @param array $headers diff --git a/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php b/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php index 3478da3..2966c2c 100644 --- a/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php +++ b/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php @@ -32,7 +32,7 @@ public static function fromApiResponse(array $data): self { return new self( name: $data['name'], - downloadUrl: $data['direct_asset_url'], + downloadUrl: $data['url'], ); } } diff --git a/src/Module/Repository/Internal/GitLab/GitLabAsset.php b/src/Module/Repository/Internal/GitLab/GitLabAsset.php index 97601ed..ed69d0a 100644 --- a/src/Module/Repository/Internal/GitLab/GitLabAsset.php +++ b/src/Module/Repository/Internal/GitLab/GitLabAsset.php @@ -59,7 +59,7 @@ public static function fromDTO( */ public function download(?\Closure $progress = null): \Generator { - $response = $this->api->request(Method::Get, $this->getUri()); + $response = $this->api->downloadArtifact($this->release->getRepository()->getName(), $this->release->getName(), $this->getName()); $body = $response->getBody(); $size = $body->getSize(); From 8ed02c2e9879d4d31750596088e9593e35e02e48 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 15 Dec 2025 20:48:51 +0100 Subject: [PATCH 3/6] fix cs --- src/Module/Repository/Internal/GitLab/Api/Client.php | 2 +- .../Repository/Internal/GitLab/Api/RepositoryApi.php | 10 ++++------ src/Module/Repository/Internal/GitLab/GitLabAsset.php | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Module/Repository/Internal/GitLab/Api/Client.php b/src/Module/Repository/Internal/GitLab/Api/Client.php index 6efd64a..afbf63b 100644 --- a/src/Module/Repository/Internal/GitLab/Api/Client.php +++ b/src/Module/Repository/Internal/GitLab/Api/Client.php @@ -50,7 +50,7 @@ public function downloadArtifact(string|UriInterface $uri): ResponseInterface $headers = []; if ($this->gitLabConfig->token !== null) { $headers = [ - 'PRIVATE-TOKEN' => $this->gitLabConfig->token + 'PRIVATE-TOKEN' => $this->gitLabConfig->token, ]; } diff --git a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php index 36d2277..43fb1d0 100644 --- a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php +++ b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php @@ -33,9 +33,6 @@ final class RepositoryApi */ public readonly string $repositoryPath; - /** - * @param non-empty-string $repo - */ public function __construct( private readonly Client $client, private readonly HttpFactory $httpFactory, @@ -53,9 +50,10 @@ public function __construct( */ public function downloadArtifact(string $repositoryPath, string $releaseName, string $fileName): ResponseInterface { - $url = sprintf(self::URL_RELEASE_ASSET, urlencode($repositoryPath), $releaseName, $fileName); + $url = \sprintf(self::URL_RELEASE_ASSET, \urlencode($repositoryPath), $releaseName, $fileName); return $this->client->downloadArtifact($url); } + /** * @param Method|non-empty-string $method * @param array $headers @@ -73,7 +71,7 @@ public function request(Method|string $method, string|UriInterface $uri, array $ */ public function getRepository(): RepositoryInfo { - $response = $this->request(Method::Get, \sprintf(self::URL_REPOSITORY, urlencode($this->repositoryPath))); + $response = $this->request(Method::Get, \sprintf(self::URL_REPOSITORY, \urlencode($this->repositoryPath))); /** @var array{ * name: string, @@ -158,7 +156,7 @@ private function releasesRequest(int $page): ResponseInterface return $this->request( Method::Get, $this->httpFactory->uri( - \sprintf(self::URL_RELEASES, urlencode($this->repositoryPath)), + \sprintf(self::URL_RELEASES, \urlencode($this->repositoryPath)), ['page' => $page], ), ); diff --git a/src/Module/Repository/Internal/GitLab/GitLabAsset.php b/src/Module/Repository/Internal/GitLab/GitLabAsset.php index ed69d0a..b4c9383 100644 --- a/src/Module/Repository/Internal/GitLab/GitLabAsset.php +++ b/src/Module/Repository/Internal/GitLab/GitLabAsset.php @@ -7,7 +7,6 @@ use Internal\Destroy\Destroyable; use Internal\DLoad\Module\Common\Architecture; use Internal\DLoad\Module\Common\OperatingSystem; -use Internal\DLoad\Module\HttpClient\Method; use Internal\DLoad\Module\Repository\Internal\Asset; use Internal\DLoad\Module\Repository\Internal\GitLab\Api\Response\AssetInfo; use Internal\DLoad\Module\Repository\Internal\GitLab\Api\RepositoryApi; From 962f3da38a0f03c671c9df199b623cd11a566a79 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 16 Dec 2025 08:32:39 +0100 Subject: [PATCH 4/6] readme fi --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e203d6..5c733ca 100644 --- a/README.md +++ b/README.md @@ -601,7 +601,7 @@ Release artifacts: image: gitlab/glab:latest needs: [ "Build artifacts" ] script: - - glab auth login --token $GL_TOKEN --hostname $CI_SERVER_HOST + - glab auth login --job-token $CI_JOB_TOKEN --hostname $CI_SERVER_HOST - glab release upload --use-package-registry "$CI_COMMIT_TAG" ./bin/* rules: - if: $CI_COMMIT_TAG From af02ae5f9514a6c89a50fa177b57d70106f5092d Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 16 Dec 2025 08:38:42 +0100 Subject: [PATCH 5/6] fix: rate limit exception handling --- src/Module/Repository/Internal/GitLab/Api/Client.php | 4 ++-- .../GitLab/Exception/GitLabRateLimitException.php | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Module/Repository/Internal/GitLab/Api/Client.php b/src/Module/Repository/Internal/GitLab/Api/Client.php index afbf63b..634408e 100644 --- a/src/Module/Repository/Internal/GitLab/Api/Client.php +++ b/src/Module/Repository/Internal/GitLab/Api/Client.php @@ -80,8 +80,8 @@ public function sendRequest(RequestInterface $request): ResponseInterface { $response = $this->client->sendRequest($request); - if ($response->getStatusCode() === 403) { - throw GitLabRateLimitException::fromApiResponse(); + if ($response->getStatusCode() === 429) { + throw new GitLabRateLimitException(); } return $response; diff --git a/src/Module/Repository/Internal/GitLab/Exception/GitLabRateLimitException.php b/src/Module/Repository/Internal/GitLab/Exception/GitLabRateLimitException.php index 86598f2..8f9dbae 100644 --- a/src/Module/Repository/Internal/GitLab/Exception/GitLabRateLimitException.php +++ b/src/Module/Repository/Internal/GitLab/Exception/GitLabRateLimitException.php @@ -18,12 +18,4 @@ public function __construct( ) { parent::__construct($message, 0, $previous); } - - /** - * Creates exception from GitLab API response body. - */ - public static function fromApiResponse(): self - { - return new self(); - } } From fe637e81a50520d7ec563ac16c5da0fe71bf5f19 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 17 Dec 2025 00:53:50 +0400 Subject: [PATCH 6/6] Use `direct_asset_url` for assets if exists --- .../Internal/GitLab/Api/RepositoryApi.php | 17 ++++++++------- .../GitLab/Api/Response/AssetInfo.php | 12 +++++++---- .../GitLab/Api/Response/ReleaseInfo.php | 21 +++++++++++-------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php index 43fb1d0..020aeb1 100644 --- a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php +++ b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php @@ -103,15 +103,18 @@ public function getReleases(int $page = 1): Paginator $response = $this->releasesRequest($currentPage); /** @var array + * name: non-empty-string, + * url: non-empty-string, + * direct_asset_url?: non-empty-string, + * link_type: non-empty-string, + * }> * }, * upcoming_release: bool * }> $data */ diff --git a/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php b/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php index 2966c2c..65544eb 100644 --- a/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php +++ b/src/Module/Repository/Internal/GitLab/Api/Response/AssetInfo.php @@ -15,24 +15,28 @@ final class AssetInfo /** * @param non-empty-string $name * @param non-empty-string $downloadUrl + * @param non-empty-string|null $linkType */ public function __construct( public readonly string $name, public readonly string $downloadUrl, + public readonly ?string $linkType = null, ) {} /** * @param array{ - * name: string, - * url: string, - * direct_asset_url: string, + * name: non-empty-string, + * url: non-empty-string, + * direct_asset_url?: non-empty-string, + * link_type: non-empty-string, * } $data */ public static function fromApiResponse(array $data): self { return new self( name: $data['name'], - downloadUrl: $data['url'], + downloadUrl: !empty($data['direct_asset_url']) ? $data['direct_asset_url'] : $data['url'], + linkType: $data['link_type'] ?? null, ); } } diff --git a/src/Module/Repository/Internal/GitLab/Api/Response/ReleaseInfo.php b/src/Module/Repository/Internal/GitLab/Api/Response/ReleaseInfo.php index 8f14685..07d6ee2 100644 --- a/src/Module/Repository/Internal/GitLab/Api/Response/ReleaseInfo.php +++ b/src/Module/Repository/Internal/GitLab/Api/Response/ReleaseInfo.php @@ -27,15 +27,18 @@ public function __construct( /** * @param array{ - * name: string|null, - * tag_name: string, - * released_at: string, - * assets: array{ - * links: list + * name: non-empty-string|null, + * tag_name: non-empty-string, + * description: null|non-empty-string, + * created_at: non-empty-string, + * released_at: non-empty-string, + * assets: array{ + * links: list * }, * upcoming_release: bool * } $data