From 5747e451b3574a29f914b80cb5ffa5eaba9fabec Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Mon, 4 Oct 2021 16:20:31 -0400 Subject: [PATCH] cmd/releasebot: create GitHub milestones for next release The current release process relies on humans to create GitHub milestones for future Go releases. releasebot includes automated checks that detect when that step is forgotten. CL 294249 included one of those checks, and its commit message mentioned: (Future release process improvements may include automatically making the milestone. That is better suited to be in scope of golang/go#40279.) This is the release process improvement that automates making milestones. For golang/go#40279. Change-Id: I8e02aff6714a5cf2de4ee4bcfe98aaf68abb0cd4 Reviewed-on: https://go-review.googlesource.com/c/build/+/354758 Run-TryBot: Dmitri Shuralyov TryBot-Result: Go Bot Reviewed-by: Heschi Kreinick Reviewed-by: Alexander Rakoczy Trust: Dmitri Shuralyov --- cmd/releasebot/github.go | 78 ++++++++++++++++++++++++++++++++++++++-- cmd/releasebot/main.go | 25 +------------ 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/cmd/releasebot/github.go b/cmd/releasebot/github.go index 4cd2f9af..cbced620 100644 --- a/cmd/releasebot/github.go +++ b/cmd/releasebot/github.go @@ -149,16 +149,42 @@ func (w *Work) createGitHubIssue(title, msg string) (int, error) { return i.GetNumber(), err } -// pushIssues moves open issues to the milestone of the next release of the same kind. +// pushIssues moves open issues to the milestone of the next release of the same kind, +// creating the milestone if it doesn't already exist. // For major releases, it's the milestone of the next major release (e.g., 1.14 → 1.15). // For minor releases, it's the milestone of the next minor release (e.g., 1.14.1 → 1.14.2). // For other release types, it does nothing. +// +// For major releases, it also creates the first minor release milestone if it doesn't already exist. func (w *Work) pushIssues() { if w.BetaRelease || w.RCRelease { // Nothing to do. return } + // Get the milestone for the next release. + var nextMilestone *github.Milestone + nextV, err := nextVersion(w.Version) + if err != nil { + w.logError("error determining next version: %v", err) + return + } + nextMilestone, err = w.findOrCreateMilestone(nextV) + if err != nil { + w.logError("error finding or creating %s, the next GitHub milestone after release %s: %v", nextV, w.Version, err) + return + } + + // For major releases (go1.X), also create the first minor release milestone (go1.X.1). See issue 44404. + if strings.Count(w.Version, ".") == 1 { + firstMinor := w.Version + ".1" + _, err := w.findOrCreateMilestone(firstMinor) + if err != nil { + // Log this error, but continue executing the rest of the task. + w.logError("error finding or creating %s, the first minor release GitHub milestone after major release %s: %v", firstMinor, w.Version, err) + } + } + if err := goRepo.ForeachIssue(func(gi *maintner.GitHubIssue) error { if gi.Milestone == nil || gi.Milestone.ID != w.Milestone.ID { return nil @@ -170,12 +196,12 @@ func (w *Work) pushIssues() { if gi.Closed && !w.Security { return nil } - w.log.Printf("changing milestone of issue %d to %s", gi.Number, w.NextMilestone.Title) + w.log.Printf("changing milestone of issue %d to %s", gi.Number, nextMilestone.GetTitle()) if dryRun { return nil } _, _, err := githubClient.Issues.Edit(context.TODO(), projectOwner, projectRepo, int(gi.Number), &github.IssueRequest{ - Milestone: github.Int(int(w.NextMilestone.Number)), + Milestone: github.Int(nextMilestone.GetNumber()), }) if err != nil { return fmt.Errorf("#%d: %s", gi.Number, err) @@ -187,6 +213,52 @@ func (w *Work) pushIssues() { } } +// findOrCreateMilestone finds or creates a GitHub milestone corresponding +// to the specified Go version. This is done via the GitHub API, using githubClient. +// If the milestone exists but isn't open, an error is returned. +func (w *Work) findOrCreateMilestone(version string) (*github.Milestone, error) { + // Look for an existing open milestone corresponding to version, + // and return it if found. + for opt := (&github.MilestoneListOptions{ListOptions: github.ListOptions{PerPage: 100}}); ; { + ms, resp, err := githubClient.Issues.ListMilestones(context.Background(), projectOwner, projectRepo, opt) + if err != nil { + return nil, err + } + for _, m := range ms { + if strings.ToLower(m.GetTitle()) == version { + // Found an existing milestone. + return m, nil + } + } + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + // Create a new milestone. + // For historical reasons, Go milestone titles use a capital "Go1.n" format, + // in contrast to go versions which are like "go1.n". Do the same here. + title := strings.Replace(version, "go", "Go", 1) + w.log.Printf("creating milestone titled %q", title) + if dryRun { + return &github.Milestone{Title: github.String(title)}, nil + } + m, _, err := githubClient.Issues.CreateMilestone(context.Background(), projectOwner, projectRepo, &github.Milestone{ + Title: github.String(title), + }) + if e := (*github.ErrorResponse)(nil); errors.As(err, &e) && e.Response != nil && e.Response.StatusCode == http.StatusUnprocessableEntity && len(e.Errors) == 1 && e.Errors[0].Code == "already_exists" { + // We'll run into an already_exists error here if the milestone exists, + // but it wasn't found in the loop above because the milestone isn't open. + // That shouldn't happen under normal circumstances, so if it does, + // let humans figure out how to best deal with it. + return nil, errors.New("a closed milestone with the same title already exists") + } else if err != nil { + return nil, err + } + return m, nil +} + // closeMilestone closes the milestone for the current release. func (w *Work) closeMilestone() { w.log.Printf("closing milestone %s", w.Milestone.Title) diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go index 64e790a6..00aa4d6c 100644 --- a/cmd/releasebot/main.go +++ b/cmd/releasebot/main.go @@ -166,30 +166,12 @@ func main() { // Select release targets for this Go version. w.ReleaseTargets = matchTargets(w.Version) - // Find milestones. + // Find milestone. var err error w.Milestone, err = getMilestone(w.Version) if err != nil { log.Fatalf("cannot find the GitHub milestone for release %s: %v", w.Version, err) } - if !w.BetaRelease && !w.RCRelease { - nextV, err := nextVersion(w.Version) - if err != nil { - log.Fatalln("nextVersion:", err) - } - w.NextMilestone, err = getMilestone(nextV) - if err != nil { - log.Fatalf("cannot find %s, the next GitHub milestone after release %s: %v", nextV, w.Version, err) - } - } - // For major releases (go1.X), also check the "create first minor release milestone" - // step in the release process wasn't accidentally missed. See issue 44404. - if !w.BetaRelease && !w.RCRelease && strings.Count(w.Version, ".") == 1 { - firstMinor := w.Version + ".1" - if _, err := getMilestone(firstMinor); err != nil { - log.Fatalf("cannot find %s, the first minor release GitHub milestone after major release %s: %v", firstMinor, w.Version, err) - } - } w.doRelease() } @@ -325,11 +307,6 @@ type Work struct { ReleaseInfo map[string]*ReleaseInfo // map and info protected by releaseMu Milestone *maintner.GitHubMilestone // Milestone for the current release. - // NextMilestone is the milestone of the next release of the same kind. - // For major releases, it's the milestone of the next major release (e.g., 1.14 → 1.15). - // For minor releases, it's the milestone of the next minor release (e.g., 1.14.1 → 1.14.2). - // For other release types, it's unset. - NextMilestone *maintner.GitHubMilestone } // ReleaseInfo describes a release build for a specific target.