diff --git a/internal/helm/repository.go b/internal/helm/repository.go index 2e555369..762a4cb5 100644 --- a/internal/helm/repository.go +++ b/internal/helm/repository.go @@ -98,11 +98,7 @@ func (r *ChartRepository) Get(name, version string) (*repo.ChartVersion, error) if err != nil { continue } - // NB: given the entries are already sorted in LoadIndex, - // there is a high probability the first match would be - // the right match to return. However, due to the fact that - // we use a different semver package than Helm does, we still - // need to sort it by our own rules. + if match != nil && !match(v) { continue } @@ -112,7 +108,25 @@ func (r *ChartRepository) Get(name, version string) (*repo.ChartVersion, error) if len(filteredVersions) == 0 { return nil, fmt.Errorf("no chart version found for %s-%s", name, version) } - sort.Sort(sort.Reverse(filteredVersions)) + + // Sort versions + sort.SliceStable(filteredVersions, func(i, j int) bool { + // Reverse + return !(func() bool { + left := filteredVersions[i] + right := filteredVersions[j] + + if !left.EQ(right) { + return left.LT(right) + } + + // Having chart creation timestamp at our disposal, we put package with the + // same version into a chronological order. This is especially important for + // versions that differ only by build metadata, because it is not considered + // a part of the comparable version in Semver + return lookup[left.String()].Created.Before(lookup[right.String()].Created) + })() + }) latest := filteredVersions[0] if latestStable { diff --git a/internal/helm/repository_test.go b/internal/helm/repository_test.go index 884a3732..469186ad 100644 --- a/internal/helm/repository_test.go +++ b/internal/helm/repository_test.go @@ -23,6 +23,7 @@ import ( "reflect" "strings" "testing" + "time" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/getter" @@ -104,6 +105,11 @@ func TestChartRepository_Get(t *testing.T) { i.Add(&chart.Metadata{Name: "chart", Version: "exact"}, "chart-exact.tgz", "http://example.com/charts", "sha256:1234567890") i.Add(&chart.Metadata{Name: "chart", Version: "0.1.0"}, "chart-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc") i.Add(&chart.Metadata{Name: "chart", Version: "0.1.1"}, "chart-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc") + i.Add(&chart.Metadata{Name: "chart", Version: "0.1.5+b.min.minute"}, "chart-0.1.5+b.min.minute.tgz", "http://example.com/charts", "sha256:1234567890abc") + i.Entries["chart"][len(i.Entries["chart"])-1].Created = time.Now().Add(-time.Minute) + i.Add(&chart.Metadata{Name: "chart", Version: "0.1.5+a.min.hour"}, "chart-0.1.5+a.min.hour.tgz", "http://example.com/charts", "sha256:1234567890abc") + i.Entries["chart"][len(i.Entries["chart"])-1].Created = time.Now().Add(-time.Hour) + i.Add(&chart.Metadata{Name: "chart", Version: "0.1.5+c.now"}, "chart-0.1.5+c.now.tgz", "http://example.com/charts", "sha256:1234567890abc") i.Add(&chart.Metadata{Name: "chart", Version: "0.2.0"}, "chart-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc") i.Add(&chart.Metadata{Name: "chart", Version: "1.0.0"}, "chart-1.0.0.tgz", "http://example.com/charts", "sha256:1234567890abc") i.Add(&chart.Metadata{Name: "chart", Version: "1.1.0-rc.1"}, "chart-1.1.0-rc.1.tgz", "http://example.com/charts", "sha256:1234567890abc") @@ -152,6 +158,12 @@ func TestChartRepository_Get(t *testing.T) { chartName: "non-existing", wantErr: true, }, + { + name: "match newest if ambiguous", + chartName: "chart", + chartVersion: "0.1.5", + wantVersion: "0.1.5+c.now", + }, } for _, tt := range tests { diff --git a/pkg/git/checkout.go b/pkg/git/checkout.go index 26a54f8b..103f49f5 100644 --- a/pkg/git/checkout.go +++ b/pkg/git/checkout.go @@ -19,6 +19,8 @@ package git import ( "context" "fmt" + "sort" + "time" "github.com/blang/semver/v4" "github.com/go-git/go-git/v5" @@ -189,7 +191,19 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth tr } tags := make(map[string]string) + tagTimestamps := make(map[string]time.Time) _ = repoTags.ForEach(func(t *plumbing.Reference) error { + revision := plumbing.Revision(t.Name().String()) + hash, err := repo.ResolveRevision(revision) + if err != nil { + return fmt.Errorf("unable to resolve tag revision: %w", err) + } + commit, err := repo.CommitObject(*hash) + if err != nil { + return fmt.Errorf("unable to resolve commit of a tag revision: %w", err) + } + tagTimestamps[t.Name().Short()] = commit.Committer.When + tags[t.Name().Short()] = t.Strings()[1] return nil }) @@ -203,12 +217,25 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth tr svTags[v.String()] = tag } } - if len(svers) == 0 { return nil, "", fmt.Errorf("no match found for semver: %s", c.semVer) } - semver.Sort(svers) + // Sort versions + sort.SliceStable(svers, func(i, j int) bool { + left := svers[i] + right := svers[j] + + if !left.EQ(right) { + return left.LT(right) + } + + // Having tag target timestamps at our disposal, we further try to sort + // versions into a chronological order. This is especially important for + // versions that differ only by build metadata, because it is not considered + // a part of the comparable version in Semver + return tagTimestamps[left.String()].Before(tagTimestamps[right.String()]) + }) v := svers[len(svers)-1] t := svTags[v.String()]