Skip to content

Commit f9dedb9

Browse files
authored
fix(composer): preserve v prefix on package versions (#159)
1 parent 552eca6 commit f9dedb9

7 files changed

Lines changed: 65 additions & 16 deletions

File tree

app/Domains/Package/Contracts/Data/PackageVersionData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ protected static function generateTagUrlFromSourceUrl(
199199

200200
public function isStable(): bool
201201
{
202-
return ! str_contains($this->version, 'dev') && preg_match('/^\d+\.\d+/', $this->version);
202+
return ! str_contains($this->version, 'dev') && preg_match('/^v?\d+\.\d+/', $this->version);
203203
}
204204

205205
public function isDev(): bool

app/Domains/Repository/Contracts/Data/ComposerMetadataData.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,10 @@ protected static function validateRequiredFields(array $data): void
8888
*/
8989
public static function extractVersion(string $ref): string
9090
{
91-
// Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0)
92-
if (str_starts_with($ref, 'v') && preg_match('/^v\d/', $ref)) {
93-
return substr($ref, 1);
94-
}
95-
96-
// Branch names need dev- prefix for Composer compatibility
97-
if (! preg_match('/^\d+\.\d+/', $ref)) {
91+
// Branch names need a dev- prefix for Composer compatibility. Version tags are
92+
// kept verbatim (e.g. v1.0.0 stays v1.0.0) so the pretty version matches the
93+
// original Git tag; Composer derives the normalized form separately.
94+
if (! preg_match('/^v?\d+\.\d+/', $ref)) {
9895
return "dev-{$ref}";
9996
}
10097

tests/Feature/Actions/FilterChangedRefsActionTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function makeRefs(array $tags = [], array $branches = []): RefsCollectionData
6666
PackageVersion::factory()
6767
->forPackage($package)
6868
->create([
69-
'version' => '1.0.0',
69+
'version' => 'v1.0.0',
7070
'source_reference' => 'abc123',
7171
]);
7272

@@ -100,7 +100,7 @@ function makeRefs(array $tags = [], array $branches = []): RefsCollectionData
100100
PackageVersion::factory()
101101
->forPackage($package)
102102
->create([
103-
'version' => '1.0.0',
103+
'version' => 'v1.0.0',
104104
'source_reference' => 'old-sha',
105105
]);
106106

@@ -166,7 +166,7 @@ function makeRefs(array $tags = [], array $branches = []): RefsCollectionData
166166
PackageVersion::factory()
167167
->forPackage($package)
168168
->create([
169-
'version' => '1.0.0',
169+
'version' => 'v1.0.0',
170170
'source_reference' => 'tag-sha',
171171
]);
172172

tests/Feature/Actions/RemoveStaleVersionsActionTest.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ function makeRefsCollection(array $tags = [], array $branches = []): RefsCollect
9898
$repository = Repository::factory()->forOrganization($organization)->create();
9999
$package = Package::factory()->forOrganization($organization)->forRepository($repository)->create();
100100

101-
// Version stored without v prefix (extractVersion strips it)
102-
PackageVersion::factory()->forPackage($package)->create(['version' => '1.0.0', 'normalized_version' => '1.0.0.0']);
101+
// Version stored with the v prefix preserved, matching the original tag
102+
PackageVersion::factory()->forPackage($package)->create(['version' => 'v1.0.0', 'normalized_version' => '1.0.0.0']);
103103

104104
$refs = makeRefsCollection(
105105
tags: [
@@ -113,6 +113,27 @@ function makeRefsCollection(array $tags = [], array $branches = []): RefsCollect
113113
expect($removed)->toBe(0);
114114
});
115115

116+
it('removes legacy versions stored without the v prefix on re-sync', function () {
117+
$organization = Organization::factory()->create();
118+
$repository = Repository::factory()->forOrganization($organization)->create();
119+
$package = Package::factory()->forOrganization($organization)->forRepository($repository)->create();
120+
121+
// Legacy row stored before the prefix was preserved
122+
PackageVersion::factory()->forPackage($package)->create(['version' => '1.0.0', 'normalized_version' => '1.0.0.0']);
123+
124+
$refs = makeRefsCollection(
125+
tags: [
126+
['name' => 'v1.0.0', 'commit' => 'abc123'],
127+
]
128+
);
129+
130+
$action = app(RemoveStaleVersionsAction::class);
131+
$removed = $action->handle($repository, $refs);
132+
133+
expect($removed)->toBe(1);
134+
expect(PackageVersion::where('package_uuid', $package->uuid)->count())->toBe(0);
135+
});
136+
116137
it('returns zero when repository has no packages', function () {
117138
$organization = Organization::factory()->create();
118139
$repository = Repository::factory()->forOrganization($organization)->create();

tests/Feature/Composer/ComposerApiTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,37 @@ function authenticatedGet(string $uri, string $token): TestResponse
7777
->assertJsonPath('packages.acme/awesome-package.0.source.reference', 'abc123');
7878
});
7979

80+
it('preserves the v prefix in the version field while normalizing separately', function () {
81+
$package = Package::factory()
82+
->for($this->organization, 'organization')
83+
->create(['name' => 'acme/awesome-package']);
84+
85+
PackageVersion::factory()
86+
->for($package)
87+
->create([
88+
'version' => 'v1.2.0',
89+
'normalized_version' => '1.2.0.0',
90+
'composer_json' => [
91+
'name' => 'acme/awesome-package',
92+
'type' => 'library',
93+
],
94+
'source_url' => 'https://github.com/acme/awesome-package.git',
95+
'source_reference' => 'abc123',
96+
]);
97+
98+
$response = authenticatedGet("/{$this->organization->slug}/p2/acme/awesome-package.json", $this->plainToken);
99+
100+
$response->assertOk();
101+
102+
$versions = $response->json('packages.acme/awesome-package');
103+
104+
// The pretty version keeps the original tag (so {$version} placeholders and
105+
// installed pretty versions match), while the normalized form drives comparison.
106+
expect($versions)->toHaveCount(1)
107+
->and($versions[0]['version'])->toBe('v1.2.0')
108+
->and($versions[0]['version_normalized'])->toBe('1.2.0.0');
109+
});
110+
80111
it('returns multiple versions ordered by release date', function () {
81112
$package = Package::factory()
82113
->for($this->organization, 'organization')

tests/Feature/Jobs/SyncRefJobTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
expect(PackageVersion::count())->toBe(1);
5757

5858
$version = PackageVersion::first();
59-
expect($version->version)->toBe('1.0.0');
59+
expect($version->version)->toBe('v1.0.0');
6060
expect($version->source_reference)->toBe('abc123');
6161
});
6262

tests/Unit/ComposerMetadataParserTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
expect($result)->toBeInstanceOf(ComposerMetadataData::class)
1919
->and($result->name)->toBe('vendor/package')
20-
->and($result->version)->toBe('1.0.0')
20+
->and($result->version)->toBe('v1.0.0')
2121
->and($result->normalizedVersion)->toBe('1.0.0.0')
2222
->and($result->type)->toBe('library')
2323
->and($result->description)->toBe('Test package')
@@ -50,7 +50,7 @@
5050
'simple branch' => ['develop', 'dev-develop'],
5151
'branch with slash' => ['feature/my-feature', 'dev-feature/my-feature'],
5252
'release branch' => ['carconnect-release', 'dev-carconnect-release'],
53-
'tag version' => ['v1.0.0', '1.0.0'],
53+
'tag version' => ['v1.0.0', 'v1.0.0'],
5454
'tag without v' => ['1.2.3', '1.2.3'],
5555
]);
5656

0 commit comments

Comments
 (0)