diff --git a/app/Domains/Package/Contracts/Data/PackageVersionData.php b/app/Domains/Package/Contracts/Data/PackageVersionData.php index baa481a..e17a609 100644 --- a/app/Domains/Package/Contracts/Data/PackageVersionData.php +++ b/app/Domains/Package/Contracts/Data/PackageVersionData.php @@ -199,7 +199,7 @@ protected static function generateTagUrlFromSourceUrl( public function isStable(): bool { - return ! str_contains($this->version, 'dev') && preg_match('/^\d+\.\d+/', $this->version); + return ! str_contains($this->version, 'dev') && preg_match('/^v?\d+\.\d+/', $this->version); } public function isDev(): bool diff --git a/app/Domains/Repository/Contracts/Data/ComposerMetadataData.php b/app/Domains/Repository/Contracts/Data/ComposerMetadataData.php index 26e926b..7167771 100644 --- a/app/Domains/Repository/Contracts/Data/ComposerMetadataData.php +++ b/app/Domains/Repository/Contracts/Data/ComposerMetadataData.php @@ -88,13 +88,10 @@ protected static function validateRequiredFields(array $data): void */ public static function extractVersion(string $ref): string { - // Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0) - if (str_starts_with($ref, 'v') && preg_match('/^v\d/', $ref)) { - return substr($ref, 1); - } - - // Branch names need dev- prefix for Composer compatibility - if (! preg_match('/^\d+\.\d+/', $ref)) { + // Branch names need a dev- prefix for Composer compatibility. Version tags are + // kept verbatim (e.g. v1.0.0 stays v1.0.0) so the pretty version matches the + // original Git tag; Composer derives the normalized form separately. + if (! preg_match('/^v?\d+\.\d+/', $ref)) { return "dev-{$ref}"; } diff --git a/tests/Feature/Actions/FilterChangedRefsActionTest.php b/tests/Feature/Actions/FilterChangedRefsActionTest.php index 448e001..eab43b2 100644 --- a/tests/Feature/Actions/FilterChangedRefsActionTest.php +++ b/tests/Feature/Actions/FilterChangedRefsActionTest.php @@ -66,7 +66,7 @@ function makeRefs(array $tags = [], array $branches = []): RefsCollectionData PackageVersion::factory() ->forPackage($package) ->create([ - 'version' => '1.0.0', + 'version' => 'v1.0.0', 'source_reference' => 'abc123', ]); @@ -100,7 +100,7 @@ function makeRefs(array $tags = [], array $branches = []): RefsCollectionData PackageVersion::factory() ->forPackage($package) ->create([ - 'version' => '1.0.0', + 'version' => 'v1.0.0', 'source_reference' => 'old-sha', ]); @@ -166,7 +166,7 @@ function makeRefs(array $tags = [], array $branches = []): RefsCollectionData PackageVersion::factory() ->forPackage($package) ->create([ - 'version' => '1.0.0', + 'version' => 'v1.0.0', 'source_reference' => 'tag-sha', ]); diff --git a/tests/Feature/Actions/RemoveStaleVersionsActionTest.php b/tests/Feature/Actions/RemoveStaleVersionsActionTest.php index e9254ca..fbc320d 100644 --- a/tests/Feature/Actions/RemoveStaleVersionsActionTest.php +++ b/tests/Feature/Actions/RemoveStaleVersionsActionTest.php @@ -98,8 +98,8 @@ function makeRefsCollection(array $tags = [], array $branches = []): RefsCollect $repository = Repository::factory()->forOrganization($organization)->create(); $package = Package::factory()->forOrganization($organization)->forRepository($repository)->create(); - // Version stored without v prefix (extractVersion strips it) - PackageVersion::factory()->forPackage($package)->create(['version' => '1.0.0', 'normalized_version' => '1.0.0.0']); + // Version stored with the v prefix preserved, matching the original tag + PackageVersion::factory()->forPackage($package)->create(['version' => 'v1.0.0', 'normalized_version' => '1.0.0.0']); $refs = makeRefsCollection( tags: [ @@ -113,6 +113,27 @@ function makeRefsCollection(array $tags = [], array $branches = []): RefsCollect expect($removed)->toBe(0); }); +it('removes legacy versions stored without the v prefix on re-sync', function () { + $organization = Organization::factory()->create(); + $repository = Repository::factory()->forOrganization($organization)->create(); + $package = Package::factory()->forOrganization($organization)->forRepository($repository)->create(); + + // Legacy row stored before the prefix was preserved + PackageVersion::factory()->forPackage($package)->create(['version' => '1.0.0', 'normalized_version' => '1.0.0.0']); + + $refs = makeRefsCollection( + tags: [ + ['name' => 'v1.0.0', 'commit' => 'abc123'], + ] + ); + + $action = app(RemoveStaleVersionsAction::class); + $removed = $action->handle($repository, $refs); + + expect($removed)->toBe(1); + expect(PackageVersion::where('package_uuid', $package->uuid)->count())->toBe(0); +}); + it('returns zero when repository has no packages', function () { $organization = Organization::factory()->create(); $repository = Repository::factory()->forOrganization($organization)->create(); diff --git a/tests/Feature/Composer/ComposerApiTest.php b/tests/Feature/Composer/ComposerApiTest.php index 35690db..54b163c 100644 --- a/tests/Feature/Composer/ComposerApiTest.php +++ b/tests/Feature/Composer/ComposerApiTest.php @@ -77,6 +77,37 @@ function authenticatedGet(string $uri, string $token): TestResponse ->assertJsonPath('packages.acme/awesome-package.0.source.reference', 'abc123'); }); +it('preserves the v prefix in the version field while normalizing separately', function () { + $package = Package::factory() + ->for($this->organization, 'organization') + ->create(['name' => 'acme/awesome-package']); + + PackageVersion::factory() + ->for($package) + ->create([ + 'version' => 'v1.2.0', + 'normalized_version' => '1.2.0.0', + 'composer_json' => [ + 'name' => 'acme/awesome-package', + 'type' => 'library', + ], + 'source_url' => 'https://github.com/acme/awesome-package.git', + 'source_reference' => 'abc123', + ]); + + $response = authenticatedGet("/{$this->organization->slug}/p2/acme/awesome-package.json", $this->plainToken); + + $response->assertOk(); + + $versions = $response->json('packages.acme/awesome-package'); + + // The pretty version keeps the original tag (so {$version} placeholders and + // installed pretty versions match), while the normalized form drives comparison. + expect($versions)->toHaveCount(1) + ->and($versions[0]['version'])->toBe('v1.2.0') + ->and($versions[0]['version_normalized'])->toBe('1.2.0.0'); +}); + it('returns multiple versions ordered by release date', function () { $package = Package::factory() ->for($this->organization, 'organization') diff --git a/tests/Feature/Jobs/SyncRefJobTest.php b/tests/Feature/Jobs/SyncRefJobTest.php index 191185b..673e22b 100644 --- a/tests/Feature/Jobs/SyncRefJobTest.php +++ b/tests/Feature/Jobs/SyncRefJobTest.php @@ -56,7 +56,7 @@ expect(PackageVersion::count())->toBe(1); $version = PackageVersion::first(); - expect($version->version)->toBe('1.0.0'); + expect($version->version)->toBe('v1.0.0'); expect($version->source_reference)->toBe('abc123'); }); diff --git a/tests/Unit/ComposerMetadataParserTest.php b/tests/Unit/ComposerMetadataParserTest.php index cc6daa8..3aea4bd 100644 --- a/tests/Unit/ComposerMetadataParserTest.php +++ b/tests/Unit/ComposerMetadataParserTest.php @@ -17,7 +17,7 @@ expect($result)->toBeInstanceOf(ComposerMetadataData::class) ->and($result->name)->toBe('vendor/package') - ->and($result->version)->toBe('1.0.0') + ->and($result->version)->toBe('v1.0.0') ->and($result->normalizedVersion)->toBe('1.0.0.0') ->and($result->type)->toBe('library') ->and($result->description)->toBe('Test package') @@ -50,7 +50,7 @@ 'simple branch' => ['develop', 'dev-develop'], 'branch with slash' => ['feature/my-feature', 'dev-feature/my-feature'], 'release branch' => ['carconnect-release', 'dev-carconnect-release'], - 'tag version' => ['v1.0.0', '1.0.0'], + 'tag version' => ['v1.0.0', 'v1.0.0'], 'tag without v' => ['1.2.3', '1.2.3'], ]);