internal/task, cmd/relui: add definition for releasing gopls
1. Param: coordinator need to provide the version string that will be
released.
2. Task: validate the input version a valid version.
3. Output: returns whether the input version is valid.
For golang/go#57643
Change-Id: Ib6d3f10c9a46ad2f24b0a1fc053265456d152f41
Reviewed-on: https://go-review.googlesource.com/c/build/+/601236
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/cmd/relui/main.go b/cmd/relui/main.go
index e3e6cf9..4d2cac8 100644
--- a/cmd/relui/main.go
+++ b/cmd/relui/main.go
@@ -308,6 +308,11 @@
}
dh.RegisterDefinition("Tag a new version of x/telemetry/config (if necessary)", tagTelemetryTasks.NewDefinition())
+ releaseGoplsTasks := task.ReleaseGoplsTasks{
+ Gerrit: gerritClient,
+ }
+ dh.RegisterDefinition("Release a new version of gopls", releaseGoplsTasks.NewDefinition())
+
privateSyncTask := &task.PrivateMasterSyncTask{
Git: gitClient,
PrivateGerritURL: "https://go-internal.googlesource.com/golang/go-private",
diff --git a/internal/task/releasegopls.go b/internal/task/releasegopls.go
new file mode 100644
index 0000000..fd956d4
--- /dev/null
+++ b/internal/task/releasegopls.go
@@ -0,0 +1,125 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package task
+
+import (
+ "fmt"
+ "slices"
+ "strings"
+
+ wf "golang.org/x/build/internal/workflow"
+ "golang.org/x/mod/semver"
+)
+
+// ReleaseGoplsTasks implements a new workflow definition include all the tasks
+// to release a gopls.
+type ReleaseGoplsTasks struct {
+ Gerrit GerritClient
+}
+
+// NewDefinition create a new workflow definition for releasing gopls.
+func (r *ReleaseGoplsTasks) NewDefinition() *wf.Definition {
+ wd := wf.New()
+
+ // TODO(hxjiang): provide potential release versions in the relui where the
+ // coordinator can choose which version to release instead of manual input.
+ version := wf.Param(wd, wf.ParamDef[string]{Name: "version"})
+ isValid := wf.Task1(wd, "validating input version", r.isValidVersion, version)
+ wf.Output(wd, "valid", isValid)
+ return wd
+}
+
+func (r *ReleaseGoplsTasks) isValidVersion(ctx *wf.TaskContext, ver string) (bool, error) {
+ if !semver.IsValid(ver) {
+ return false, nil
+ }
+
+ versions, err := r.possibleGoplsVersions(ctx)
+ if err != nil {
+ return false, fmt.Errorf("failed to get latest Gopls version tags from x/tool: %w", err)
+ }
+
+ return slices.Contains(versions, ver), nil
+}
+
+// semversion is a parsed semantic version.
+type semversion struct {
+ major, minor, patch int
+ pre string
+}
+
+// parseSemver attempts to parse semver components out of the provided semver
+// v. If v is not valid semver in canonical form, parseSemver returns false.
+func parseSemver(v string) (_ semversion, ok bool) {
+ var parsed semversion
+ v, parsed.pre, _ = strings.Cut(v, "-")
+ if _, err := fmt.Sscanf(v, "v%d.%d.%d", &parsed.major, &parsed.minor, &parsed.patch); err == nil {
+ ok = true
+ }
+ return parsed, ok
+}
+
+// possibleGoplsVersions identifies suitable versions for the upcoming release
+// based on the current tags in the repo.
+func (r *ReleaseGoplsTasks) possibleGoplsVersions(ctx *wf.TaskContext) ([]string, error) {
+ tags, err := r.Gerrit.ListTags(ctx, "tools")
+ if err != nil {
+ return nil, err
+ }
+
+ var semVersions []semversion
+ majorMinorPatch := map[int]map[int]map[int]bool{}
+ for _, tag := range tags {
+ v, ok := strings.CutPrefix(tag, "gopls/")
+ if !ok {
+ continue
+ }
+
+ if !semver.IsValid(v) {
+ continue
+ }
+
+ // Skip for pre-release versions.
+ if semver.Prerelease(v) != "" {
+ continue
+ }
+
+ semv, ok := parseSemver(v)
+ semVersions = append(semVersions, semv)
+
+ if majorMinorPatch[semv.major] == nil {
+ majorMinorPatch[semv.major] = map[int]map[int]bool{}
+ }
+ if majorMinorPatch[semv.major][semv.minor] == nil {
+ majorMinorPatch[semv.major][semv.minor] = map[int]bool{}
+ }
+ majorMinorPatch[semv.major][semv.minor][semv.patch] = true
+ }
+
+ var possible []string
+ seen := map[string]bool{}
+ for _, v := range semVersions {
+ nextMajor := fmt.Sprintf("v%d.%d.%d", v.major+1, 0, 0)
+ if _, ok := majorMinorPatch[v.major+1]; !ok && !seen[nextMajor] {
+ seen[nextMajor] = true
+ possible = append(possible, nextMajor)
+ }
+
+ nextMinor := fmt.Sprintf("v%d.%d.%d", v.major, v.minor+1, 0)
+ if _, ok := majorMinorPatch[v.major][v.minor+1]; !ok && !seen[nextMinor] {
+ seen[nextMinor] = true
+ possible = append(possible, nextMinor)
+ }
+
+ nextPatch := fmt.Sprintf("v%d.%d.%d", v.major, v.minor, v.patch+1)
+ if _, ok := majorMinorPatch[v.major][v.minor][v.patch+1]; !ok && !seen[nextPatch] {
+ seen[nextPatch] = true
+ possible = append(possible, nextPatch)
+ }
+ }
+
+ semver.Sort(possible)
+ return possible, nil
+}
diff --git a/internal/task/releasegopls_test.go b/internal/task/releasegopls_test.go
new file mode 100644
index 0000000..b916190
--- /dev/null
+++ b/internal/task/releasegopls_test.go
@@ -0,0 +1,85 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package task
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/build/internal/workflow"
+)
+
+func TestPossibleGoplsVersions(t *testing.T) {
+ tests := []struct {
+ name string
+ tags []string
+ want []string
+ }{
+ {
+ name: "any one version tag should have three possible next versions",
+ tags: []string{"gopls/v1.2.3"},
+ want: []string{"v1.2.4", "v1.3.0", "v2.0.0"},
+ },
+ {
+ name: "1.2.0 should be skipped because 1.2.3 already exist",
+ tags: []string{"gopls/v1.2.3", "gopls/v1.1.0"},
+ want: []string{"v1.1.1", "v1.2.4", "v1.3.0", "v2.0.0"},
+ },
+ {
+ name: "2.0.0 should be skipped because 2.1.3 already exist",
+ tags: []string{"gopls/v1.2.3", "gopls/v2.1.3"},
+ want: []string{"v1.2.4", "v1.3.0", "v2.1.4", "v2.2.0", "v3.0.0"},
+ },
+ {
+ name: "1.2.0 is still consider valid version because there is no 1.2.X",
+ tags: []string{"gopls/v1.1.3", "gopls/v1.3.2", "gopls/v2.1.2"},
+ want: []string{"v1.1.4", "v1.2.0", "v1.3.3", "v1.4.0", "v2.1.3", "v2.2.0", "v3.0.0"},
+ },
+ {
+ name: "2.0.0 is still consider valid version because there is no 2.X.X",
+ tags: []string{"gopls/v1.2.3", "gopls/v3.1.2"},
+ want: []string{"v1.2.4", "v1.3.0", "v2.0.0", "v3.1.3", "v3.2.0", "v4.0.0"},
+ },
+ {
+ name: "pre-release version tag should not have any effect on the next version",
+ tags: []string{"gopls/v0.16.1-pre.1", "gopls/v0.16.1-pre.2", "gopls/v0.16.0"},
+ want: []string{"v0.16.1", "v0.17.0", "v1.0.0"},
+ },
+ {
+ name: "other unrelated tag should not have any effect on the next version",
+ tags: []string{"v0.9.2", "v0.9.3", "v0.23.0", "gopls/v0.16.0"},
+ want: []string{"v0.16.1", "v0.17.0", "v1.0.0"},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ tools := NewFakeRepo(t, "tools")
+ commit := tools.Commit(map[string]string{
+ "go.mod": "module golang.org/x/tools\n",
+ "go.sum": "\n",
+ })
+
+ for _, tag := range tc.tags {
+ tools.Tag(tag, commit)
+ }
+
+ gerrit := NewFakeGerrit(t, tools)
+
+ tasks := &ReleaseGoplsTasks{
+ Gerrit: gerrit,
+ }
+
+ got, err := tasks.possibleGoplsVersions(&workflow.TaskContext{Context: context.Background()})
+ if err != nil {
+ t.Fatalf("possibleGoplsVersions() should not return error, but return %v", err)
+ }
+ if diff := cmp.Diff(tc.want, got); diff != "" {
+ t.Errorf("possibleGoplsVersions() mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}