cmd/releasebot: add support for beta releases

Change-Id: I20b4c68dd2512dc76f5751c200d487784b68670b
Reviewed-on: https://go-review.googlesource.com/119537
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/cmd/releasebot/README.md b/cmd/releasebot/README.md
index 4e49997..e49f60c 100644
--- a/cmd/releasebot/README.md
+++ b/cmd/releasebot/README.md
@@ -4,10 +4,10 @@
 
 The release happens in two stages:
 
-* the `prepare` stage checks preconditions, makes the release commit and mails it for review;
-* the `release` stage runs after the release commit is merged, and it tags, builds and cleans up the release.
+* the `prepare` stage checks preconditions, and if needed makes the release commit and mails it for review;
+* the `release` stage runs after the release commit (if any) is merged, and it tags, builds and cleans up the release.
 
-At the moment only minor releases are supported.
+At the moment only minor and beta releases are supported.
 
 ## Permissions
 
diff --git a/cmd/releasebot/git.go b/cmd/releasebot/git.go
index e2474fd..f81e3fd 100644
--- a/cmd/releasebot/git.go
+++ b/cmd/releasebot/git.go
@@ -19,10 +19,6 @@
 // $HOME/go-releasebot-work/<release>/gitmirror,
 // to use as an object cache to speed future checkouts.
 func (w *Work) gitCheckout() {
-	shortRel := strings.ToLower(w.Milestone.Title)
-	shortRel = shortRel[:strings.LastIndex(shortRel, ".")]
-	w.ReleaseBranch = "release-branch." + shortRel
-
 	w.Dir = filepath.Join(os.Getenv("HOME"), "go-releasebot-work/"+strings.ToLower(w.Version))
 	w.log.Printf("working in %s\n", w.Dir)
 	if err := os.MkdirAll(w.Dir, 0777); err != nil {
@@ -65,6 +61,7 @@
 	out = r.runOut("git", "show", w.VersionCommit)
 	fmt.Printf("About to tag the following commit as %s:\n\n%s\n\nOk? (y/n) ", w.Version, out)
 	if dryRun {
+		fmt.Println("dry-run")
 		return
 	}
 	var response string
diff --git a/cmd/releasebot/github.go b/cmd/releasebot/github.go
index 7f13392..b9dc7db 100644
--- a/cmd/releasebot/github.go
+++ b/cmd/releasebot/github.go
@@ -73,8 +73,8 @@
 // for the given milestone.
 // If you change this function, releasebot will not be able to find an
 // existing tracking issue using the old name and will create a new one.
-func releaseStatusTitle(m *maintner.GitHubMilestone) string {
-	return "all: " + strings.Replace(m.Title, "Go", "Go ", -1) + " release status"
+func (w *Work) releaseStatusTitle() string {
+	return "all: " + strings.Replace(w.Version, "go", "Go ", -1) + " release status"
 }
 
 type tokenSource oauth2.Token
@@ -84,13 +84,13 @@
 }
 
 func (w *Work) findOrCreateReleaseIssue() {
-	w.log.Printf("Release status issue title: %q", releaseStatusTitle(w.Milestone))
+	w.log.Printf("Release status issue title: %q", w.releaseStatusTitle())
 	if dryRun {
 		return
 	}
 	if w.ReleaseIssue == 0 {
-		title := releaseStatusTitle(w.Milestone)
-		body := fmt.Sprintf("Issue tracking the %s release by releasebot.", w.Milestone.Title)
+		title := w.releaseStatusTitle()
+		body := fmt.Sprintf("Issue tracking the %s release by releasebot.", w.Version)
 		num, err := w.createGitHubIssue(title, body)
 		if err != nil {
 			w.log.Panic(err)
@@ -116,11 +116,14 @@
 	if dup != 0 {
 		return dup, nil
 	}
-	is, _, err := githubClient.Issues.ListByRepo(context.TODO(), "golang", "go", &github.IssueListByRepoOptions{
+	opts := &github.IssueListByRepoOptions{
 		State:       "all",
 		ListOptions: github.ListOptions{PerPage: 100},
-		Milestone:   strconv.Itoa(int(w.Milestone.Number)),
-	})
+	}
+	if !w.BetaRelease {
+		opts.Milestone = strconv.Itoa(int(w.Milestone.Number))
+	}
+	is, _, err := githubClient.Issues.ListByRepo(context.TODO(), "golang", "go", opts)
 	if err != nil {
 		return 0, err
 	}
@@ -130,11 +133,14 @@
 			return i.GetNumber(), nil
 		}
 	}
-	i, _, err := githubClient.Issues.Create(context.TODO(), "golang", "go", &github.IssueRequest{
-		Title:     github.String(title),
-		Body:      github.String(msg),
-		Milestone: github.Int(int(w.Milestone.Number)),
-	})
+	copts := &github.IssueRequest{
+		Title: github.String(title),
+		Body:  github.String(msg),
+	}
+	if !w.BetaRelease {
+		copts.Milestone = github.Int(int(w.Milestone.Number))
+	}
+	i, _, err := githubClient.Issues.Create(context.TODO(), "golang", "go", copts)
 	return i.GetNumber(), err
 }
 
@@ -144,7 +150,7 @@
 		if gi.Milestone == nil || gi.Milestone.Title != w.Milestone.Title {
 			return nil
 		}
-		if gi.Closed || gi.Title == releaseStatusTitle(w.Milestone) {
+		if gi.Closed || gi.Title == w.releaseStatusTitle() {
 			return nil
 		}
 		w.log.Printf("changing milestone of issue %d to %s", gi.Number, w.NextMilestone.Title)
diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go
index df08340..659f6b0 100644
--- a/cmd/releasebot/main.go
+++ b/cmd/releasebot/main.go
@@ -51,7 +51,7 @@
 }
 
 func usage() {
-	fmt.Fprintln(os.Stderr, "usage: releasebot -mode <release mode> [-dry-run] go1.8.5")
+	fmt.Fprintln(os.Stderr, "usage: releasebot -mode <release mode> [-dry-run] {go1.8.5|go1.10beta2}")
 	fmt.Fprintln(os.Stderr, "Release modes:")
 	fmt.Fprintln(os.Stderr)
 	for m := range releaseModes {
@@ -81,6 +81,20 @@
 
 	var wg sync.WaitGroup
 	for _, release := range flag.Args() {
+		if strings.Contains(release, "beta") {
+			w := &Work{
+				Prepare:     *modeFlag == "prepare",
+				Version:     release,
+				BetaRelease: true,
+			}
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				w.doRelease()
+			}()
+			continue
+		}
+
 		errFoundMilestone := errors.New("found milestone")
 		err := goRepo.ForeachMilestone(func(m *maintner.GitHubMilestone) error {
 			if strings.ToLower(m.Title) == release {
@@ -91,9 +105,9 @@
 				w := &Work{
 					Milestone:     m,
 					NextMilestone: nextM,
+					Prepare:       *modeFlag == "prepare",
+					Version:       release,
 				}
-				w.Prepare = *modeFlag == "prepare"
-				w.Version = release
 				wg.Add(1)
 				go func() {
 					defer wg.Done()
@@ -174,12 +188,11 @@
 	logBuf *bytes.Buffer
 	log    *log.Logger
 
-	Prepare bool // create the release commit and submit it for review
+	Prepare     bool // create the release commit and submit it for review
+	BetaRelease bool
 
-	Milestone     *maintner.GitHubMilestone
-	NextMilestone *maintner.GitHubMilestone // Next minor milestone
-	ReleaseIssue  int                       // Release status issue number
-	ReleaseBranch string
+	ReleaseIssue  int    // Release status issue number
+	ReleaseBranch string // "master" for beta releases
 	Dir           string // work directory ($HOME/go-releasebot-work/<release>)
 	Errors        []string
 	ReleaseBinary string
@@ -188,6 +201,10 @@
 
 	releaseMu   sync.Mutex
 	ReleaseInfo map[string]*ReleaseInfo // map and info protected by releaseMu
+
+	// Properties set for minor releases only.
+	Milestone     *maintner.GitHubMilestone
+	NextMilestone *maintner.GitHubMilestone // Next minor milestone
 }
 
 // ReleaseInfo describes a release build for a specific target.
@@ -283,30 +300,48 @@
 
 	w.log.Printf("starting")
 
+	if w.BetaRelease {
+		w.ReleaseBranch = "master"
+	} else {
+		shortRel := strings.ToLower(w.Milestone.Title)
+		shortRel = shortRel[:strings.LastIndex(shortRel, ".")]
+		w.ReleaseBranch = "release-branch." + shortRel
+	}
+
 	w.checkSpelling()
-	w.checkReleaseBlockers()
 	w.gitCheckout()
-	w.checkDocs()
+	if !w.BetaRelease {
+		w.checkReleaseBlockers()
+		w.checkDocs()
+	} else {
+		// TODO: go tool api -allow_new false
+	}
 	w.findOrCreateReleaseIssue()
 	if len(w.Errors) > 0 && !dryRun {
 		w.logError("**Found errors during release. Stopping!**")
 		return
 	}
 
-	if w.Prepare {
+	if w.Prepare && w.BetaRelease {
+		w.nextStepsBeta()
+	} else if w.Prepare {
 		changeID := w.writeVersion()
 		w.nextStepsPrepare(changeID)
 	} else {
-		w.checkVersion()
 		// TODO: check build.golang.org
+		if !w.BetaRelease {
+			w.checkVersion()
+		}
 		if len(w.Errors) > 0 {
 			w.logError("**Found errors during release. Stopping!**")
 			return
 		}
 		w.gitTagVersion()
 		w.buildReleases()
-		w.pushIssues()
-		w.closeMilestone()
+		if !w.BetaRelease {
+			w.pushIssues()
+			w.closeMilestone()
+		}
 		w.nextStepsRelease()
 	}
 }
@@ -349,6 +384,14 @@
 `, changeID)
 }
 
+func (w *Work) nextStepsBeta() {
+	w.log.Printf(`
+
+The release is ready. Run with mode=release to execute it.
+
+`)
+}
+
 func (w *Work) nextStepsRelease() {
 	w.log.Printf(`