diff --git a/README-es.md b/README-es.md
index b3ca0532..f9963562 100644
--- a/README-es.md
+++ b/README-es.md
@@ -560,6 +560,12 @@ El binario de RoadRunner construido incluirá solo los plugins especificados en
+
+
+
+
+
+
```
@@ -571,6 +577,7 @@ El binario de RoadRunner construido incluirá solo los plugins especificados en
- **type**: Actualmente soporta "github"
- **uri**: Ruta del repositorio (ej., "username/repo")
- **asset-pattern**: Patrón regex para hacer match con assets de release
+- **tag-prefix**: Texto antes de la versión en los tags de release, p. ej. `bun-` para `bun-v1.4.2`. Los releases con otros tags se ignoran, así que de un monorepo con tags por componente (release-please) solo se usa el componente seleccionado
#### Elementos Binary
diff --git a/README-ru.md b/README-ru.md
index 9c34f9e5..38d78516 100644
--- a/README-ru.md
+++ b/README-ru.md
@@ -562,6 +562,12 @@ DLoad автоматически управляет процессом сбор
+
+
+
+
+
+
```
@@ -573,6 +579,7 @@ DLoad автоматически управляет процессом сбор
- **type**: В настоящее время поддерживает "github"
- **uri**: Путь репозитория (например, "username/repo")
- **asset-pattern**: Regex-паттерн для соответствия ресурсам релиза
+- **tag-prefix**: Текст перед версией в тегах релизов, например `bun-` для `bun-v1.4.2`. Релизы с другими тегами игнорируются, поэтому из монорепозитория с тегами по компонентам (release-please) берётся только нужный компонент
#### Элементы Binary
diff --git a/README-zh.md b/README-zh.md
index 463b9b13..2316910f 100644
--- a/README-zh.md
+++ b/README-zh.md
@@ -553,6 +553,12 @@ DLoad 会自动处理构建过程:
+
+
+
+
+
+
```
@@ -564,6 +570,7 @@ DLoad 会自动处理构建过程:
- **type**:目前支持 "github"
- **uri**:仓库路径(例如 "username/repo")
- **asset-pattern**:匹配发布资源的正则表达式模式
+- **tag-prefix**:发布标签中版本号之前的文本,例如 `bun-v1.4.2` 中的 `bun-`。标签不以它开头的发布会被忽略,因此对于按组件打标签的 monorepo(release-please),只会使用选定的组件
#### 二进制要素
diff --git a/README.md b/README.md
index 86bfe516..cc98273c 100644
--- a/README.md
+++ b/README.md
@@ -566,6 +566,12 @@ This ensures consistent Velox versions across different environments and team me
+
+
+
+
+
+
```
@@ -577,6 +583,7 @@ This ensures consistent Velox versions across different environments and team me
- **type**: Currently supports "github"
- **uri**: Repository path (e.g., "username/repo")
- **asset-pattern**: Regex pattern to match release assets
+- **tag-prefix**: Text before the version in release tags, e.g. `bun-` for `bun-v1.4.2`. Releases with other tags are ignored, so only the selected component of a monorepo with per-component tags (release-please) is used
#### Binary Elements
diff --git a/dload.xsd b/dload.xsd
index b737373b..3fa0168a 100644
--- a/dload.xsd
+++ b/dload.xsd
@@ -166,6 +166,11 @@
Regular expression pattern to match assets
+
+
+ Text before the version in release tags, like "bun-" in "bun-v1.4.2". Releases whose tag does not start with it are ignored, which selects one component of a monorepo (release-please style tags).
+
+
diff --git a/resources/software.json b/resources/software.json
index de666e2b..0a58283e 100644
--- a/resources/software.json
+++ b/resources/software.json
@@ -276,6 +276,24 @@
"name": "deno",
"version-command": "--version"
}
+ },
+ {
+ "name": "Bun",
+ "alias": "bun",
+ "description": "Incredibly fast JavaScript runtime, bundler, test runner, and package manager - all in one.",
+ "homepage": "https://bun.com",
+ "repositories": [
+ {
+ "type": "github",
+ "uri": "oven-sh/bun",
+ "tag-prefix": "bun-",
+ "asset-pattern": "/^bun-[a-z]+-(?:x64|aarch64)\\.zip$/"
+ }
+ ],
+ "binary": {
+ "name": "bun",
+ "version-command": "--version"
+ }
}
]
}
diff --git a/resources/software.schema.json b/resources/software.schema.json
index 4489b968..55d12a97 100644
--- a/resources/software.schema.json
+++ b/resources/software.schema.json
@@ -79,6 +79,10 @@
"asset-pattern": {
"type": "string",
"description": "Regular expression pattern to match assets"
+ },
+ "tag-prefix": {
+ "type": "string",
+ "description": "Text before the version in release tags, like `bun-` in `bun-v1.4.2`. Releases whose tag does not start with it are ignored, which selects one component of a monorepo (release-please style tags)."
}
}
}
diff --git a/skills/dload-fetch-tool/references/registry-entry.md b/skills/dload-fetch-tool/references/registry-entry.md
index d1a9357c..d8eac32c 100644
--- a/skills/dload-fetch-tool/references/registry-entry.md
+++ b/skills/dload-fetch-tool/references/registry-entry.md
@@ -5,7 +5,7 @@ How to describe a tool dload does not ship, as a `` block inside `//releases/tags/ \
@@ -38,6 +38,12 @@ Tokens the OS/arch matchers recognise (case-insensitive, bounded by `_` or a wor
Patterns are slash-delimited, and in XML attributes backslashes are literal — write `\.exe`.
+## `tag-prefix`
+
+A literal string the release tags start with, cut off before the version is parsed. dload reads the version from the tag, so a tag like `bun-v1.4.2` is skipped as unparsable until `tag-prefix="bun-"` is set.
+
+It also selects one component of a monorepo with per-component tags (release-please: `cli-v2.0.0`, `sdk-v1.4.0`): releases whose tag does not start with the prefix are ignored. Include the separator in the prefix (`cli-`, `cli/`), leave the `v` out — it is part of the version.
+
## `binary.pattern`
Matches the **executable on disk after the asset is downloaded and extracted**:
diff --git a/skills/dload-fetch-tool/references/troubleshooting.md b/skills/dload-fetch-tool/references/troubleshooting.md
index 7e12fb6d..6cd7ca44 100644
--- a/skills/dload-fetch-tool/references/troubleshooting.md
+++ b/skills/dload-fetch-tool/references/troubleshooting.md
@@ -13,6 +13,7 @@ curl -s "https://api.github.com/repos///releases?per_page=20" \
- `prerelease: true` while `version` demands stable → lower the stability (`@beta`/`@RC`/`@alpha`).
- `draft: true` → invisible to dload until published.
+- Tags carry text before the version (`bun-v1.4.2`, `cli-v2.0.0`) → set `tag-prefix` on the ``; without it the tags do not parse and every release is skipped. See [`registry-entry.md`](registry-entry.md#tag-prefix).
- Tag is on GitHub but dload picks an older one → the version registry still holds a fresh check; rerun with `dload get --refresh`. A release inserted below the top of the listing (e.g. a GitLab release with a backdated date) needs `dload cache:clear `. See [`version-registry.md`](version-registry.md).
- API errors (HTTP 401, 403, rate limit) → set a valid `GITHUB_TOKEN` / `GITLAB_TOKEN`. When a check fails but the registry already holds releases of the repository, dload serves the stored ones (a rate limit is reported as "newer ones may be missing"); a never-seen repository fails outright.
diff --git a/src/Module/Config/Schema/Embed/Repository.php b/src/Module/Config/Schema/Embed/Repository.php
index 4eb6860d..4aafa5e9 100644
--- a/src/Module/Config/Schema/Embed/Repository.php
+++ b/src/Module/Config/Schema/Embed/Repository.php
@@ -23,7 +23,8 @@
* @psalm-type RepositoryArray = array{
* type: non-empty-string,
* uri: non-empty-string,
- * asset-pattern?: non-empty-string
+ * asset-pattern?: non-empty-string,
+ * tag-prefix?: string
* }
*/
#[InflectableConfig]
@@ -41,6 +42,14 @@ final class Repository
#[XPath('@asset-pattern')]
public string $assetPattern = '/^.*$/';
+ /**
+ * @var string $tagPrefix Text before the version in release tags, like `bun-` in `bun-v1.4.2`.
+ * Releases whose tag does not start with it are ignored, which selects one component
+ * in a monorepo with per-component tags (release-please style).
+ */
+ #[XPath('@tag-prefix')]
+ public string $tagPrefix = '';
+
/**
* Creates a Repository configuration from an array.
*
@@ -52,6 +61,7 @@ public static function fromArray(mixed $repositoryArray): self
$self->type = $repositoryArray['type'] ?? 'github';
$self->uri = $repositoryArray['uri'];
$self->assetPattern = $repositoryArray['asset-pattern'] ?? '/^.*$/';
+ $self->tagPrefix = $repositoryArray['tag-prefix'] ?? '';
return $self;
}
diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php
index b009427f..2e712641 100644
--- a/src/Module/Downloader/Downloader.php
+++ b/src/Module/Downloader/Downloader.php
@@ -234,8 +234,7 @@ private function processRepository(Repository $repository, DownloadContext $cont
return $context->release;
} catch (ReleaseGone $e) {
// The registry must not offer this release again, and the list needs a fresh check
- $tag = $context->release->getVersion()->string;
- $tag === '' or $this->registry->forget($context->repositoryId, $tag);
+ $this->registry->forget($context->repositoryId, $context->release->getTag());
$forgotten = true;
$context->releaseAttempt->reason ??= $e->getMessage();
diff --git a/src/Module/Repository/Internal/GitHub/Factory.php b/src/Module/Repository/Internal/GitHub/Factory.php
index 67731539..4001393b 100644
--- a/src/Module/Repository/Internal/GitHub/Factory.php
+++ b/src/Module/Repository/Internal/GitHub/Factory.php
@@ -52,7 +52,7 @@ public function create(RepositoryConfig $config): GitHubRepository
$api = $this->createRepositoryApi($org, $repo);
- return new GitHubRepository($api, $org, $repo, $this->logger, $this->registry);
+ return new GitHubRepository($api, $org, $repo, $this->logger, $this->registry, $config->tagPrefix);
}
/**
diff --git a/src/Module/Repository/Internal/GitHub/GitHubRelease.php b/src/Module/Repository/Internal/GitHub/GitHubRelease.php
index adffc49d..291c9c23 100644
--- a/src/Module/Repository/Internal/GitHub/GitHubRelease.php
+++ b/src/Module/Repository/Internal/GitHub/GitHubRelease.php
@@ -21,25 +21,31 @@ final class GitHubRelease extends Release implements Destroyable
{
/**
* @param non-empty-string $name
+ * @param non-empty-string $tag
*/
private function __construct(
GitHubRepository $repository,
string $name,
+ string $tag,
Version $version,
) {
- parent::__construct($repository, $name, $version);
+ parent::__construct($repository, $name, $tag, $version);
}
/**
+ * @param string $tagPrefix Prefix the tag starts with; it is not part of the version.
* @throws \InvalidArgumentException When the release tag is not a version.
*/
public static function fromRecord(
RepositoryApi $api,
GitHubRepository $repository,
ReleaseRecord $record,
+ string $tagPrefix = '',
): self {
- $version = Version::fromVersionString($record->tag);
- $result = new self($repository, $record->name, $version);
+ $version = \substr($record->tag, \strlen($tagPrefix));
+ $version === '' and throw new \InvalidArgumentException("Release tag `{$record->tag}` has no version.");
+ $version = Version::fromVersionString($version);
+ $result = new self($repository, $record->name, $record->tag, $version);
$result->assets = AssetsCollection::create(static function () use ($api, $result, $record): \Generator {
foreach ($record->assets as $asset) {
diff --git a/src/Module/Repository/Internal/GitHub/GitHubRepository.php b/src/Module/Repository/Internal/GitHub/GitHubRepository.php
index a0c80e03..d5033e44 100644
--- a/src/Module/Repository/Internal/GitHub/GitHubRepository.php
+++ b/src/Module/Repository/Internal/GitHub/GitHubRepository.php
@@ -44,6 +44,7 @@ public function __construct(
string $repo,
private readonly Logger $logger,
private readonly VersionRegistry $registry,
+ private readonly string $tagPrefix = '',
) {
$this->name = $org . '/' . $repo;
}
@@ -83,8 +84,13 @@ public function getReleases(): ReleasesCollection
$toYield = [];
foreach ($pages->current() ?? [] as $record) {
+ // Releases of other components sharing the repository
+ if (!\str_starts_with($record->tag, $this->tagPrefix)) {
+ continue;
+ }
+
try {
- $toYield[] = GitHubRelease::fromRecord($this->api, $this, $record);
+ $toYield[] = GitHubRelease::fromRecord($this->api, $this, $record, $this->tagPrefix);
} catch (\Throwable $e) {
$this->logger->exception($e, important: false);
// Skip invalid releases
diff --git a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php
index 5dec6646..d5a42c53 100644
--- a/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php
+++ b/src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php
@@ -52,13 +52,13 @@ public function __construct(
/**
* @param non-empty-string $repositoryPath
- * @param non-empty-string $releaseName
+ * @param non-empty-string $tag Tag of the release; GitLab addresses release downloads by it, not by the release name.
* @param non-empty-string $fileName
* @throws RepositoryException
*/
- public function downloadArtifact(string $repositoryPath, string $releaseName, string $fileName): ResponseInterface
+ public function downloadArtifact(string $repositoryPath, string $tag, string $fileName): ResponseInterface
{
- $url = \sprintf(self::URL_RELEASE_ASSET, \urlencode($repositoryPath), $releaseName, $fileName);
+ $url = \sprintf(self::URL_RELEASE_ASSET, \urlencode($repositoryPath), \rawurlencode($tag), $fileName);
return $this->client->downloadArtifact($url);
}
diff --git a/src/Module/Repository/Internal/GitLab/Factory.php b/src/Module/Repository/Internal/GitLab/Factory.php
index 2ccf6447..83e6106f 100644
--- a/src/Module/Repository/Internal/GitLab/Factory.php
+++ b/src/Module/Repository/Internal/GitLab/Factory.php
@@ -51,7 +51,7 @@ public function create(RepositoryConfig $config): GitLabRepository
$uri = \is_string($path) && $path !== '' ? $path : $config->uri;
$api = $this->createRepositoryApi($uri);
- return new GitLabRepository($api, $uri, $this->logger, $this->registry);
+ return new GitLabRepository($api, $uri, $this->logger, $this->registry, $config->tagPrefix);
}
/**
diff --git a/src/Module/Repository/Internal/GitLab/GitLabAsset.php b/src/Module/Repository/Internal/GitLab/GitLabAsset.php
index 927c1b6d..67350b98 100644
--- a/src/Module/Repository/Internal/GitLab/GitLabAsset.php
+++ b/src/Module/Repository/Internal/GitLab/GitLabAsset.php
@@ -59,7 +59,7 @@ public static function fromRecord(
*/
public function download(?\Closure $progress = null): \Generator
{
- $response = $this->api->downloadArtifact($this->release->getRepository()->getName(), $this->release->getName(), $this->getName());
+ $response = $this->api->downloadArtifact($this->release->getRepository()->getName(), $this->release->getTag(), $this->getName());
yield from StreamReader::chunks($response->getBody(), $progress);
}
diff --git a/src/Module/Repository/Internal/GitLab/GitLabRelease.php b/src/Module/Repository/Internal/GitLab/GitLabRelease.php
index 8760d2c4..e6830e8c 100644
--- a/src/Module/Repository/Internal/GitLab/GitLabRelease.php
+++ b/src/Module/Repository/Internal/GitLab/GitLabRelease.php
@@ -21,25 +21,31 @@ final class GitLabRelease extends Release implements Destroyable
{
/**
* @param non-empty-string $name
+ * @param non-empty-string $tag
*/
private function __construct(
GitLabRepository $repository,
string $name,
+ string $tag,
Version $version,
) {
- parent::__construct($repository, $name, $version);
+ parent::__construct($repository, $name, $tag, $version);
}
/**
+ * @param string $tagPrefix Prefix the tag starts with; it is not part of the version.
* @throws \InvalidArgumentException When the release tag is not a version.
*/
public static function fromRecord(
RepositoryApi $api,
GitLabRepository $repository,
ReleaseRecord $record,
+ string $tagPrefix = '',
): self {
- $version = Version::fromVersionString($record->tag);
- $result = new self($repository, $record->name, $version);
+ $version = \substr($record->tag, \strlen($tagPrefix));
+ $version === '' and throw new \InvalidArgumentException("Release tag `{$record->tag}` has no version.");
+ $version = Version::fromVersionString($version);
+ $result = new self($repository, $record->name, $record->tag, $version);
$result->assets = AssetsCollection::create(static function () use ($api, $result, $record): \Generator {
foreach ($record->assets as $asset) {
diff --git a/src/Module/Repository/Internal/GitLab/GitLabRepository.php b/src/Module/Repository/Internal/GitLab/GitLabRepository.php
index 6410e4a6..58fde09b 100644
--- a/src/Module/Repository/Internal/GitLab/GitLabRepository.php
+++ b/src/Module/Repository/Internal/GitLab/GitLabRepository.php
@@ -42,6 +42,7 @@ public function __construct(
string $projectPath,
private readonly Logger $logger,
private readonly VersionRegistry $registry,
+ private readonly string $tagPrefix = '',
) {
$this->name = $projectPath;
}
@@ -81,8 +82,13 @@ public function getReleases(): ReleasesCollection
$toYield = [];
foreach ($pages->current() ?? [] as $record) {
+ // Releases of other components sharing the repository
+ if (!\str_starts_with($record->tag, $this->tagPrefix)) {
+ continue;
+ }
+
try {
- $toYield[] = GitLabRelease::fromRecord($this->api, $this, $record);
+ $toYield[] = GitLabRelease::fromRecord($this->api, $this, $record, $this->tagPrefix);
} catch (\Throwable) {
// Skip invalid releases
continue;
diff --git a/src/Module/Repository/Internal/Release.php b/src/Module/Repository/Internal/Release.php
index 9e32c7c7..d12d3b33 100644
--- a/src/Module/Repository/Internal/Release.php
+++ b/src/Module/Repository/Internal/Release.php
@@ -20,10 +20,12 @@ abstract class Release implements ReleaseInterface
/**
* @param non-empty-string $name Release name.
+ * @param non-empty-string $tag Tag the release was made from.
*/
public function __construct(
protected Repository $repository,
protected string $name,
+ protected string $tag,
protected Version $version,
iterable $assets = [],
) {
@@ -40,6 +42,11 @@ public function getName(): string
return $this->name;
}
+ public function getTag(): string
+ {
+ return $this->tag;
+ }
+
public function getVersion(): Version
{
return $this->version;
diff --git a/src/Module/Repository/ReleaseInterface.php b/src/Module/Repository/ReleaseInterface.php
index 6193ec1a..7c60ad11 100644
--- a/src/Module/Repository/ReleaseInterface.php
+++ b/src/Module/Repository/ReleaseInterface.php
@@ -36,16 +36,24 @@ interface ReleaseInterface
public function getRepository(): Repository;
/**
- * Returns Composer's compatible "pretty" release version.
+ * Returns the human-readable release name.
*
- * This version is formatted for semantic versioning compatibility.
- *
- * @return non-empty-string Formatted version string (e.g. "1.2.3")
+ * @return non-empty-string
*/
public function getName(): string;
/**
- * Returns the version of this release.
+ * Returns the tag the release was made from, exactly as the repository reports it.
+ *
+ * Unlike the version, it keeps the configured tag prefix: it identifies the release
+ * within the repository and in the version registry.
+ *
+ * @return non-empty-string
+ */
+ public function getTag(): string;
+
+ /**
+ * Returns the version of this release, parsed from the tag without the tag prefix.
*/
public function getVersion(): Version;
diff --git a/tests/Unit/Module/Config/Schema/Embed/RepositoryTest.php b/tests/Unit/Module/Config/Schema/Embed/RepositoryTest.php
new file mode 100644
index 00000000..88975c95
--- /dev/null
+++ b/tests/Unit/Module/Config/Schema/Embed/RepositoryTest.php
@@ -0,0 +1,30 @@
+ 'github', 'uri' => 'oven-sh/bun', 'tag-prefix' => 'bun-']);
+
+ Assert::same($repository->tagPrefix, 'bun-');
+ }
+
+ #[Test]
+ public function tagPrefixIsEmptyByDefault(): void
+ {
+ $repository = Repository::fromArray(['type' => 'github', 'uri' => 'owner/repo']);
+
+ Assert::same($repository->tagPrefix, '');
+ }
+}
diff --git a/tests/Unit/Module/Downloader/AssetSelectionTest.php b/tests/Unit/Module/Downloader/AssetSelectionTest.php
new file mode 100644
index 00000000..c6a62721
--- /dev/null
+++ b/tests/Unit/Module/Downloader/AssetSelectionTest.php
@@ -0,0 +1,210 @@
+ [OperatingSystem::Linux, Architecture::X86_64, 'bun-linux-x64.zip'];
+ yield 'Linux arm64' => [OperatingSystem::Linux, Architecture::ARM_64, 'bun-linux-aarch64.zip'];
+ yield 'macOS x64' => [OperatingSystem::Darwin, Architecture::X86_64, 'bun-darwin-x64.zip'];
+ yield 'macOS arm64' => [OperatingSystem::Darwin, Architecture::ARM_64, 'bun-darwin-aarch64.zip'];
+ yield 'Windows x64' => [OperatingSystem::Windows, Architecture::X86_64, 'bun-windows-x64.zip'];
+ yield 'Windows arm64' => [OperatingSystem::Windows, Architecture::ARM_64, 'bun-windows-aarch64.zip'];
+ yield 'FreeBSD x64' => [OperatingSystem::BSD, Architecture::X86_64, 'bun-freebsd-x64.zip'];
+ yield 'FreeBSD arm64' => [OperatingSystem::BSD, Architecture::ARM_64, 'bun-freebsd-aarch64.zip'];
+ }
+
+ #[DataProvider('provideBunHosts')]
+ #[Test]
+ public function bunRegistryEntrySelectsThePlainBuild(
+ OperatingSystem $os,
+ Architecture $arch,
+ string $expected,
+ ): void {
+ $result = $this->downloadBun(self::registryEntry('bun'), $os, $arch);
+
+ Assert::same($result->file->getFilename(), $expected);
+ }
+
+ /**
+ * The target of variant ranking: the plain build wins without a pattern that spells out
+ * the asset shape of one tool.
+ */
+ #[DataProvider('provideBunHosts')]
+ #[Test]
+ public function bunPlainBuildIsSelectedWithABroadAssetPattern(
+ OperatingSystem $os,
+ Architecture $arch,
+ string $expected,
+ ): void {
+ $software = Software::fromArray([
+ 'name' => 'Bun',
+ 'alias' => 'bun',
+ 'repositories' => [['type' => 'github', 'uri' => 'oven-sh/bun', 'asset-pattern' => '/^bun-.*/']],
+ 'binary' => ['name' => 'bun'],
+ ]);
+
+ $result = $this->downloadBun($software, $os, $arch);
+
+ $result->file->getFilename() === $expected or throw new SkipTest(\sprintf(
+ 'Picks `%s`: asset variants are not ranked yet, see https://github.com/php-internal/dload/issues/134',
+ $result->file->getFilename(),
+ ));
+ Assert::same($result->file->getFilename(), $expected);
+ }
+
+ #[BeforeTest]
+ protected function setUp(): void
+ {
+ $this->tempDir = \sys_get_temp_dir() . '/dload-asset-selection-' . \bin2hex(\random_bytes(6));
+ \mkdir($this->tempDir, recursive: true);
+ }
+
+ #[AfterTest]
+ protected function cleanup(): void
+ {
+ \is_dir($this->tempDir) and FS::removeDir(Path::create($this->tempDir));
+ }
+
+ /**
+ * @param non-empty-string $alias
+ */
+ private static function registryEntry(string $alias): Software
+ {
+ /** @var array{software: list} $registry */
+ $registry = \json_decode(
+ (string) \file_get_contents(\dirname(__DIR__, 4) . '/resources/software.json'),
+ true,
+ flags: \JSON_THROW_ON_ERROR,
+ );
+
+ foreach ($registry['software'] as $entry) {
+ if (($entry['alias'] ?? null) === $alias) {
+ return Software::fromArray($entry);
+ }
+ }
+
+ throw new \LogicException("No `$alias` in the software registry.");
+ }
+
+ private function downloadBun(Software $software, OperatingSystem $os, Architecture $arch): DownloadResult
+ {
+ $repository = new RepositoryStub('oven-sh/bun');
+ $release = new ReleaseStub($repository, 'Bun v1.4.2', Version::fromVersionString('v1.4.2'), tag: 'bun-v1.4.2');
+ $release->setAssets(\array_map(
+ static fn(string $name): AssetStub => new AssetStub(
+ $release,
+ $name,
+ 'https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/' . $name,
+ OperatingSystem::tryFromBuildName($name),
+ Architecture::tryFromBuildName($name),
+ ),
+ self::BUN_ASSETS,
+ ));
+ $repository = new RepositoryStub('oven-sh/bun', ReleasesCollection::create([$release]));
+
+ $config = new DownloaderConfig();
+ $config->tmpDir = $this->tempDir;
+
+ $downloader = new Downloader(
+ config: $config,
+ logger: new Logger(),
+ repositoryProvider: (new RepositoryProvider())->addRepositoryFactory(
+ new SequenceRepositoryFactoryStub([$repository]),
+ ),
+ architecture: $arch,
+ operatingSystem: $os,
+ stability: Stability::Stable,
+ archiveService: new ArchiveFactory(),
+ registry: new RecordingRegistry(),
+ );
+ $task = $downloader->download($software, DownloadConfig::fromSoftwareId('bun'), static fn(): null => null);
+
+ /** @var DownloadResult */
+ return await(($task->handler)());
+ }
+}
diff --git a/tests/Unit/Module/Downloader/DownloaderTest.php b/tests/Unit/Module/Downloader/DownloaderTest.php
index 266f92ce..8c6fa654 100644
--- a/tests/Unit/Module/Downloader/DownloaderTest.php
+++ b/tests/Unit/Module/Downloader/DownloaderTest.php
@@ -73,6 +73,20 @@ public function deletedReleaseIsForgottenAndTheNextOneIsUsed(): void
Assert::same($this->registry->attached, [['rr', 'github:owner/repo']]);
}
+ #[Test]
+ public function deletedReleaseIsForgottenByItsTagWithThePrefix(): void
+ {
+ $repository = new RepositoryStub('owner/repo');
+ $gone = new ReleaseStub($repository, 'Bun v2.0.0', Version::fromVersionString('v2.0.0'), tag: 'bun-v2.0.0');
+ $gone->setAssets([new GoneAssetStub($gone, 'rr-linux-amd64.tar.gz')]);
+ $alive = self::release($repository, 'v1.9.0', assets: true);
+ $repository = new RepositoryStub('owner/repo', ReleasesCollection::create([$gone, $alive]));
+
+ $this->download([$repository]);
+
+ Assert::same($this->registry->forgotten, [['github:owner/repo', 'bun-v2.0.0']]);
+ }
+
#[Test]
public function registryIdComesFromTheRepositoryNotTheConfiguredUri(): void
{
diff --git a/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryTest.php b/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryTest.php
index aa837eb9..e423e12a 100644
--- a/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryTest.php
+++ b/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryTest.php
@@ -207,6 +207,38 @@ public function aReleaseWithAnUnparsableTagIsSkipped(): void
Assert::same($names, ['v1.0.1']);
}
+ #[Test]
+ public function tagPrefixSelectsReleasesAndIsNotPartOfTheVersion(): void
+ {
+ $registry = new ScriptedRegistryStub([[
+ self::record('cli-v2.0.0'),
+ self::record('bun-v1.4.2'),
+ self::record('v1.0.0'),
+ self::record('bun-v1.4.1-canary.1'),
+ ]]);
+ $repository = self::createRepository(new PagedClientStub(pages: 1), $registry, tagPrefix: 'bun-');
+
+ $releases = \iterator_to_array($repository->getReleases(), false);
+
+ Assert::same(
+ \array_map(static fn(ReleaseInterface $release): string => $release->getTag(), $releases),
+ ['bun-v1.4.2', 'bun-v1.4.1-canary.1'],
+ );
+ Assert::same(
+ \array_map(static fn(ReleaseInterface $release): string => $release->getVersion()->string, $releases),
+ ['v1.4.2', 'v1.4.1-canary.1'],
+ );
+ }
+
+ #[Test]
+ public function aTagConsistingOfThePrefixOnlyIsSkipped(): void
+ {
+ $registry = new ScriptedRegistryStub([[self::record('bun-'), self::record('bun-v1.0.1')]]);
+ $repository = self::createRepository(new PagedClientStub(pages: 1), $registry, tagPrefix: 'bun-');
+
+ Assert::same(self::names($repository), ['bun-v1.0.1']);
+ }
+
#[Test]
public function aFailureOfTheFirstPageReachesTheCaller(): void
{
@@ -256,8 +288,9 @@ private static function record(string $tag): ReleaseRecord
private static function createRepository(
PagedClientStub $client,
VersionRegistry $registry = new PassThroughRegistry(),
+ string $tagPrefix = '',
): GitHubRepository {
- return new GitHubRepository(self::api($client), 'owner', 'repo', new Logger(), $registry);
+ return new GitHubRepository(self::api($client), 'owner', 'repo', new Logger(), $registry, $tagPrefix);
}
private static function api(PagedClientStub $client): RepositoryApi
diff --git a/tests/Unit/Module/Repository/Internal/GitLab/GitLabRepositoryTest.php b/tests/Unit/Module/Repository/Internal/GitLab/GitLabRepositoryTest.php
index 8394045e..5f27f681 100644
--- a/tests/Unit/Module/Repository/Internal/GitLab/GitLabRepositoryTest.php
+++ b/tests/Unit/Module/Repository/Internal/GitLab/GitLabRepositoryTest.php
@@ -8,6 +8,7 @@
use Internal\DLoad\Module\HttpClient\Internal\NyholmFactoryImpl;
use Internal\DLoad\Module\Registry\Internal\PassThroughRegistry;
use Internal\DLoad\Module\Registry\Internal\StoredVersionRegistry;
+use Internal\DLoad\Module\Registry\Record\AssetRecord;
use Internal\DLoad\Module\Registry\Record\ReleaseRecord;
use Internal\DLoad\Module\Registry\Record\RepositoryRecord;
use Internal\DLoad\Module\Registry\RepositoryId;
@@ -15,6 +16,7 @@
use Internal\DLoad\Module\Repository\Exception\RateLimitException;
use Internal\DLoad\Module\Repository\Internal\GitLab\Api\Client;
use Internal\DLoad\Module\Repository\Internal\GitLab\Api\RepositoryApi;
+use Internal\DLoad\Module\Repository\Internal\GitLab\GitLabAsset;
use Internal\DLoad\Module\Repository\Internal\GitLab\GitLabReleaseSource;
use Internal\DLoad\Module\Repository\Internal\GitLab\GitLabRepository;
use Internal\DLoad\Module\Repository\ReleaseInterface;
@@ -29,6 +31,7 @@
#[Covers(GitLabRepository::class)]
#[Covers(GitLabReleaseSource::class)]
+#[Covers(GitLabAsset::class)]
final class GitLabRepositoryTest
{
#[Test]
@@ -161,6 +164,59 @@ public function aReleaseWithAnUnparsableTagIsSkipped(): void
Assert::same($names, ['v1.0.1']);
}
+ #[Test]
+ public function tagPrefixSelectsReleasesAndIsNotPartOfTheVersion(): void
+ {
+ $registry = new ScriptedRegistryStub([[
+ self::record('cli-v2.0.0'),
+ self::record('bun-v1.4.2'),
+ self::record('v1.0.0'),
+ self::record('bun-v1.4.1-canary.1'),
+ ]]);
+ $repository = self::createRepository(new PagedClientStub(pages: 1), $registry, tagPrefix: 'bun-');
+
+ $releases = \iterator_to_array($repository->getReleases(), false);
+
+ Assert::same(
+ \array_map(static fn(ReleaseInterface $release): string => $release->getTag(), $releases),
+ ['bun-v1.4.2', 'bun-v1.4.1-canary.1'],
+ );
+ Assert::same(
+ \array_map(static fn(ReleaseInterface $release): string => $release->getVersion()->string, $releases),
+ ['v1.4.2', 'v1.4.1-canary.1'],
+ );
+ }
+
+ #[Test]
+ public function assetsAreDownloadedByTheTagNotTheReleaseName(): void
+ {
+ $client = new PagedClientStub(pages: 1);
+ $registry = new ScriptedRegistryStub([[new ReleaseRecord(
+ 'cli/v1.2.3',
+ 'CLI 1.2.3',
+ assets: [new AssetRecord('cli-linux-amd64.tar.gz', 'https://gitlab.com/group/project/-/releases/cli%2Fv1.2.3/downloads/cli-linux-amd64.tar.gz')],
+ )]]);
+ $repository = self::createRepository($client, $registry, tagPrefix: 'cli/');
+ $asset = $repository->getReleases()->first()?->getAssets()->first();
+ Assert::notNull($asset);
+
+ \iterator_to_array($asset->download(), false);
+
+ Assert::same(
+ \end($client->uris),
+ 'https://gitlab.com/api/v4/projects/group%2Fproject/releases/cli%2Fv1.2.3/downloads/cli-linux-amd64.tar.gz',
+ );
+ }
+
+ #[Test]
+ public function aTagConsistingOfThePrefixOnlyIsSkipped(): void
+ {
+ $registry = new ScriptedRegistryStub([[self::record('bun-'), self::record('bun-v1.0.1')]]);
+ $repository = self::createRepository(new PagedClientStub(pages: 1), $registry, tagPrefix: 'bun-');
+
+ Assert::same(self::names($repository), ['bun-v1.0.1']);
+ }
+
#[Test]
public function aFailureOfTheFirstPageReachesTheCaller(): void
{
@@ -210,6 +266,7 @@ private static function record(string $tag): ReleaseRecord
private static function createRepository(
PagedClientStub $client,
VersionRegistry $registry = new PassThroughRegistry(),
+ string $tagPrefix = '',
): GitLabRepository {
$logger = new Logger();
$httpFactory = new NyholmFactoryImpl($logger);
@@ -219,7 +276,7 @@ private static function createRepository(
'group/project',
);
- return new GitLabRepository($api, 'group/project', $logger, $registry);
+ return new GitLabRepository($api, 'group/project', $logger, $registry, $tagPrefix);
}
private static function registry(InMemoryRegistryStorage $storage): StoredVersionRegistry
diff --git a/tests/Unit/Module/Repository/Internal/GitLab/Stub/PagedClientStub.php b/tests/Unit/Module/Repository/Internal/GitLab/Stub/PagedClientStub.php
index eca2b51f..ca644008 100644
--- a/tests/Unit/Module/Repository/Internal/GitLab/Stub/PagedClientStub.php
+++ b/tests/Unit/Module/Repository/Internal/GitLab/Stub/PagedClientStub.php
@@ -25,6 +25,13 @@ final class PagedClientStub implements ClientInterface
*/
public array $requests = [];
+ /**
+ * Full URI of every received request, in order.
+ *
+ * @var list
+ */
+ public array $uris = [];
+
/**
* @param int<1, max> $pages Number of pages the list is split into when 100 releases are requested per page.
* @param int<1, max> $releasesPerPage Number of releases on every such page.
@@ -38,6 +45,7 @@ public function sendRequest(RequestInterface $request): ResponseInterface
{
$query = $request->getUri()->getQuery();
$this->requests[] = $query;
+ $this->uris[] = (string) $request->getUri();
$page = self::pageOf($query);
$perPage = self::perPageOf($query);
diff --git a/tests/Unit/Module/Repository/Stub/ReleaseStub.php b/tests/Unit/Module/Repository/Stub/ReleaseStub.php
index 09f85b6f..1fa9d336 100644
--- a/tests/Unit/Module/Repository/Stub/ReleaseStub.php
+++ b/tests/Unit/Module/Repository/Stub/ReleaseStub.php
@@ -19,12 +19,14 @@ final class ReleaseStub implements ReleaseInterface
/**
* @param non-empty-string $name Formatted version (e.g. "1.2.3")
* @param array $assets
+ * @param non-empty-string|null $tag Defaults to the name
*/
public function __construct(
private readonly RepositoryStub $repository,
private readonly string $name,
private readonly Version $version,
private array $assets = [],
+ private readonly ?string $tag = null,
) {}
public function getRepository(): Repository
@@ -37,6 +39,11 @@ public function getName(): string
return $this->name;
}
+ public function getTag(): string
+ {
+ return $this->tag ?? $this->name;
+ }
+
public function getVersion(): Version
{
return $this->version;