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)
+ }
+ })
+ }
+}