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
39 changes: 26 additions & 13 deletions internal/ghcr/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (c *Client) FullImage() string {
// creation time — the same order GitHub shows package versions. Tags whose
// creation time can't be resolved keep a semver-descending fallback order
// after the dated ones. The tags that "latest", "nightly" and "main" point to
// are marked Latest, Nightly and Main; the Latest row is moved to the front.
// are marked Latest, Nightly and Main and moved to the front in that order.
func (c *Client) ListTags(ctx context.Context) ([]ImageTag, error) {
registryToken, err := c.fetchRegistryToken(ctx)
if err != nil {
Expand Down Expand Up @@ -136,7 +136,6 @@ func (c *Client) ListTags(ctx context.Context) ([]ImageTag, error) {
base := c.FullImage()
result := make([]ImageTag, len(infos))
flagged := make([]bool, len(channelTags))
latestFound := false
for i, info := range infos {
r := ImageTag{Tag: info.tag, Image: base + ":" + info.tag}
if !info.created.IsZero() {
Expand All @@ -150,7 +149,6 @@ func (c *Client) ListTags(ctx context.Context) ([]ImageTag, error) {
switch channelTags[k] {
case "latest":
r.Latest = true
latestFound = true
case "nightly":
r.Nightly = true
case "main":
Expand All @@ -160,19 +158,34 @@ func (c *Client) ListTags(ctx context.Context) ([]ImageTag, error) {
result[i] = r
}

// Ensure the latest-marked tag is first so frontends can default to result[0].
if latestFound {
for i, r := range result {
if r.Latest && i != 0 {
result[0], result[i] = result[i], result[0]
break
}
}
}

orderChannelsFirst(result)
return result, nil
}

// orderChannelsFirst moves the channel rows to the front in the order latest,
// nightly, main, so frontends can default to result[0]. The stable sort keeps
// the remaining rows in their existing newest-first order.
func orderChannelsFirst(tags []ImageTag) {
sort.SliceStable(tags, func(i, j int) bool {
return channelRank(tags[i]) < channelRank(tags[j])
})
}

// channelRank is a row's sort rank by its highest-priority channel flag;
// rows no channel points to rank last.
func channelRank(t ImageTag) int {
switch {
case t.Latest:
return 0
case t.Nightly:
return 1
case t.Main:
return 2
default:
return 3
}
}

// tagInfo carries what sorting and latest-matching need per tag.
type tagInfo struct {
tag string
Expand Down
64 changes: 64 additions & 0 deletions internal/ghcr/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package ghcr

import (
"slices"
"testing"
)

func TestOrderChannelsFirst(t *testing.T) {
tests := []struct {
name string
in []ImageTag
want []string
}{
{
name: "nightly newest, latest in the middle",
in: []ImageTag{
{Tag: "f16a2fe", Nightly: true},
{Tag: "6456f7b", Main: true},
{Tag: "cf959b0"},
{Tag: "eucalyptus", Latest: true},
{Tag: "dogwood-r1"},
{Tag: "dogwood"},
},
want: []string{"eucalyptus", "f16a2fe", "6456f7b", "cf959b0", "dogwood-r1", "dogwood"},
},
{
name: "nightly and main on one build",
in: []ImageTag{
{Tag: "a"},
{Tag: "b", Nightly: true, Main: true},
{Tag: "c", Latest: true},
{Tag: "d"},
},
want: []string{"c", "b", "a", "d"},
},
{
name: "no nightly",
in: []ImageTag{
{Tag: "a"},
{Tag: "b", Main: true},
{Tag: "c"},
{Tag: "d", Latest: true},
},
want: []string{"d", "b", "a", "c"},
},
{
name: "no channels",
in: []ImageTag{{Tag: "a"}, {Tag: "b"}, {Tag: "c"}},
want: []string{"a", "b", "c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
orderChannelsFirst(tt.in)
var got []string
for _, r := range tt.in {
got = append(got, r.Tag)
}
if !slices.Equal(got, tt.want) {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
Loading