internal/relui: add dl cl mailing to release workflows

Refactor the MailDLCL function to be on the VersionTasks struct, where
it kinda belongs but is definitely convenient, and wire it up to the
release workflows.

For minor releases, the current implementation will result in two CLs,
for better or worse.

For golang/go#51797.

Change-Id: Iefff452d1a1766a0690ea2a5c2decdd2a7e5f067
Reviewed-on: https://go-review.googlesource.com/c/build/+/411196
Auto-Submit: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go
index ff5a42f..ac4b5f5 100644
--- a/cmd/releasebot/main.go
+++ b/cmd/releasebot/main.go
@@ -29,6 +29,7 @@
 	"time"
 
 	"golang.org/x/build/buildenv"
+	"golang.org/x/build/gerrit"
 	"golang.org/x/build/internal/envutil"
 	"golang.org/x/build/internal/releasetargets"
 	"golang.org/x/build/internal/task"
@@ -169,16 +170,13 @@
 	}
 	versions := flag.Args()
 
-	extCfg := task.ExternalConfig{
-		DryRun: dryRun,
-	}
+	versionTasks := &task.VersionTasks{}
 	if !dryRun {
-		extCfg.GerritAPI.URL = gerritAPIURL
-		var err error
-		extCfg.GerritAPI.Auth, err = loadGerritAuth()
+		auth, err := loadGerritAuth()
 		if err != nil {
 			log.Fatalln("error loading Gerrit API credentials:", err)
 		}
+		versionTasks.Gerrit = &task.RealGerritClient{Client: gerrit.NewClient(gerritAPIURL, auth)}
 	}
 
 	fmt.Printf("About to create a golang.org/dl CL for the following Go versions:\n\n\t• %s\n\nOk? (Y/n) ", strings.Join(versions, "\n\t• "))
@@ -188,7 +186,7 @@
 	} else if resp != "Y" && resp != "y" {
 		log.Fatalln("stopped as requested")
 	}
-	changeURL, err := task.MailDLCL(&workflow.TaskContext{Context: context.Background(), Logger: log.Default()}, versions, extCfg)
+	changeID, err := versionTasks.MailDLCL(&workflow.TaskContext{Context: context.Background(), Logger: log.Default()}, versions, dryRun)
 	if err != nil {
 		log.Fatalf(`task.MailDLCL(ctx, %#v, extCfg) failed:
 
@@ -205,7 +203,7 @@
 
 Discuss with the secondary release coordinator as needed.`, versions, err)
 	}
-	fmt.Printf("\nPlease review and submit %s\nand then refer to the playbook for the next steps.\n\n", changeURL)
+	fmt.Printf("\nPlease review and submit %s\nand then refer to the playbook for the next steps.\n\n", task.ChangeLink(changeID))
 }
 
 // postTweet parses command-line arguments for the tweet-* modes,
diff --git a/cmd/relui/main.go b/cmd/relui/main.go
index 395eb26..4f0136c 100644
--- a/cmd/relui/main.go
+++ b/cmd/relui/main.go
@@ -88,17 +88,20 @@
 			CSSClass: *siteHeaderCSS,
 		}
 		extCfg = task.ExternalConfig{
-			GerritAPI: struct {
-				URL  string
-				Auth gerrit.Auth
-			}{"https://go-review.googlesource.com", gerrit.BasicAuth("git-gobot.golang.org", *gerritAPIFlag)},
 			// TODO(go.dev/issue/51150): When twitter client creation is factored out from task package, update code here.
 			TwitterAPI: twitterAPI,
 		}
 	)
 
+	gerritClient := &task.RealGerritClient{
+		Client: gerrit.NewClient("https://go-review.googlesource.com", gerrit.BasicAuth("git-gobot.golang.org", *gerritAPIFlag)),
+	}
+	versionTasks := &task.VersionTasks{
+		Gerrit:    gerritClient,
+		GoProject: "go",
+	}
 	dh := relui.NewDefinitionHolder()
-	relui.RegisterMailDLCLDefinition(dh, extCfg)
+	relui.RegisterMailDLCLDefinition(dh, versionTasks)
 	relui.RegisterTweetDefinitions(dh, extCfg)
 	userPassAuth := buildlet.UserPass{
 		Username: "user-relui",
@@ -137,12 +140,6 @@
 		RepoOwner: "golang",
 		RepoName:  "go",
 	}
-	versionTasks := &task.VersionTasks{
-		Gerrit: &task.RealGerritClient{
-			Client: gerrit.NewClient("https://go-review.googlesource.com", gerrit.BasicAuth("git-gobot.golang.org", *gerritAPIFlag)),
-		},
-		Project: "go",
-	}
 
 	relui.RegisterReleaseWorkflows(dh, buildTasks, milestoneTasks, versionTasks)
 	db, err := pgxpool.Connect(ctx, *pgConnect)
diff --git a/internal/relui/buildrelease_test.go b/internal/relui/buildrelease_test.go
index fcf3714..0c2034b 100644
--- a/internal/relui/buildrelease_test.go
+++ b/internal/relui/buildrelease_test.go
@@ -89,8 +89,8 @@
 
 	gerrit := &fakeGerrit{createdTags: map[string]string{}}
 	versionTasks := &task.VersionTasks{
-		Gerrit:  gerrit,
-		Project: "go",
+		Gerrit:    gerrit,
+		GoProject: "go",
 	}
 	milestoneTasks := &task.MilestoneTasks{
 		Client:    &fakeGitHub{},
@@ -171,9 +171,9 @@
 		Kind: "installer",
 	}, "I'm a .pkg!\n")
 
-	wantCLs := 1 // VERSION
+	wantCLs := 2 // VERSION bump, DL
 	if kind == task.KindBeta {
-		wantCLs = 0
+		wantCLs--
 	}
 	if gerrit.changesCreated != wantCLs {
 		t.Errorf("workflow sent %v changes to Gerrit, want %v", gerrit.changesCreated, wantCLs)
diff --git a/internal/relui/workflows.go b/internal/relui/workflows.go
index a1cd987..1597d5b 100644
--- a/internal/relui/workflows.go
+++ b/internal/relui/workflows.go
@@ -70,7 +70,7 @@
 
 // RegisterMailDLCLDefinition registers a workflow definition for mailing a golang.org/dl CL
 // onto h, using e for the external service configuration.
-func RegisterMailDLCLDefinition(h *DefinitionHolder, e task.ExternalConfig) {
+func RegisterMailDLCLDefinition(h *DefinitionHolder, tasks *task.VersionTasks) {
 	versions := workflow.Parameter{
 		Name:          "Versions",
 		ParameterType: workflow.SliceShort,
@@ -87,7 +87,11 @@
 
 	wd := workflow.New()
 	wd.Output("ChangeURL", wd.Task("mail-dl-cl", func(ctx *workflow.TaskContext, versions []string) (string, error) {
-		return task.MailDLCL(ctx, versions, e)
+		id, err := tasks.MailDLCL(ctx, versions, false)
+		if err != nil {
+			return "", err
+		}
+		return task.ChangeLink(id), nil
 	}, wd.Parameter(versions)))
 	h.RegisterDefinition("mail-dl-cl", wd)
 }
@@ -237,7 +241,9 @@
 	nextVersion := wd.Task("Get next version", version.GetNextVersion, kindVal)
 	milestones := wd.Task("Pick milestones", milestone.FetchMilestones, nextVersion, kindVal)
 	checked := wd.Action("Check blocking issues", milestone.CheckBlockers, milestones, nextVersion, kindVal)
-	// TODO(heschi): add mail-dl-cl here.
+	dlcl := wd.Task("Mail DL CL", version.MailDLCL, wd.Slice([]workflow.Value{nextVersion}), wd.Constant(false))
+	dlclCommit := wd.Task("Wait for DL CL", version.AwaitCL, dlcl)
+	wd.Output("Download CL submitted", dlclCommit)
 
 	// Build, test, and sign release.
 	signedAndTestedArtifacts, err := build.addBuildTasks(wd, "go1.19", nextVersion, branchVal, skipTests, checked)
diff --git a/internal/task/dlcl.go b/internal/task/dlcl.go
index e70df6c..a8b589d 100644
--- a/internal/task/dlcl.go
+++ b/internal/task/dlcl.go
@@ -26,8 +26,8 @@
 //   - "go1.18" for a major Go release
 //   - "go1.18beta1" or "go1.18rc1" for a pre-release
 //
-// On success, the URL of the change is returned, like "https://go.dev/cl/123".
-func MailDLCL(ctx *workflow.TaskContext, versions []string, e ExternalConfig) (changeURL string, _ error) {
+// On success, the ID of the change is returned, like "dl~1234".
+func (t *VersionTasks) MailDLCL(ctx *workflow.TaskContext, versions []string, dryRun bool) (changeID string, _ error) {
 	if len(versions) < 1 || len(versions) > 2 {
 		return "", fmt.Errorf("got %d Go versions, want 1 or 2", len(versions))
 	}
@@ -70,20 +70,15 @@
 	}
 
 	// Create a Gerrit CL using the Gerrit API.
-	if e.DryRun {
+	if dryRun {
 		return "(dry-run)", nil
 	}
-	cl := gerrit.NewClient(e.GerritAPI.URL, e.GerritAPI.Auth)
 	changeInput := gerrit.ChangeInput{
 		Project: "dl",
 		Subject: "dl: add " + strings.Join(versions, " and "),
 		Branch:  "master",
 	}
-	changeID, err := (&RealGerritClient{Client: cl}).CreateAutoSubmitChange(ctx, changeInput, files)
-	if err != nil {
-		return "", err
-	}
-	return changeLink(changeID), nil
+	return t.Gerrit.CreateAutoSubmitChange(ctx, changeInput, files)
 }
 
 func verifyGoVersions(versions ...string) error {
diff --git a/internal/task/dlcl_test.go b/internal/task/dlcl_test.go
index 077f6b8..cd70781 100644
--- a/internal/task/dlcl_test.go
+++ b/internal/task/dlcl_test.go
@@ -169,12 +169,13 @@
 			// doesn't actually try to mail a dl CL, but capture its log.
 			var buf bytes.Buffer
 			ctx := &workflow.TaskContext{Context: context.Background(), Logger: fmtWriter{&buf}}
-			changeURL, err := MailDLCL(ctx, tc.in, ExternalConfig{DryRun: true})
+			tasks := &VersionTasks{Gerrit: nil}
+			changeID, err := tasks.MailDLCL(ctx, tc.in, true)
 			if err != nil {
 				t.Fatal("got a non-nil error:", err)
 			}
-			if got, want := changeURL, "(dry-run)"; got != want {
-				t.Errorf("unexpected changeURL: got = %q, want %q", got, want)
+			if got, want := changeID, "(dry-run)"; got != want {
+				t.Errorf("unexpected changeID: got = %q, want %q", got, want)
 			}
 			if got, want := buf.String(), tc.wantLog; got != want {
 				t.Errorf("unexpected log:\ngot:\n%s\nwant:\n%s", got, want)
diff --git a/internal/task/gerrit.go b/internal/task/gerrit.go
index 16bf0d2..8b9355a 100644
--- a/internal/task/gerrit.go
+++ b/internal/task/gerrit.go
@@ -72,7 +72,7 @@
 		}
 		for _, approver := range detail.Labels["TryBot-Result"].All {
 			if approver.Value < 0 {
-				return "", fmt.Errorf("trybots failed on %v", changeLink(changeID))
+				return "", fmt.Errorf("trybots failed on %v", ChangeLink(changeID))
 			}
 		}
 
@@ -116,9 +116,9 @@
 	return tagNames, nil
 }
 
-// changeLink returns a link to the review page for the CL with the specified
+// ChangeLink returns a link to the review page for the CL with the specified
 // change ID. The change ID must be in the project~cl# form.
-func changeLink(changeID string) string {
+func ChangeLink(changeID string) string {
 	parts := strings.SplitN(changeID, "~", 3)
 	if len(parts) != 2 {
 		return fmt.Sprintf("(unparseable change ID %q)", changeID)
diff --git a/internal/task/task.go b/internal/task/task.go
index 5e0b7c2..9a3b5ff 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -6,7 +6,6 @@
 package task
 
 import (
-	"golang.org/x/build/gerrit"
 	"golang.org/x/build/internal/secret"
 )
 
@@ -19,13 +18,6 @@
 	// what would be done, without changing anything.
 	DryRun bool
 
-	// GerritAPI specifies a Gerrit API server where
-	// Go project CLs can be mailed.
-	GerritAPI struct {
-		URL  string // Gerrit API URL. For example, "https://go-review.googlesource.com".
-		Auth gerrit.Auth
-	}
-
 	// TwitterAPI holds Twitter API credentials that
 	// can be used to post a tweet.
 	TwitterAPI secret.TwitterCredentials
diff --git a/internal/task/version.go b/internal/task/version.go
index bf1c548..8e7c5fe 100644
--- a/internal/task/version.go
+++ b/internal/task/version.go
@@ -11,13 +11,13 @@
 
 // VersionTasks contains tasks related to versioning the release.
 type VersionTasks struct {
-	Gerrit  GerritClient
-	Project string
+	Gerrit    GerritClient
+	GoProject string
 }
 
 // GetNextVersion returns the next for the given type of release.
 func (t *VersionTasks) GetNextVersion(ctx *workflow.TaskContext, kind ReleaseKind) (string, error) {
-	tags, err := t.Gerrit.ListTags(ctx, t.Project)
+	tags, err := t.Gerrit.ListTags(ctx, t.GoProject)
 	if err != nil {
 		return "", err
 	}
@@ -76,7 +76,7 @@
 // CreateAutoSubmitVersionCL mails an auto-submit change to update VERSION on branch.
 func (t *VersionTasks) CreateAutoSubmitVersionCL(ctx *workflow.TaskContext, branch, version string) (string, error) {
 	return t.Gerrit.CreateAutoSubmitChange(ctx, gerrit.ChangeInput{
-		Project: t.Project,
+		Project: t.GoProject,
 		Branch:  branch,
 		Subject: fmt.Sprintf("[%v] %v", branch, version),
 	}, map[string]string{
@@ -86,11 +86,11 @@
 
 // AwaitCL waits for the specified CL to be submitted.
 func (t *VersionTasks) AwaitCL(ctx *workflow.TaskContext, changeID string) (string, error) {
-	ctx.Printf("Awaiting review/submit of %v", changeLink(changeID))
+	ctx.Printf("Awaiting review/submit of %v", ChangeLink(changeID))
 	return t.Gerrit.AwaitSubmit(ctx, changeID)
 }
 
 // TagRelease tags commit as version.
 func (t *VersionTasks) TagRelease(ctx *workflow.TaskContext, version, commit string) error {
-	return t.Gerrit.Tag(ctx, t.Project, version, commit)
+	return t.Gerrit.Tag(ctx, t.GoProject, version, commit)
 }
diff --git a/internal/task/version_test.go b/internal/task/version_test.go
index 9652799..5602520 100644
--- a/internal/task/version_test.go
+++ b/internal/task/version_test.go
@@ -20,8 +20,8 @@
 
 	cl := gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookiesAuth())
 	tasks := &VersionTasks{
-		Gerrit:  &RealGerritClient{Client: cl},
-		Project: "go",
+		Gerrit:    &RealGerritClient{Client: cl},
+		GoProject: "go",
 	}
 	ctx := &workflow.TaskContext{
 		Context: context.Background(),
@@ -49,7 +49,7 @@
 				"go1.5beta1", "go1.5rc1",
 			},
 		},
-		Project: "go",
+		GoProject: "go",
 	}
 	ctx := &workflow.TaskContext{
 		Context: context.Background(),
@@ -90,8 +90,8 @@
 	}
 	cl := gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookiesAuth())
 	tasks := &VersionTasks{
-		Gerrit:  &RealGerritClient{Client: cl},
-		Project: "scratch",
+		Gerrit:    &RealGerritClient{Client: cl},
+		GoProject: "scratch",
 	}
 	ctx := &workflow.TaskContext{
 		Context: context.Background(),