From 43cd37a62b6c22db7b899c62763a085134bf2197 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Mon, 17 Aug 2026 10:30:41 +0100 Subject: [PATCH 1/2] Centralize latest version selection --- README.md | 7 +- go.mod | 2 +- internal/core/helpers.go | 95 +++++++++------ internal/core/helpers_test.go | 221 ++++++++++++++++++++++++++++++++++ registries.go | 20 ++- 5 files changed, 304 insertions(+), 41 deletions(-) create mode 100644 internal/core/helpers_test.go diff --git a/README.md b/README.md index 33aa733..14d927b 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,16 @@ 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) +versions, err := registries.FetchVersionsFromPURL(ctx, "pkg:cargo/serde", nil) +latest = registries.SelectLatestVersion(versions, "cargo", 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 diff --git a/go.mod b/go.mod index e6c18ef..6e91a70 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/core/helpers.go b/internal/core/helpers.go index 892c450..0fd9cbe 100644 --- a/internal/core/helpers.go +++ b/internal/core/helpers.go @@ -3,9 +3,9 @@ package core import ( "context" "fmt" - "sort" "github.com/git-pkgs/purl" + "github.com/git-pkgs/vers" ) const defaultConcurrency = 15 @@ -107,50 +107,76 @@ 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. 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 { @@ -188,13 +214,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) diff --git a/internal/core/helpers_test.go b/internal/core/helpers_test.go new file mode 100644 index 0000000..b3869c5 --- /dev/null +++ b/internal/core/helpers_test.go @@ -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) + } + }) +} diff --git a/registries.go b/registries.go index fffde96..dbd678e 100644 --- a/registries.go +++ b/registries.go @@ -188,13 +188,23 @@ func FetchMaintainersFromPURL(ctx context.Context, purl string, c *Client) ([]Ma return core.FetchMaintainersFromPURL(ctx, purl, c) } -// FetchLatestVersion returns the latest non-yanked/retracted/deprecated version. -// Returns nil if no valid versions exist. +// FetchLatestVersion returns the registry-advertised latest version when +// 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. func FetchLatestVersion(ctx context.Context, reg Registry, name string) (*Version, error) { return core.FetchLatestVersion(ctx, reg, name) } -// FetchLatestVersionFromPURL returns the latest non-yanked version for a PURL. +// SelectLatestVersion applies the shared latest-release policy to versions. +// An advertised version takes precedence regardless of status; otherwise see +// FetchLatestVersion for the selection order. The input slice is not modified. +func SelectLatestVersion(versions []Version, ecosystem, advertised string) *Version { + return core.SelectLatestVersion(versions, ecosystem, advertised) +} + +// FetchLatestVersionFromPURL returns the latest version for a PURL using the +// shared latest-release policy. func FetchLatestVersionFromPURL(ctx context.Context, purl string, c *Client) (*Version, error) { return core.FetchLatestVersionFromPURL(ctx, purl, c) } @@ -224,8 +234,8 @@ func BulkFetchVersionsWithConcurrency(ctx context.Context, purls []string, c *Cl return core.BulkFetchVersionsWithConcurrency(ctx, purls, c, concurrency) } -// 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, c *Client) map[string]*Version { return core.BulkFetchLatestVersions(ctx, purls, c) } From 2afecf5068fb919a378db407e965ae2a929fa2c5 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Mon, 17 Aug 2026 10:48:09 +0100 Subject: [PATCH 2/2] Fix latest version documentation --- README.md | 5 +++-- internal/core/helpers.go | 2 ++ registries.go | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 14d927b..6e69398 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,9 @@ fmt.Println(latest.PublishedAt) // Apply the same policy to versions fetched elsewhere cargoPackage, err := registries.FetchPackageFromPURL(ctx, "pkg:cargo/serde", nil) -versions, err := registries.FetchVersionsFromPURL(ctx, "pkg:cargo/serde", nil) -latest = registries.SelectLatestVersion(versions, "cargo", cargoPackage.LatestVersion) +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) diff --git a/internal/core/helpers.go b/internal/core/helpers.go index 0fd9cbe..b6e77f3 100644 --- a/internal/core/helpers.go +++ b/internal/core/helpers.go @@ -111,6 +111,8 @@ func FetchMaintainersFromPURL(ctx context.Context, purlStr string, client *Clien // 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. +// 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) diff --git a/registries.go b/registries.go index dbd678e..1f30296 100644 --- a/registries.go +++ b/registries.go @@ -192,6 +192,8 @@ func FetchMaintainersFromPURL(ctx context.Context, purl string, c *Client) ([]Ma // 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. +// 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) { return core.FetchLatestVersion(ctx, reg, name) }