internal/task, cmd/releasebot: add mail-dl-cl task to releasebot CLI
This change adds a release task that exercises the ability to create
a Gerrit CL via the Gerrit API, which will play a part in release
automation.
The task will eventually be exposed via relui's web UI when it is ready,
and it has a signature we expect to be able to use. To be able to use it
sooner, it's exposed via releasebot's CLI.
For golang.org/issue/38075.
Change-Id: I05ff078d7fcc25b1c2ac7001b094f3a3fee6b3a1
Reviewed-on: https://go-review.googlesource.com/c/build/+/350631
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
Trust: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go
index 647e4f9..e0718f4 100644
--- a/cmd/releasebot/main.go
+++ b/cmd/releasebot/main.go
@@ -8,6 +8,7 @@
import (
"bytes"
+ "context"
"crypto/sha1"
"crypto/sha256"
"errors"
@@ -27,6 +28,7 @@
"time"
"golang.org/x/build/buildenv"
+ "golang.org/x/build/internal/task"
"golang.org/x/build/maintner"
)
@@ -66,12 +68,13 @@
}
var releaseModes = map[string]bool{
- "prepare": true,
- "release": true,
+ "prepare": true,
+ "release": true,
+ "mail-dl-cl": true,
}
func usage() {
- fmt.Fprintln(os.Stderr, "usage: releasebot -mode {prepare|release} [-security] [-dry-run] {go1.8.5|go1.10beta2|go1.11rc1}")
+ fmt.Fprintln(os.Stderr, "usage: releasebot -mode {prepare|release|mail-dl-cl} [-security] [-dry-run] {go1.8.5|go1.10beta2|go1.11rc1}")
flag.PrintDefaults()
os.Exit(2)
}
@@ -91,8 +94,14 @@
security := flag.Bool("security", false, "cut a security release from the internal Gerrit")
flag.Usage = usage
flag.Parse()
- if *modeFlag == "" || !releaseModes[*modeFlag] || flag.NArg() != 1 {
- fmt.Fprintln(os.Stderr, "need to provide a valid mode and a release name")
+ if !releaseModes[*modeFlag] {
+ fmt.Fprintln(os.Stderr, "need to provide a valid mode")
+ usage()
+ } else if *modeFlag == "mail-dl-cl" {
+ mailDLCL()
+ return
+ } else if flag.NArg() != 1 {
+ fmt.Fprintln(os.Stderr, "need to provide a release name")
usage()
}
releaseVersion := flag.Arg(0)
@@ -184,6 +193,46 @@
w.doRelease()
}
+// mailDLCL parses command-line arguments for the mail-dl-cl mode,
+// and runs it.
+func mailDLCL() {
+ if flag.NArg() != 1 && flag.NArg() != 2 {
+ fmt.Fprintln(os.Stderr, "need to provide 1 or 2 versions")
+ usage()
+ }
+ versions := flag.Args()
+
+ 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• "))
+ if dryRun {
+ fmt.Println("dry-run")
+ return
+ }
+ var response string
+ _, err := fmt.Scanln(&response)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if response != "Y" && response != "y" {
+ log.Fatalln("stopped as requested")
+ }
+ changeURL, err := task.MailDLCL(context.Background(), versions)
+ if err != nil {
+ log.Fatalf(`task.MailDLCL(ctx, %#v) failed: %v
+
+If it's neccessary to perform it manually as a workaround,
+consider the following steps:
+
+ git clone https://go.googlesource.com/dl && cd dl
+ go run ./internal/genv goX.Y.Z goX.A.B
+ git add .
+ git commit -m "dl: add goX.Y.Z and goX.A.B"
+ git codereview mail -trybot -trust
+
+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)
+}
+
// checkForGitCodereview exits the program if git-codereview is not installed
// in the user's path.
func checkForGitCodereview() {
diff --git a/internal/task/dlcl.go b/internal/task/dlcl.go
new file mode 100644
index 0000000..2b50d42
--- /dev/null
+++ b/internal/task/dlcl.go
@@ -0,0 +1,164 @@
+// Copyright 2021 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 implements tasks involved in making a Go release.
+package task
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "go/format"
+ "path"
+ "regexp"
+ "strings"
+ "text/template"
+ "time"
+
+ "golang.org/x/build/buildenv"
+ "golang.org/x/build/gerrit"
+ "golang.org/x/build/internal/secret"
+)
+
+// MailDLCL mails a golang.org/dl CL that adds commands for the
+// specified Go versions. It accepts one or two versions only.
+//
+// The versions must use the same format as Go tags. For example:
+// • "go1.17.2" and "go1.16.9" for a minor Go release
+// • "go1.18" for a major Go release
+// • "go1.18beta1" or "go1.18rc1" for a pre-release
+//
+// Credentials are fetched from Secret Manager.
+// On success, the URL of the change is returned, like "https://golang.org/cl/123".
+func MailDLCL(ctx context.Context, versions []string) (changeURL string, _ error) {
+ if len(versions) < 1 || len(versions) > 2 {
+ return "", fmt.Errorf("got %d Go versions, want 1 or 2", len(versions))
+ }
+ for _, ver := range versions {
+ if ver != strings.ToLower(ver) {
+ return "", fmt.Errorf("version %q is not lowercase", ver)
+ } else if strings.Contains(ver, " ") {
+ return "", fmt.Errorf("version %q contains a space", ver)
+ } else if !strings.HasPrefix(ver, "go") {
+ return "", fmt.Errorf("version %q doesn't have the 'go' prefix", ver)
+ }
+ }
+
+ var files = make(map[string]string) // Map key is relative path, and map value is file content.
+
+ // Generate main.go files for versions from the template.
+ for _, ver := range versions {
+ var buf bytes.Buffer
+ versionNoPatch, err := versionNoPatch(ver)
+ if err != nil {
+ return "", err
+ }
+ if err := dlTmpl.Execute(&buf, struct {
+ Year int
+ Version string // "go1.5.3rc2"
+ VersionNoPatch string // "go1.5"
+ CapitalSpaceVersion string // "Go 1.5"
+ DocHost string // "golang.org" or "tip.golang.org" for rc/beta
+ }{
+ Year: time.Now().UTC().Year(),
+ Version: ver,
+ VersionNoPatch: versionNoPatch,
+ DocHost: docHost(ver),
+ CapitalSpaceVersion: strings.Replace(ver, "go", "Go ", 1),
+ }); err != nil {
+ return "", fmt.Errorf("dlTmpl.Execute: %v", err)
+ }
+ gofmted, err := format.Source(buf.Bytes())
+ if err != nil {
+ return "", fmt.Errorf("could not gofmt: %v", err)
+ }
+ files[path.Join(ver, "main.go")] = string(gofmted)
+ }
+
+ // Create a Gerrit CL using the Gerrit API.
+ gobot, err := gobot()
+ if err != nil {
+ return "", err
+ }
+ ci, err := gobot.CreateChange(ctx, gerrit.ChangeInput{
+ Project: "dl",
+ Subject: "dl: add " + strings.Join(versions, " and "),
+ Branch: "master",
+ })
+ if err != nil {
+ return "", err
+ }
+ changeID := fmt.Sprintf("%s~%d", ci.Project, ci.ChangeNumber)
+ for path, content := range files {
+ err := gobot.ChangeFileContentInChangeEdit(ctx, changeID, path, content)
+ if err != nil {
+ return "", err
+ }
+ }
+ err = gobot.PublishChangeEdit(ctx, changeID)
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("https://golang.org/cl/%d", ci.ChangeNumber), nil
+}
+
+func docHost(ver string) string {
+ if strings.Contains(ver, "rc") || strings.Contains(ver, "beta") {
+ return "tip.golang.org"
+ }
+ return "golang.org"
+}
+
+func versionNoPatch(ver string) (string, error) {
+ rx := regexp.MustCompile(`^(go\d+\.\d+)($|[\.]?\d*)($|rc|beta|\.)`)
+ m := rx.FindStringSubmatch(ver)
+ if m == nil {
+ return "", fmt.Errorf("unrecognized version %q", ver)
+ }
+ if m[2] != "" {
+ return "devel/release.html#" + m[1] + ".minor", nil
+ }
+ return m[1], nil
+}
+
+var dlTmpl = template.Must(template.New("").Parse(`// Copyright {{.Year}} 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.
+
+// The {{.Version}} command runs the go command from {{.CapitalSpaceVersion}}.
+//
+// To install, run:
+//
+// $ go install golang.org/dl/{{.Version}}@latest
+// $ {{.Version}} download
+//
+// And then use the {{.Version}} command as if it were your normal go
+// command.
+//
+// See the release notes at https://{{.DocHost}}/doc/{{.VersionNoPatch}}
+//
+// File bugs at https://golang.org/issues/new
+package main
+
+import "golang.org/dl/internal/version"
+
+func main() {
+ version.Run("{{.Version}}")
+}
+`))
+
+// gobot creates an authenticated Gerrit API client
+// that uses the gobot@golang.org Gerrit account.
+func gobot() (*gerrit.Client, error) {
+ sc, err := secret.NewClientInProject(buildenv.Production.ProjectName)
+ if err != nil {
+ return nil, err
+ }
+ defer sc.Close()
+ token, err := sc.Retrieve(context.Background(), secret.NameGobotPassword)
+ if err != nil {
+ return nil, err
+ }
+ return gerrit.NewClient("https://go-review.googlesource.com", gerrit.BasicAuth("git-gobot.golang.org", token)), nil
+}