From f6a6872edadf5528b35d1928afa04b29c076ed3f Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Tue, 5 Dec 2023 09:51:56 +0100 Subject: [PATCH] feat: add helper functions to generate semver tags from refs (#34) --- go.mod | 2 +- plugin/tags.go | 100 +++++++++++++++++++ plugin/tags_test.go | 233 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 plugin/tags.go create mode 100644 plugin/tags_test.go diff --git a/go.mod b/go.mod index 6b58474..d5a6f27 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/thegeeklab/wp-plugin-go go 1.21 require ( + github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/sprig/v3 v3.2.3 github.com/joho/godotenv v1.5.1 github.com/rs/zerolog v1.31.0 @@ -12,7 +13,6 @@ require ( require ( github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/google/uuid v1.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect diff --git a/plugin/tags.go b/plugin/tags.go new file mode 100644 index 0000000..157a2cb --- /dev/null +++ b/plugin/tags.go @@ -0,0 +1,100 @@ +package plugin + +import ( + "fmt" + "strings" + + "github.com/Masterminds/semver/v3" +) + +// SemverTagSuffix returns a set of default suggested tags +// based on the commit ref with an attached suffix. +func SemverTagSuffix(ref, suffix string, strict bool) ([]string, error) { + tags, err := SemverTags(ref, strict) + if err != nil { + return nil, err + } + + if len(suffix) == 0 { + return tags, nil + } + + for i, tag := range tags { + if tag == "latest" { + tags[i] = suffix + } else { + tags[i] = fmt.Sprintf("%s-%s", tag, suffix) + } + } + + return tags, nil +} + +// SemverTags returns a set of default suggested tags based on +// the commit ref. +func SemverTags(ref string, strict bool) ([]string, error) { + var ( + version *semver.Version + err error + ) + + if !strings.HasPrefix(ref, "refs/tags/") { + return []string{"latest"}, nil + } + + rawVersion := stripTagPrefix(ref) + + version, err = semver.NewVersion(rawVersion) + if strict { + version, err = semver.StrictNewVersion(rawVersion) + } + + if err != nil { + return []string{"latest"}, err + } + + if version.Prerelease() != "" { + return []string{ + version.String(), + }, nil + } + + if version.Major() == 0 { + return []string{ + fmt.Sprintf("%v.%v", version.Major(), version.Minor()), + fmt.Sprintf("%v.%v.%v", version.Major(), version.Minor(), version.Patch()), + }, nil + } + + return []string{ + fmt.Sprintf("%v", version.Major()), + fmt.Sprintf("%v.%v", version.Major(), version.Minor()), + fmt.Sprintf("%v.%v.%v", version.Major(), version.Minor(), version.Patch()), + }, nil +} + +// IsTaggable checks whether tags should be created for the specified ref. +// The function returns true if the ref either matches the default branch +// or is a tag ref. +func IsTaggable(ref, defaultBranch string) bool { + if strings.HasPrefix(ref, "refs/tags/") { + return true + } + + if stripHeadPrefix(ref) == defaultBranch { + return true + } + + return false +} + +func stripHeadPrefix(ref string) string { + return strings.TrimPrefix(ref, "refs/heads/") +} + +func stripTagPrefix(ref string) string { + ref = strings.TrimPrefix(ref, "refs/tags/") + ref = strings.TrimPrefix(ref, "v") + + return ref +} diff --git a/plugin/tags_test.go b/plugin/tags_test.go new file mode 100644 index 0000000..299e424 --- /dev/null +++ b/plugin/tags_test.go @@ -0,0 +1,233 @@ +package plugin + +import ( + "reflect" + "testing" +) + +func Test_stripTagPrefix(t *testing.T) { + tests := []struct { + Before string + After string + }{ + {"refs/tags/1.0.0", "1.0.0"}, + {"refs/tags/v1.0.0", "1.0.0"}, + {"v1.0.0", "1.0.0"}, + } + + for _, test := range tests { + got, want := stripTagPrefix(test.Before), test.After + if got != want { + t.Errorf("Got tag %s, want %s", got, want) + } + } +} + +func TestSemverTagsStrict(t *testing.T) { + tests := []struct { + Before string + After []string + }{ + {"", []string{"latest"}}, + {"refs/heads/main", []string{"latest"}}, + {"refs/tags/0.9.0", []string{"0.9", "0.9.0"}}, + {"refs/tags/1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0+1", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0-alpha.1", []string{"1.0.0-alpha.1"}}, + {"refs/tags/v1.0.0-alpha", []string{"1.0.0-alpha"}}, + } + + for _, test := range tests { + tags, err := SemverTags(test.Before, true) + if err != nil { + t.Error(err) + + continue + } + + got, want := tags, test.After + if !reflect.DeepEqual(got, want) { + t.Errorf("Got tag %v, want %v", got, want) + } + } +} + +func TestSemverTags(t *testing.T) { + tests := []struct { + Before string + After []string + }{ + {"", []string{"latest"}}, + {"refs/heads/main", []string{"latest"}}, + {"refs/tags/0.9.0", []string{"0.9", "0.9.0"}}, + {"refs/tags/1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0+1", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0-alpha.1", []string{"1.0.0-alpha.1"}}, + {"refs/tags/v1.0.0-alpha", []string{"1.0.0-alpha"}}, + {"refs/tags/v1.0-alpha", []string{"1.0.0-alpha"}}, + {"refs/tags/22.04.0", []string{"22", "22.4", "22.4.0"}}, + {"refs/tags/22.04", []string{"22", "22.4", "22.4.0"}}, + } + + for _, test := range tests { + tags, err := SemverTags(test.Before, false) + if err != nil { + t.Error(err) + + continue + } + + got, want := tags, test.After + if !reflect.DeepEqual(got, want) { + t.Errorf("Got tag %v, want %v", got, want) + } + } +} + +func TestSemverTagsSrtictError(t *testing.T) { + tests := []string{ + "refs/tags/x1.0.0", + "refs/tags/20190203", + "refs/tags/22.04.0", + "refs/tags/22.04", + } + + for _, test := range tests { + _, err := SemverTags(test, true) + if err == nil { + t.Errorf("Expect tag error for %s", test) + } + } +} + +func TestSemverTagSuffix(t *testing.T) { + tests := []struct { + Before string + Suffix string + After []string + }{ + // without suffix + { + After: []string{"latest"}, + }, + { + Before: "refs/tags/v1.0.0", + After: []string{ + "1", + "1.0", + "1.0.0", + }, + }, + // with suffix + { + Suffix: "linux-amd64", + After: []string{"linux-amd64"}, + }, + { + Before: "refs/tags/v1.0.0", + Suffix: "linux-amd64", + After: []string{ + "1-linux-amd64", + "1.0-linux-amd64", + "1.0.0-linux-amd64", + }, + }, + { + Suffix: "nanoserver", + After: []string{"nanoserver"}, + }, + { + Before: "refs/tags/v1.9.2", + Suffix: "nanoserver", + After: []string{ + "1-nanoserver", + "1.9-nanoserver", + "1.9.2-nanoserver", + }, + }, + } + + for _, test := range tests { + tag, err := SemverTagSuffix(test.Before, test.Suffix, true) + if err != nil { + t.Error(err) + + continue + } + + got, want := tag, test.After + if !reflect.DeepEqual(got, want) { + t.Errorf("Got tag %v, want %v", got, want) + } + } +} + +func Test_stripHeadPrefix(t *testing.T) { + type args struct { + ref string + } + + tests := []struct { + args args + want string + }{ + { + args: args{ + ref: "refs/heads/main", + }, + want: "main", + }, + } + + for _, tt := range tests { + if got := stripHeadPrefix(tt.args.ref); got != tt.want { + t.Errorf("stripHeadPrefix() = %v, want %v", got, tt.want) + } + } +} + +func TestIsTaggable(t *testing.T) { + type args struct { + ref string + defaultBranch string + } + + tests := []struct { + name string + args args + want bool + }{ + { + name: "latest tag for default branch", + args: args{ + ref: "refs/heads/main", + defaultBranch: "main", + }, + want: true, + }, + { + name: "build from tags", + args: args{ + ref: "refs/tags/v1.0.0", + defaultBranch: "main", + }, + want: true, + }, + { + name: "skip build for not default branch", + args: args{ + ref: "refs/heads/develop", + defaultBranch: "main", + }, + want: false, + }, + } + + for _, tt := range tests { + if got := IsTaggable(tt.args.ref, tt.args.defaultBranch); got != tt.want { + t.Errorf("%q. IsTaggable() = %v, want %v", tt.name, got, tt.want) + } + } +}