Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,17 @@ for _, m := range maintainers {
fmt.Printf("%s <%s>\n", m.Login, m.Email)
}

// Fetch latest non-yanked version
// Fetch the registry-advertised latest version, with an active-release fallback
latest, err := registries.FetchLatestVersionFromPURL(ctx, "pkg:cargo/serde", nil)
fmt.Println(latest.Number) // e.g., "1.0.197"
fmt.Println(latest.PublishedAt)

// Apply the same policy to versions fetched elsewhere
cargoPackage, err := registries.FetchPackageFromPURL(ctx, "pkg:cargo/serde", nil)
cargoRegistry, cargoName, _, err := registries.NewFromPURL("pkg:cargo/serde", nil)
cargoVersions, err := cargoRegistry.FetchVersions(ctx, cargoName)
latest = registries.SelectLatestVersion(cargoVersions, cargoRegistry.Ecosystem(), cargoPackage.LatestVersion)

// Parse a PURL to get the registry client
reg, name, version, err := registries.NewFromPURL("pkg:pypi/requests@2.31.0", nil)
// reg is a Registry for pypi
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ require (
github.com/git-pkgs/pom v0.1.5
github.com/git-pkgs/purl v0.1.15
github.com/git-pkgs/spdx v0.3.0
github.com/git-pkgs/vers v0.3.0
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
github.com/rubyist/circuitbreaker v2.2.1+incompatible
)

require (
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/git-pkgs/vers v0.3.0 // indirect
github.com/github/go-spdx/v2 v2.7.0 // indirect
github.com/package-url/packageurl-go v0.1.6 // indirect
github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea // indirect
Expand Down
97 changes: 63 additions & 34 deletions internal/core/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package core
import (
"context"
"fmt"
"sort"

"github.com/git-pkgs/purl"
"github.com/git-pkgs/vers"
)

const defaultConcurrency = 15
Expand Down Expand Up @@ -107,50 +107,78 @@ func FetchMaintainersFromPURL(ctx context.Context, purlStr string, client *Clien
return reg.FetchMaintainers(ctx, name)
}

// FetchLatestVersion returns the latest non-yanked/retracted/deprecated version.
// Returns nil if no valid versions exist.
// FetchLatestVersion returns the registry-advertised latest version when one
// is available. Otherwise it selects the latest active version by publication
// time, falling back to ecosystem version ordering when timestamps are absent.
// It returns nil when the registry reports no active versions.
Comment thread
andrew marked this conversation as resolved.
// If fetching versions fails after an advertised version is found, it returns
// that version without metadata and suppresses the versions error.
func FetchLatestVersion(ctx context.Context, reg Registry, name string) (*Version, error) {
var advertised string
pkg, err := reg.FetchPackage(ctx, name)
if err == nil && pkg != nil {
advertised = pkg.LatestVersion
}

versions, err := reg.FetchVersions(ctx, name)
if err != nil {
if advertised != "" {
return &Version{Number: advertised}, nil
}
return nil, err
}
return SelectLatestVersion(versions, reg.Ecosystem(), advertised), nil
}

if len(versions) == 0 {
return nil, nil
}

// Filter out yanked/retracted/deprecated versions
var valid []Version
for _, v := range versions {
if v.Status == StatusNone {
valid = append(valid, v)
// SelectLatestVersion applies the shared latest-release policy to versions.
// An advertised version takes precedence, including when it is a prerelease
// or has a non-empty status.
// Without one, versions with a non-empty status are excluded and the newest
// publication time wins. Ecosystem version ordering breaks timestamp ties and
// is used when every active version lacks a timestamp. Prereleases remain
// eligible in that fallback. The input slice is not modified.
func SelectLatestVersion(versions []Version, ecosystem, advertised string) *Version {
if advertised != "" {
for i := range versions {
if vers.CompareWithScheme(versions[i].Number, advertised, ecosystem) == 0 {
selected := versions[i]
return &selected
}
}
return &Version{Number: advertised}
}

if len(valid) == 0 {
return nil, nil
}

// Sort by PublishedAt descending (newest first)
// If PublishedAt is zero, fall back to assuming the list order is correct
hasTimestamps := false
for _, v := range valid {
if !v.PublishedAt.IsZero() {
hasTimestamps = true
break
var latest *Version
hasTimestamp := false
for i := range versions {
candidate := &versions[i]
if candidate.Number == "" || candidate.Status != StatusNone {
continue
}
if !candidate.PublishedAt.IsZero() {
if !hasTimestamp || latest == nil || candidate.PublishedAt.After(latest.PublishedAt) ||
(candidate.PublishedAt.Equal(latest.PublishedAt) &&
vers.CompareWithScheme(candidate.Number, latest.Number, ecosystem) > 0) {
selected := *candidate
latest = &selected
}
hasTimestamp = true
continue
}
if hasTimestamp {
continue
}
if latest == nil || vers.CompareWithScheme(candidate.Number, latest.Number, ecosystem) > 0 {
selected := *candidate
latest = &selected
}
}

if hasTimestamps {
sort.Slice(valid, func(i, j int) bool {
return valid[i].PublishedAt.After(valid[j].PublishedAt)
})
}

return &valid[0], nil
return latest
}

// FetchLatestVersionFromPURL returns the latest non-yanked version for a PURL.
// FetchLatestVersionFromPURL returns the latest version for a PURL using the
// shared latest-release policy.
func FetchLatestVersionFromPURL(ctx context.Context, purl string, client *Client) (*Version, error) {
reg, name, _, err := NewFromPURL(purl, client)
if err != nil {
Expand Down Expand Up @@ -188,13 +216,14 @@ func BulkFetchVersionsWithConcurrency(ctx context.Context, purls []string, clien
})
}

// BulkFetchLatestVersions fetches the latest version for multiple PURLs in parallel.
// Returns a map of PURL to the latest non-yanked Version.
// BulkFetchLatestVersions fetches the latest version for multiple PURLs in
// parallel using the shared latest-release policy.
func BulkFetchLatestVersions(ctx context.Context, purls []string, client *Client) map[string]*Version {
return BulkFetchLatestVersionsWithConcurrency(ctx, purls, client, defaultConcurrency)
}

// BulkFetchLatestVersionsWithConcurrency fetches latest versions with a custom concurrency limit.
// BulkFetchLatestVersionsWithConcurrency fetches latest versions using the
// shared policy and a custom concurrency limit.
func BulkFetchLatestVersionsWithConcurrency(ctx context.Context, purls []string, client *Client, concurrency int) map[string]*Version {
return ParallelMap(ctx, purls, concurrency, func(ctx context.Context, p string) (*Version, error) {
return FetchLatestVersionFromPURL(ctx, p, client)
Expand Down
221 changes: 221 additions & 0 deletions internal/core/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package core

import (
"context"
"errors"
"reflect"
"testing"
"time"
)

func TestSelectLatestVersion(t *testing.T) {
older := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
newer := older.Add(24 * time.Hour)

tests := []struct {
name string
versions []Version
ecosystem string
advertised string
want string
}{
{
name: "advertised version wins",
versions: []Version{
{Number: "1.0.0", PublishedAt: older},
{Number: "2.0.0-beta.1", PublishedAt: newer},
},
ecosystem: "npm",
advertised: "1.0.0",
want: "1.0.0",
},
{
name: "advertised scheme equivalent version returns metadata",
versions: []Version{
{Number: "1.0.0", Licenses: "MIT"},
},
ecosystem: "pypi",
advertised: "1.0",
want: "1.0.0",
},
{
name: "advertised version wins regardless of status",
versions: []Version{
{Number: "1.0.0", Status: StatusDeprecated},
{Number: "2.0.0"},
},
ecosystem: "npm",
advertised: "1.0.0",
want: "1.0.0",
},
{
name: "advertised version missing from list",
versions: []Version{{Number: "1.0.0"}},
ecosystem: "npm",
advertised: "2.0.0",
want: "2.0.0",
},
{
name: "newest publication wins",
versions: []Version{
{Number: "2.0.0", PublishedAt: older},
{Number: "1.9.0", PublishedAt: newer},
},
ecosystem: "npm",
want: "1.9.0",
},
{
name: "version ordering breaks timestamp ties",
versions: []Version{
{Number: "1.9.0", PublishedAt: newer},
{Number: "2.0.0", PublishedAt: newer},
},
ecosystem: "npm",
want: "2.0.0",
},
{
name: "ecosystem ordering without timestamps",
versions: []Version{
{Number: "1.9.0"},
{Number: "1.10.0"},
},
ecosystem: "gem",
want: "1.10.0",
},
{
name: "dated versions take precedence over undated versions",
versions: []Version{
{Number: "2.0.0"},
{Number: "1.0.0", PublishedAt: older},
},
ecosystem: "npm",
want: "1.0.0",
},
{
name: "prerelease is eligible without advertised version",
versions: []Version{
{Number: "1.9.0"},
{Number: "2.0.0-beta.1"},
},
ecosystem: "npm",
want: "2.0.0-beta.1",
},
{
name: "inactive and empty versions are excluded",
versions: []Version{
{Number: "", PublishedAt: newer},
{Number: "4.0.0", Status: StatusYanked},
{Number: "3.0.0", Status: StatusDeprecated},
{Number: "2.0.0", Status: StatusRetracted},
{Number: "1.0.0"},
},
ecosystem: "npm",
want: "1.0.0",
},
{
name: "no active versions",
versions: []Version{
{Number: "2.0.0", Status: StatusYanked},
{Number: "1.0.0", Status: StatusRetracted},
},
ecosystem: "npm",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
before := append([]Version(nil), test.versions...)
got := SelectLatestVersion(test.versions, test.ecosystem, test.advertised)
if test.want == "" {
if got != nil {
t.Fatalf("SelectLatestVersion() = %+v, want nil", got)
}
} else if got == nil || got.Number != test.want {
t.Fatalf("SelectLatestVersion() = %+v, want number %q", got, test.want)
}
if !reflect.DeepEqual(test.versions, before) {
t.Errorf("SelectLatestVersion() modified its input")
}
})
}
}

type latestRegistry struct {
ecosystem string
pkg *Package
packageErr error
versions []Version
versionErr error
}

func (r *latestRegistry) Ecosystem() string { return r.ecosystem }

func (r *latestRegistry) FetchPackage(context.Context, string) (*Package, error) {
return r.pkg, r.packageErr
}

func (r *latestRegistry) FetchVersions(context.Context, string) ([]Version, error) {
return r.versions, r.versionErr
}

func (r *latestRegistry) FetchDependencies(context.Context, string, string) ([]Dependency, error) {
return nil, nil
}

func (r *latestRegistry) FetchMaintainers(context.Context, string) ([]Maintainer, error) {
return nil, nil
}

func (r *latestRegistry) URLs() URLBuilder { //nolint:ireturn // Registry requires this interface return type.
return &BaseURLs{}
}

func TestFetchLatestVersion(t *testing.T) {
t.Run("uses advertised latest", func(t *testing.T) {
reg := &latestRegistry{
ecosystem: "npm",
pkg: &Package{LatestVersion: "1.0.0"},
versions: []Version{
{Number: "1.0.0", Licenses: "MIT"},
{Number: "2.0.0-beta.1"},
},
}
got, err := FetchLatestVersion(context.Background(), reg, "example")
if err != nil {
t.Fatalf("FetchLatestVersion: %v", err)
}
if got == nil || got.Number != "1.0.0" || got.Licenses != "MIT" {
t.Errorf("FetchLatestVersion() = %+v, want advertised version metadata", got)
}
})

t.Run("falls back when package metadata fails", func(t *testing.T) {
reg := &latestRegistry{
ecosystem: "npm",
packageErr: errors.New("package endpoint unavailable"),
versions: []Version{{Number: "1.0.0"}, {Number: "2.0.0"}},
}
got, err := FetchLatestVersion(context.Background(), reg, "example")
if err != nil {
t.Fatalf("FetchLatestVersion: %v", err)
}
if got == nil || got.Number != "2.0.0" {
t.Errorf("FetchLatestVersion() = %+v, want version-order fallback", got)
}
})

t.Run("uses advertised version when versions fail", func(t *testing.T) {
reg := &latestRegistry{
ecosystem: "npm",
pkg: &Package{LatestVersion: "1.0.0"},
versionErr: errors.New("versions endpoint unavailable"),
}
got, err := FetchLatestVersion(context.Background(), reg, "example")
if err != nil {
t.Fatalf("FetchLatestVersion: %v", err)
}
if got == nil || got.Number != "1.0.0" {
t.Errorf("FetchLatestVersion() = %+v, want advertised version", got)
}
})
}
Loading