From 78d83cea4f44c43561a2bcfaed93c20db07a9d6a Mon Sep 17 00:00:00 2001 From: Maarten Bode Date: Mon, 30 Mar 2026 19:43:07 +0200 Subject: [PATCH 1/2] feat: add tag/version links to package versions --- .../Contracts/Data/PackageVersionData.php | 70 +++++++++++++++++++ .../Data/PackageVersionDetailData.php | 4 ++ .../Repository/Actions/SyncRefAction.php | 2 + app/Models/PackageVersion.php | 1 + database/factories/PackageVersionFactory.php | 2 + ...001_add_source_tag_to_package_versions.php | 22 ++++++ .../js/pages/organizations/packages/show.tsx | 36 ++++++++++ resources/types/generated.d.ts | 4 ++ 8 files changed, 141 insertions(+) create mode 100644 database/migrations/2026_03_30_000001_add_source_tag_to_package_versions.php diff --git a/app/Domains/Package/Contracts/Data/PackageVersionData.php b/app/Domains/Package/Contracts/Data/PackageVersionData.php index d0ee020..baa481a 100644 --- a/app/Domains/Package/Contracts/Data/PackageVersionData.php +++ b/app/Domains/Package/Contracts/Data/PackageVersionData.php @@ -19,7 +19,9 @@ public function __construct( public ?CarbonInterface $releasedAt, public ?string $sourceUrl, public ?string $sourceReference, + public ?string $sourceTag, public ?string $commitUrl, + public ?string $tagUrl, public ?int $distSize, public int $vulnerabilityCount = 0, public ?AdvisorySeverity $highestSeverity = null, @@ -56,6 +58,21 @@ public static function fromModel( $highestSeverity = $highestWeight ? AdvisorySeverity::fromWeight($highestWeight) : null; } + $tagUrl = null; + + if ($version->source_tag && $version->source_url) { + $versionProvider ??= $version->package->repository?->provider; + $versionRepoIdentifier ??= $version->package->repository?->repo_identifier; + + $tagUrl = static::generateTagUrl( + sourceUrl: $version->source_url, + sourceTag: $version->source_tag, + version: $version->version, + provider: $versionProvider, + repoIdentifier: $versionRepoIdentifier, + ); + } + return new self( uuid: $version->uuid, version: $version->version, @@ -63,7 +80,9 @@ public static function fromModel( releasedAt: $version->released_at, sourceUrl: $version->source_url, sourceReference: $version->source_reference, + sourceTag: $version->source_tag, commitUrl: $commitUrl, + tagUrl: $tagUrl, distSize: $version->dist_size, vulnerabilityCount: $vulnerabilityCount, highestSeverity: $highestSeverity, @@ -127,6 +146,57 @@ protected static function generateCommitUrlFromSourceUrl( return null; } + protected static function generateTagUrl( + ?string $sourceUrl, + string $sourceTag, + string $version, + ?GitProvider $provider, + ?string $repoIdentifier + ): ?string { + // Dev branches should not get tag URLs + if (str_starts_with($version, 'dev-') || str_ends_with($version, '-dev')) { + return null; + } + + if ($provider && $repoIdentifier) { + return match ($provider) { + GitProvider::GitHub => "https://github.com/{$repoIdentifier}/releases/tag/{$sourceTag}", + GitProvider::GitLab => "https://gitlab.com/{$repoIdentifier}/-/tags/{$sourceTag}", + GitProvider::Bitbucket => "https://bitbucket.org/{$repoIdentifier}/src/{$sourceTag}", + GitProvider::Git => static::generateTagUrlFromSourceUrl($sourceUrl, $sourceTag), + }; + } + + return static::generateTagUrlFromSourceUrl($sourceUrl, $sourceTag); + } + + protected static function generateTagUrlFromSourceUrl( + ?string $sourceUrl, + string $sourceTag + ): ?string { + if (! $sourceUrl) { + return null; + } + + $cleanUrl = preg_replace('/^git@/', '', $sourceUrl) ?? ''; + $cleanUrl = preg_replace('/\.git$/', '', $cleanUrl) ?? ''; + $cleanUrl = str_replace(':', '/', $cleanUrl); + + if (str_starts_with($cleanUrl, 'github.com/')) { + return "https://{$cleanUrl}/releases/tag/{$sourceTag}"; + } + + if (str_starts_with($cleanUrl, 'gitlab.com/')) { + return "https://{$cleanUrl}/-/tags/{$sourceTag}"; + } + + if (str_starts_with($cleanUrl, 'bitbucket.org/')) { + return "https://{$cleanUrl}/src/{$sourceTag}"; + } + + return null; + } + public function isStable(): bool { return ! str_contains($this->version, 'dev') && preg_match('/^\d+\.\d+/', $this->version); diff --git a/app/Domains/Package/Contracts/Data/PackageVersionDetailData.php b/app/Domains/Package/Contracts/Data/PackageVersionDetailData.php index 5e5ae76..b134903 100644 --- a/app/Domains/Package/Contracts/Data/PackageVersionDetailData.php +++ b/app/Domains/Package/Contracts/Data/PackageVersionDetailData.php @@ -26,7 +26,9 @@ public function __construct( public ?CarbonInterface $releasedAt, public ?string $sourceUrl, public ?string $sourceReference, + public ?string $sourceTag, public ?string $commitUrl, + public ?string $tagUrl, public ?string $description, public ?string $type, public ?string $license, @@ -70,7 +72,9 @@ public static function fromModel( releasedAt: $base->releasedAt, sourceUrl: $base->sourceUrl, sourceReference: $base->sourceReference, + sourceTag: $base->sourceTag, commitUrl: $base->commitUrl, + tagUrl: $base->tagUrl, description: $composerJson['description'] ?? null, type: $composerJson['type'] ?? null, license: $license, diff --git a/app/Domains/Repository/Actions/SyncRefAction.php b/app/Domains/Repository/Actions/SyncRefAction.php index 6df021b..218143a 100644 --- a/app/Domains/Repository/Actions/SyncRefAction.php +++ b/app/Domains/Repository/Actions/SyncRefAction.php @@ -76,6 +76,7 @@ public function handle( 'composer_json' => $metadata->composerJson, 'source_url' => $sourceUrl, 'source_reference' => $ref->commit, + 'source_tag' => $ref->name, ]); return ['status' => 'updated', 'version' => $version, 'package' => $package]; @@ -88,6 +89,7 @@ public function handle( 'composer_json' => $metadata->composerJson, 'source_url' => $sourceUrl, 'source_reference' => $ref->commit, + 'source_tag' => $ref->name, 'released_at' => now(), ]); diff --git a/app/Models/PackageVersion.php b/app/Models/PackageVersion.php index 311d5de..794b8af 100644 --- a/app/Models/PackageVersion.php +++ b/app/Models/PackageVersion.php @@ -23,6 +23,7 @@ * @property array $composer_json * @property string|null $source_url * @property string|null $source_reference + * @property string|null $source_tag * @property string|null $dist_url * @property string|null $dist_shasum * @property string|null $dist_path diff --git a/database/factories/PackageVersionFactory.php b/database/factories/PackageVersionFactory.php index 0d6606c..9602e56 100644 --- a/database/factories/PackageVersionFactory.php +++ b/database/factories/PackageVersionFactory.php @@ -38,6 +38,7 @@ public function definition(): array 'composer_json' => $this->generateComposerJson($version), 'source_url' => fake()->url(), 'source_reference' => fake()->sha1(), + 'source_tag' => "v{$version}", 'dist_url' => fake()->url().'/archive/'.$version.'.zip', 'released_at' => fake()->dateTimeBetween('-2 years', 'now'), ]; @@ -94,6 +95,7 @@ public function devBranch(string $branch = 'main'): static return [ 'version' => $version, 'normalized_version' => '9999999-dev', + 'source_tag' => null, 'composer_json' => array_merge($attributes['composer_json'] ?? [], [ 'version' => $version, ]), diff --git a/database/migrations/2026_03_30_000001_add_source_tag_to_package_versions.php b/database/migrations/2026_03_30_000001_add_source_tag_to_package_versions.php new file mode 100644 index 0000000..f1cb0ed --- /dev/null +++ b/database/migrations/2026_03_30_000001_add_source_tag_to_package_versions.php @@ -0,0 +1,22 @@ +string('source_tag')->nullable()->after('source_reference'); + }); + } + + public function down(): void + { + Schema::table('package_versions', function (Blueprint $table) { + $table->dropColumn('source_tag'); + }); + } +}; diff --git a/resources/js/pages/organizations/packages/show.tsx b/resources/js/pages/organizations/packages/show.tsx index 2f6797f..4de1c69 100644 --- a/resources/js/pages/organizations/packages/show.tsx +++ b/resources/js/pages/organizations/packages/show.tsx @@ -62,6 +62,7 @@ import { Package as PackageIcon, Search, ShieldAlert, + Tag, Terminal, Trash2, Users, @@ -564,6 +565,22 @@ export default function PackageShow({ )} )} + {version.tagUrl && ( + + e.stopPropagation() + } + className="flex items-center gap-1 text-primary hover:underline" + > + + {version.sourceTag} + + )} )} + {activeVersion.tagUrl && ( +
+
+ + Tag +
+ + {activeVersion.sourceTag} + + +
+ )} {/* Install command */} diff --git a/resources/types/generated.d.ts b/resources/types/generated.d.ts index 0e8e764..68e69dc 100644 --- a/resources/types/generated.d.ts +++ b/resources/types/generated.d.ts @@ -162,7 +162,9 @@ normalizedVersion: string; releasedAt: string | null; sourceUrl: string | null; sourceReference: string | null; +sourceTag: string | null; commitUrl: string | null; +tagUrl: string | null; distSize: number | null; vulnerabilityCount: number; highestSeverity: App.Domains.Security.Contracts.Enums.AdvisorySeverity | null; @@ -174,7 +176,9 @@ normalizedVersion: string; releasedAt: string | null; sourceUrl: string | null; sourceReference: string | null; +sourceTag: string | null; commitUrl: string | null; +tagUrl: string | null; description: string | null; type: string | null; license: string | null; From 69a6ab80bbea09a452cef9503c3f1d20b90f07b6 Mon Sep 17 00:00:00 2001 From: Maarten Bode Date: Mon, 30 Mar 2026 19:47:37 +0200 Subject: [PATCH 2/2] fix(ui): use subtle styling for tag links --- resources/js/pages/organizations/packages/show.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/pages/organizations/packages/show.tsx b/resources/js/pages/organizations/packages/show.tsx index 4de1c69..99cd388 100644 --- a/resources/js/pages/organizations/packages/show.tsx +++ b/resources/js/pages/organizations/packages/show.tsx @@ -575,7 +575,7 @@ export default function PackageShow({ onClick={(e) => e.stopPropagation() } - className="flex items-center gap-1 text-primary hover:underline" + className="flex items-center gap-1 hover:text-foreground hover:underline" > {version.sourceTag} @@ -762,7 +762,7 @@ export default function PackageShow({ } target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-1.5 font-medium text-primary transition-colors hover:underline" + className="inline-flex items-center gap-1.5 font-medium text-muted-foreground transition-colors hover:text-foreground hover:underline" > {activeVersion.sourceTag}