Repository: crazy-max/diun (go)

Issue:
Support semver sorting method for tags with a prefix
## Behaviour

(Previously mentioned in https://github.com/crazy-max/diun/pull/680#issuecomment-1318597960)

Some Docker images use tag names with a prefix in front of the version number for different variants of the image, e.g. [gitlab-runner](https://hub.docker.com/r/gitlab/gitlab-runner/tags?page=1&name=alpine-v).

The current [`semver` sorting method](https://github.com/crazy-max/diun/blob/f0278085db5523028ab05c24cf4577e09e7277a1/pkg/registry/tags_sort.go#L23-L41) for `sort_tags` doesn't support this pattern.

### Steps to reproduce this issue

Add the following config to a file provider:

```yaml
- name: gitlab/gitlab-runner
  watch_repo: true
  max_tags: 5
  sort_tags: semver
  include_tags:
    - ^alpine-v\d+\.\d+\.\d+$
```

### Expected behaviour

Tags should be sorted by the version number, and a notification should be generated for the 5 newest versions, e.g.:

```
alpine-v15.7.1 alpine-v15.7.0 alpine-v15.6.1 alpine-v15.6.0 alpine-v15.5.1
```

### Actual behaviour

Notifications are generated for 5 random old tags:

```
alpine-v1.0.0 alpine-v1.1.4 alpine-v1.1.3 alpine-v1.1.1 alpine-v1.1.0
```

### Workaround

The `semver` sort currently supports plain version numbers, and versions with a `v` prefix as a special case. Suffixes separated by a `-` are also supported as these will be parsed as the "prerelease" part of a semver.

Most Docker images will have one of these patterns available as an alternative that you can watch instead. This only has the disadvantage that the tags you are watching are not exactly the same as the tags that you are using for your containers, which might or might not be a problem.

## Configuration

* Diun version : 4.24.0
* Platform : Linux
* System info : `Linux my-server 5.4.0-107-generic #121-Ubuntu SMP Thu Mar 24 16:04:27 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux`

> DIUN_PROVIDERS_FILE_FILENAME=/file-provider-config.yml
> (see above for contents)

## Suggested improvement

The semver sorting method should be extended to support the somewhat common tag naming pattern of `variant-v1.2.3`. Possible solutions are:
* As mentioned in #680, add some syntax to `include_tags` to explicitly extract the part of the tag that should be used for sorting. This could potentially also be useful for other sorting methods in some edge cases. To avoid breaking existing configurations, a named capture group could be used instead of just the first capture group.
* Use a fixed regex on the whole tag to extract the version number. I currently use the regex `(\d+)\.(\d+)\.(\d+)(?:-(.*\d))?` in a script to extract version numbers from Docker image tags, which works for my case. This is less flexible than the first solution, but you don't need special syntax for your `include_tags` which might be unintuitive. It also doesn't support semvers with less than 3 parts, but you usually wouldn't want to mix full versions with partial versions anyways.
* Add a special case just for the `variant-v1.2.3` and `variant-1.2.3` pattern, just like there is currently a special case for including or excluding the `v` before the version number. This would not support all use cases, but probably the majority. Least difference to the current behavior, but least flexible.

Pull request:
Add sorting for prefixed semver
Strips non-numeric prefixes when checking semver. This also adds a secondary layer to the sort whereby identical semvers are sorted relative to eachother lexigraphically.

This will ensure a more stable sort for non-versioned tags (eg. `latest` or `edge`) as well as multiple variants tagged with the same number. (eg `1.0.0-alpine` compared to `1.0.0-ubuntu`).

Fixes #758

Patch:
diff --git a/pkg/registry/tags_sort.go b/pkg/registry/tags_sort.go
index 184e743e..2cd0c62b 100644
--- a/pkg/registry/tags_sort.go
+++ b/pkg/registry/tags_sort.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"sort"
 	"strings"
+	"unicode"
 
 	"golang.org/x/mod/semver"
 )
@@ -22,9 +23,9 @@ func SortTags(tags []string, sortTag SortTag) []string {
 		return tags
 	case SortTagSemver:
 		semverIsh := func(s string) string {
-			if semver.IsValid(s) {
-				return s
-			}
+			s = strings.TrimLeftFunc(s, func(r rune) bool {
+				return !unicode.IsNumber(r)
+			})
 			if vt := fmt.Sprintf("v%s", s); semver.IsValid(vt) {
 				return vt
 			}
@@ -36,7 +37,13 @@ func SortTags(tags []string, sortTag SortTag) []string {
 			} else if c < 0 {
 				return false
 			}
-			return strings.Count(tags[i], ".") > strings.Count(tags[j], ".")
+			if c := strings.Count(tags[i], ".") - strings.Count(tags[j], "."); c > 0 {
+				return true
+			} else if c < 0 {
+				return false
+			}
+
+			return strings.Compare(tags[i], tags[j]) < 0
 		})
 		return tags
 	default:


Test patch:
diff --git a/pkg/registry/tags_test.go b/pkg/registry/tags_test.go
index 943ae21a..a077bd7b 100644
--- a/pkg/registry/tags_test.go
+++ b/pkg/registry/tags_test.go
@@ -60,6 +60,8 @@ func TestTagsSort(t *testing.T) {
 		"4.8.0",
 		"4.8.1",
 		"4.9.0",
+		"ubuntu-5.0",
+		"alpine-5.0",
 		"edge",
 		"latest",
 	}
@@ -103,6 +105,8 @@ func TestTagsSort(t *testing.T) {
 				"4.8.0",
 				"4.8.1",
 				"4.9.0",
+				"ubuntu-5.0",
+				"alpine-5.0",
 				"edge",
 				"latest",
 			},
@@ -141,16 +145,20 @@ func TestTagsSort(t *testing.T) {
 				"4.8.0",
 				"4.8.1",
 				"4.9.0",
+				"alpine-5.0",
 				"edge",
 				"latest",
+				"ubuntu-5.0",
 			},
 		},
 		{
 			name:    "sort reverse",
 			sortTag: registry.SortTagReverse,
 			expected: []string{
+				"ubuntu-5.0",
 				"latest",
 				"edge",
+				"alpine-5.0",
 				"4.9.0",
 				"4.8.1",
 				"4.8.0",
@@ -187,6 +195,8 @@ func TestTagsSort(t *testing.T) {
 			name:    "sort semver",
 			sortTag: registry.SortTagSemver,
 			expected: []string{
+				"alpine-5.0",
+				"ubuntu-5.0",
 				"4.21.0",
 				"4.21",
 				"4.20.1",