internal/task: update x/tools dependency and create a CL for review updateXToolsDependency will drop the replace directive in gopls sub module and pin the x/tools version to the head of the current gopls release branch. A local relui screenshot is at https://go.dev/issue/57643#issuecomment-2263725465 For golang/go#57643 Change-Id: I0d9f4462b71b394ff432bc65142cf35e70977e6a Reviewed-on: https://go-review.googlesource.com/c/build/+/602516 Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Auto-Submit: Hongxiang Jiang <hxjiang@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/task/releasegopls.go b/internal/task/releasegopls.go index 243d527..e763299 100644 --- a/internal/task/releasegopls.go +++ b/internal/task/releasegopls.go
@@ -8,6 +8,7 @@ "errors" "fmt" "slices" + "strconv" "strings" "time" @@ -34,7 +35,9 @@ semversion := wf.Task1(wd, "validating input version", r.isValidVersion, version) branchCreated := wf.Action1(wd, "creating new branch if minor release", r.createBranchIfMinor, semversion) changeID := wf.Task2(wd, "updating branch's codereview.cfg", r.updateCodeReviewConfig, semversion, reviewers, wf.After(branchCreated)) - _ = wf.Action1(wd, "await config CL submission", r.AwaitSubmission, changeID) + submitted := wf.Action1(wd, "await config CL submission", r.AwaitSubmission, changeID) + changeID = wf.Task2(wd, "updating gopls' x/tools dependency", r.updateXToolsDependency, semversion, reviewers, wf.After(submitted)) + _ = wf.Action1(wd, "await gopls' x/tools dependency CL submission", r.AwaitSubmission, changeID) return wd } @@ -73,10 +76,29 @@ return err } -// updateCodeReviewConfig ensures codereview.cfg contains the expected -// configuration. +// openCL checks if an open CL with the given title exists in the specified +// branch. // -// It returns the change ID, or "" if the CL was not created. +// It returns an empty string if no such CL is found, otherwise it returns the +// CL's change ID. +func (r *ReleaseGoplsTasks) openCL(ctx *wf.TaskContext, branch, title string) (string, error) { + // Query for an existing pending config CL, to avoid duplication. + query := fmt.Sprintf(`message:%q status:open owner:gobot@golang.org repo:tools branch:%q -age:7d`, title, branch) + changes, err := r.Gerrit.QueryChanges(ctx, query) + if err != nil { + return "", err + } + if len(changes) == 0 { + return "", nil + } + + return changes[0].ChangeID, nil +} + +// updateCodeReviewConfig checks if codereview.cfg has the desired configuration. +// +// It returns the change ID required to update the config if changes are needed, +// otherwise it returns an empty string indicating no update is necessary. func (r *ReleaseGoplsTasks) updateCodeReviewConfig(ctx *wf.TaskContext, semv semversion, reviewers []string) (string, error) { const configFile = "codereview.cfg" const configFmt = `issuerepo: golang/go @@ -87,15 +109,13 @@ branch := goplsReleaseBranchName(semv) clTitle := fmt.Sprintf("all: update %s for %s", configFile, branch) - // Query for an existing pending config CL, to avoid duplication. - query := fmt.Sprintf(`message:%q status:open owner:gobot@golang.org repo:tools branch:%q -age:7d`, clTitle, branch) - changes, err := r.Gerrit.QueryChanges(ctx, query) + openCL, err := r.openCL(ctx, branch, clTitle) if err != nil { - return "", err + return "", fmt.Errorf("failed to find the open CL of title %q in branch %q: %w", clTitle, branch, err) } - if len(changes) > 0 { - ctx.Printf("not creating CL: found existing CL %d", changes[0].ChangeNumber) - return changes[0].ChangeID, nil + if openCL != "" { + ctx.Printf("not creating CL: found existing CL %s", openCL) + return openCL, nil } head, err := r.Gerrit.ReadBranchHead(ctx, "tools", branch) @@ -128,6 +148,110 @@ return r.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files) } +// nextPrerelease go through the tags in tools repo that matches with the given +// version and find the next pre-release version. +func (r *ReleaseGoplsTasks) nextPrerelease(ctx *wf.TaskContext, semv semversion) (int, error) { + tags, err := r.Gerrit.ListTags(ctx, "tools") + if err != nil { + return -1, fmt.Errorf("failed to list tags for tools repo: %w", err) + } + + max := 0 + for _, tag := range tags { + v, ok := strings.CutPrefix(tag, "gopls/") + if !ok { + continue + } + cur, ok := parseSemver(v) + if !ok { + continue + } + if cur.Major != semv.Major || cur.Minor != semv.Minor || cur.Patch != semv.Patch { + continue + } + pre, err := cur.prereleaseVersion() + if err != nil { + continue + } + + if pre > max { + max = pre + } + } + + return max + 1, nil +} + +// updateXToolsDependency ensures gopls sub module have the correct x/tools +// version as dependency. +// +// It returns the change ID, or "" if the CL was not created. +func (r *ReleaseGoplsTasks) updateXToolsDependency(ctx *wf.TaskContext, semv semversion, reviewers []string) (string, error) { + const scriptFmt = `cp gopls/go.mod gopls/go.mod.before +cp gopls/go.sum gopls/go.sum.before +cd gopls +go mod edit -dropreplace=golang.org/x/tools +go get -u golang.org/x/tools@%s +go mod tidy -compat=1.19 +` + + pre, err := r.nextPrerelease(ctx, semv) + if err != nil { + return "", fmt.Errorf("failed to find the next prerelease version: %w", err) + } + + branch := goplsReleaseBranchName(semv) + clTitle := fmt.Sprintf("gopls: update go.mod for v%v.%v.%v-pre.%v", semv.Major, semv.Minor, semv.Patch, pre) + openCL, err := r.openCL(ctx, branch, clTitle) + if err != nil { + return "", fmt.Errorf("failed to find the open CL of title %q in branch %q: %w", clTitle, branch, err) + } + if openCL != "" { + ctx.Printf("not creating CL: found existing CL %s", openCL) + return openCL, nil + } + + outputFiles := []string{"gopls/go.mod.before", "gopls/go.mod", "gopls/go.sum.before", "gopls/go.sum"} + // TODO(hxjiang): Replacing branch with the latest non-pinned commit in the + // release branch. Rationale: + // 1. Module proxy might return an outdated commit when using the branch name + // (to be confirmed with samthanawalla@). + // 2. Pinning x/tools using the latest commit from a branch isn't idempotent. + // It's best to avoid pinning x/tools to a version that's effectively another + // pin. + build, err := r.CloudBuild.RunScript(ctx, fmt.Sprintf(scriptFmt, branch), "tools", outputFiles) + if err != nil { + return "", err + } + + outputs, err := buildToOutputs(ctx, r.CloudBuild, build) + if err != nil { + return "", err + } + + changedFiles := map[string]string{} + for i := 0; i < len(outputFiles); i += 2 { + before, after := outputs[outputFiles[i]], outputs[outputFiles[i+1]] + if before != after { + changedFiles[outputFiles[i+1]] = after + } + } + + // Skip CL creation as nothing changed. + if len(changedFiles) == 0 { + return "", nil + } + + changeInput := gerrit.ChangeInput{ + Project: "tools", + Branch: branch, + Subject: fmt.Sprintf("%s\n\nThis is an automated CL which updates the go.mod go.sum.", clTitle), + } + + ctx.Printf("creating auto-submit change under branch %q in x/tools repo.", branch) + return r.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, changedFiles) +} + // AwaitSubmission waits for the CL with the given change ID to be submitted. // // The return value is the submitted commit hash, or "" if changeID is "". @@ -179,6 +303,24 @@ return parsed, ok } +func (s *semversion) prereleaseVersion() (int, error) { + parts := strings.Split(s.Pre, ".") + if len(parts) == 1 { + return -1, fmt.Errorf(`prerelease version does not contain any "."`) + } + + if len(parts) > 2 { + return -1, fmt.Errorf(`prerelease version contains %v "."`, len(parts)-1) + } + + pre, err := strconv.Atoi(parts[1]) + if err != nil { + return -1, fmt.Errorf("failed to convert prerelease version to int %q: %w", pre, err) + } + + return pre, nil +} + // 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) {
diff --git a/internal/task/releasegopls_test.go b/internal/task/releasegopls_test.go index ebe33a6..6c4d6aa 100644 --- a/internal/task/releasegopls_test.go +++ b/internal/task/releasegopls_test.go
@@ -312,3 +312,65 @@ }) } } + +func TestNextPrerelease(t *testing.T) { + ctx := context.Background() + testcases := []struct { + name string + tags []string + version string + want int + }{ + { + name: "next pre-release is 2", + tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1"}, + version: "v0.16.0", + want: 2, + }, + { + name: "next pre-release is 2 regardless of other minor or patch version", + tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1", "gopls/v0.16.1-pre.1", "gopls/v0.2.0-pre.3"}, + version: "v0.16.0", + want: 2, + }, + { + name: "next pre-release is 2 regardless of non-int prerelease version", + tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1", "gopls/v0.16.0-pre.foo", "gopls/v0.16.0-pre.bar"}, + version: "v0.16.0", + want: 2, + }, + } + + for _, tc := range testcases { + 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, + } + + semv, ok := parseSemver(tc.version) + if !ok { + t.Fatalf("parseSemver(%q) should success", tc.version) + } + got, err := tasks.nextPrerelease(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv) + if err != nil { + t.Fatalf("nextPrerelease(%q) should not return error: %v", tc.version, err) + } + + if tc.want != got { + t.Errorf("nextPrerelease(%q) = %v want %v", tc.version, got, tc.want) + } + }) + } +}