internal/task: update codereview.cfg and create a CL for review
1. Add optional parameter reviewers to gopls release flow.
2. Add a Task to update code review configuration in release branch.
3. Add a Action to wait for CL to be submitted.
A local relui screenshot is at https://go.dev/issue/57643#issuecomment-2259049777
For golang/go#57643
Change-Id: Iae3ebd5dd7bc9040979185aa353e2a4afed8cb54
Reviewed-on: https://go-review.googlesource.com/c/build/+/601241
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Hongxiang Jiang <hxjiang@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/relui/main.go b/cmd/relui/main.go
index 4d2cac8..def4682 100644
--- a/cmd/relui/main.go
+++ b/cmd/relui/main.go
@@ -309,7 +309,8 @@
dh.RegisterDefinition("Tag a new version of x/telemetry/config (if necessary)", tagTelemetryTasks.NewDefinition())
releaseGoplsTasks := task.ReleaseGoplsTasks{
- Gerrit: gerritClient,
+ Gerrit: gerritClient,
+ CloudBuild: cloudBuildClient,
}
dh.RegisterDefinition("Release a new version of gopls", releaseGoplsTasks.NewDefinition())
diff --git a/internal/task/releasegopls.go b/internal/task/releasegopls.go
index 8f72251..243d527 100644
--- a/internal/task/releasegopls.go
+++ b/internal/task/releasegopls.go
@@ -5,9 +5,11 @@
package task
import (
+ "errors"
"fmt"
"slices"
"strings"
+ "time"
"golang.org/x/build/gerrit"
wf "golang.org/x/build/internal/workflow"
@@ -17,7 +19,8 @@
// ReleaseGoplsTasks implements a new workflow definition include all the tasks
// to release a gopls.
type ReleaseGoplsTasks struct {
- Gerrit GerritClient
+ Gerrit GerritClient
+ CloudBuild CloudBuildClient
}
// NewDefinition create a new workflow definition for releasing gopls.
@@ -27,16 +30,24 @@
// 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"})
+ reviewers := wf.Param(wd, reviewersParam)
semversion := wf.Task1(wd, "validating input version", r.isValidVersion, version)
- _ = wf.Action1(wd, "creating new branch if minor release", r.createBranchIfMinor, semversion)
+ 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)
return wd
}
+// goplsReleaseBranchName returns the branch name for given input release version.
+func goplsReleaseBranchName(semv semversion) string {
+ return fmt.Sprintf("gopls-release-branch.%v.%v", semv.Major, semv.Minor)
+}
+
// createBranchIfMinor create the release branch if the input version is a minor
// release.
// All patch releases under the same minor version share the same release branch.
func (r *ReleaseGoplsTasks) createBranchIfMinor(ctx *wf.TaskContext, semv semversion) error {
- branch := fmt.Sprintf("gopls-release-branch.%v.%v", semv.Major, semv.Minor)
+ branch := goplsReleaseBranchName(semv)
// Require gopls release branch existence if this is a non-minor release.
if semv.Patch != 0 {
@@ -62,6 +73,77 @@
return err
}
+// updateCodeReviewConfig ensures codereview.cfg contains the expected
+// configuration.
+//
+// It returns the change ID, or "" if the CL was not created.
+func (r *ReleaseGoplsTasks) updateCodeReviewConfig(ctx *wf.TaskContext, semv semversion, reviewers []string) (string, error) {
+ const configFile = "codereview.cfg"
+ const configFmt = `issuerepo: golang/go
+branch: %s
+parent-branch: master
+`
+
+ 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)
+ if err != nil {
+ return "", err
+ }
+ if len(changes) > 0 {
+ ctx.Printf("not creating CL: found existing CL %d", changes[0].ChangeNumber)
+ return changes[0].ChangeID, nil
+ }
+
+ head, err := r.Gerrit.ReadBranchHead(ctx, "tools", branch)
+ if err != nil {
+ return "", err
+ }
+
+ before, err := r.Gerrit.ReadFile(ctx, "tools", head, configFile)
+ if err != nil && !errors.Is(err, gerrit.ErrResourceNotExist) {
+ return "", err
+ }
+
+ after := fmt.Sprintf(configFmt, branch)
+ // Skip CL creation as config has not changed.
+ if string(before) == after {
+ return "", nil
+ }
+
+ changeInput := gerrit.ChangeInput{
+ Project: "tools",
+ Subject: fmt.Sprintf("%s\n\nThis is an automated CL which updates the %s.", clTitle, configFile),
+ Branch: branch,
+ }
+
+ files := map[string]string{
+ configFile: string(after),
+ }
+
+ ctx.Printf("creating auto-submit change to %s under branch %q in x/tools repo.", configFile, branch)
+ return r.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files)
+}
+
+// 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 "".
+func (r *ReleaseGoplsTasks) AwaitSubmission(ctx *wf.TaskContext, changeID string) error {
+ if changeID == "" {
+ ctx.Printf("not awaiting: no CL was created")
+ return nil
+ }
+
+ ctx.Printf("awaiting review/submit of %v", ChangeLink(changeID))
+ _, err := AwaitCondition(ctx, 10*time.Second, func() (string, bool, error) {
+ return r.Gerrit.Submitted(ctx, changeID, "")
+ })
+ return err
+}
+
func (r *ReleaseGoplsTasks) isValidVersion(ctx *wf.TaskContext, ver string) (semversion, error) {
if !semver.IsValid(ver) {
return semversion{}, fmt.Errorf("the input %q version does not follow semantic version schema", ver)
diff --git a/internal/task/releasegopls_test.go b/internal/task/releasegopls_test.go
index 19b5248..ebe33a6 100644
--- a/internal/task/releasegopls_test.go
+++ b/internal/task/releasegopls_test.go
@@ -158,7 +158,7 @@
t.Errorf("createBranchIfMinor() should return nil but return err: %v", err)
}
- // Created branch should have same revision as master branch's HEAD.
+ // Created branch should have same revision as master branch's head.
if tc.wantBranch != "" {
gotRevision, err := gerritClient.ReadBranchHead(ctx, "tools", tc.wantBranch)
if err != nil {
@@ -171,3 +171,144 @@
})
}
}
+
+func TestUpdateCodeReviewConfig(t *testing.T) {
+ ctx := context.Background()
+ testcases := []struct {
+ name string
+ version string
+ config string
+ wantCommit bool
+ wantConfig string
+ }{
+ {
+ name: "should update the codereview.cfg with version 1.2 for input minor release 1.2.0",
+ version: "v1.2.0",
+ config: "foo",
+ wantCommit: true,
+ wantConfig: `issuerepo: golang/go
+branch: gopls-release-branch.1.2
+parent-branch: master
+`,
+ },
+ {
+ name: "should update the codereview.cfg with version 1.2 for input patch release 1.2.3",
+ version: "v1.2.3",
+ config: "foo",
+ wantCommit: true,
+ wantConfig: `issuerepo: golang/go
+branch: gopls-release-branch.1.2
+parent-branch: master
+`,
+ },
+ {
+ name: "no need to update the config for a minor release 1.3.0",
+ version: "v1.3.0",
+ config: `issuerepo: golang/go
+branch: gopls-release-branch.1.3
+parent-branch: master
+`,
+ wantCommit: false,
+ wantConfig: `issuerepo: golang/go
+branch: gopls-release-branch.1.3
+parent-branch: master
+`,
+ },
+ {
+ name: "no need to update the config for a patch release 1.3.3",
+ version: "v1.3.3",
+ config: `issuerepo: golang/go
+branch: gopls-release-branch.1.3
+parent-branch: master
+`,
+ wantCommit: false,
+ wantConfig: `issuerepo: golang/go
+branch: gopls-release-branch.1.3
+parent-branch: master
+`,
+ },
+ }
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ tools := NewFakeRepo(t, "tools")
+ _ = tools.Commit(map[string]string{
+ "go.mod": "module golang.org/x/tools\n",
+ "go.sum": "\n",
+ })
+ _ = tools.Commit(map[string]string{
+ "codereview.cfg": tc.config,
+ })
+
+ gerritClient := NewFakeGerrit(t, tools)
+
+ headMaster, err := gerritClient.ReadBranchHead(ctx, "tools", "master")
+ if err != nil {
+ t.Fatalf("ReadBranchHead should be able to get revision of master branch's head: %v", err)
+ }
+
+ configMaster, err := gerritClient.ReadFile(ctx, "tools", headMaster, "codereview.cfg")
+ if err != nil {
+ t.Fatalf("ReadFile should be able to read the codereview.cfg file from master branch head: %v", err)
+ }
+
+ semv, _ := parseSemver(tc.version)
+ releaseBranch := goplsReleaseBranchName(semv)
+ if _, err := gerritClient.CreateBranch(ctx, "tools", releaseBranch, gerrit.BranchInput{Revision: headMaster}); err != nil {
+ t.Fatalf("failed to create the branch %q: %v", releaseBranch, err)
+ }
+
+ headRelease, err := gerritClient.ReadBranchHead(ctx, "tools", releaseBranch)
+ if err != nil {
+ t.Fatalf("ReadBranchHead should be able to get revision of release branch's head: %v", err)
+ }
+
+ tasks := &ReleaseGoplsTasks{
+ Gerrit: gerritClient,
+ CloudBuild: NewFakeCloudBuild(t, gerritClient, "", nil, fakeGo),
+ }
+
+ _, err = tasks.updateCodeReviewConfig(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv, nil)
+ if err != nil {
+ t.Fatalf("updateCodeReviewConfig() returns error: %v", err)
+ }
+
+ // master branch's head commit should not change.
+ headMasterAfter, err := gerritClient.ReadBranchHead(ctx, "tools", "master")
+ if err != nil {
+ t.Fatalf("ReadBranchHead() should be able to get revision of master branch's head: %v", err)
+ }
+ if headMasterAfter != headMaster {
+ t.Errorf("updateCodeReviewConfig() should not change master branch's head, got = %s want = %s", headMasterAfter, headMaster)
+ }
+
+ // master branch's head codereview.cfg content should not change.
+ configMasterAfter, err := gerritClient.ReadFile(ctx, "tools", headMasterAfter, "codereview.cfg")
+ if err != nil {
+ t.Fatalf("ReadFile() should be able to read the codereview.cfg file from master branch head: %v", err)
+ }
+ if diff := cmp.Diff(configMaster, configMasterAfter); diff != "" {
+ t.Errorf("updateCodeReviewConfig() should not change codereview.cfg content in master branch (-want +got) \n %s", diff)
+ }
+
+ // verify the release branch commit have the expected behavior.
+ headReleaseAfter, err := gerritClient.ReadBranchHead(ctx, "tools", releaseBranch)
+ if err != nil {
+ t.Fatalf("ReadBranchHead() should be able to get revision of master branch's head: %v", err)
+ }
+ if tc.wantCommit && headReleaseAfter == headRelease {
+ t.Errorf("updateCodeReviewConfig() should have one commit to release branch, head of branch got = %s want = %s", headRelease, headReleaseAfter)
+ } else if !tc.wantCommit && headReleaseAfter != headRelease {
+ t.Errorf("updateCodeReviewConfig() should have not change release branch's head, got = %s want = %s", headRelease, headReleaseAfter)
+ }
+
+ // verify the release branch configreview.cfg have the expected content.
+ configReleaseAfter, err := gerritClient.ReadFile(ctx, "tools", headReleaseAfter, "codereview.cfg")
+ if err != nil {
+ t.Fatalf("ReadFile() should be able to read the codereview.cfg file from release branch head: %v", err)
+ }
+ if diff := cmp.Diff(tc.wantConfig, string(configReleaseAfter)); diff != "" {
+ t.Errorf("codereview.cfg mismatch (-want +got) \n %s", diff)
+ }
+ })
+ }
+}