blob: cd65e2c91d1072f54739c9296c43448983668243 [file] [log] [blame]
// Copyright 2023 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
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
"github.com/google/go-github/github"
"github.com/shurcooL/githubv4"
"golang.org/x/build/internal/workflow"
"golang.org/x/oauth2"
)
func TestCheckBlockers(t *testing.T) {
var errManualApproval = fmt.Errorf("manual approval is required")
for _, tc := range [...]struct {
name string
milestoneIssues map[int]map[string]bool
version string
kind ReleaseKind
want error
}{
{
name: "beta 1 with one hard blocker",
milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true}},
version: "go1.20beta1", kind: KindBeta,
want: errManualApproval,
},
{
name: "beta 1 with one blocker marked okay-after-beta1",
milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-beta1": true}},
version: "go1.20beta1", kind: KindBeta,
want: nil, // Want no error.
},
{
name: "beta 2 with one hard blocker and meaningless okay-after-beta1 label",
milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-beta1": true}},
version: "go1.20beta2", kind: KindBeta,
want: errManualApproval,
},
{
name: "RC 1 with one hard blocker",
milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true}},
version: "go1.20rc1", kind: KindRC,
want: errManualApproval,
},
{
name: "RC 1 with one blocker marked okay-after-rc1",
milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-rc1": true}},
version: "go1.20rc1", kind: KindRC,
want: nil, // Want no error.
},
{
name: "RC 2 with one hard blocker and meaningless okay-after-rc1 label",
milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-rc1": true}},
version: "go1.20rc2", kind: KindRC,
want: errManualApproval,
},
} {
t.Run(tc.name, func(t *testing.T) {
tasks := &MilestoneTasks{
Client: fakeGitHub{tc.milestoneIssues},
ApproveAction: func(*workflow.TaskContext) error { return errManualApproval },
}
ctx := &workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t: t}}
got := tasks.CheckBlockers(ctx, ReleaseMilestones{1, 2}, tc.version, tc.kind)
if got != tc.want {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
}
type fakeGitHub struct {
milestoneIssues map[int]map[string]bool
}
func (fakeGitHub) FetchMilestone(_ context.Context, owner, repo, name string, create bool) (int, error) {
return 0, nil
}
func (g fakeGitHub) FetchMilestoneIssues(_ context.Context, owner, repo string, milestoneID int) (map[int]map[string]bool, error) {
return g.milestoneIssues, nil
}
func (fakeGitHub) EditIssue(_ context.Context, owner string, repo string, number int, issue *github.IssueRequest) (*github.Issue, *github.Response, error) {
return nil, nil, nil
}
func (fakeGitHub) EditMilestone(_ context.Context, owner string, repo string, number int, milestone *github.Milestone) (*github.Milestone, *github.Response, error) {
return nil, nil, nil
}
func (fakeGitHub) PostComment(_ context.Context, _ githubv4.ID, _ string) error {
return fmt.Errorf("pretend that PostComment failed")
}
var (
flagRun = flag.Bool("run-destructive-milestones-test", false, "Run the milestone test. Requires repository owner and name flags, and GITHUB_TOKEN set in the environment.")
flagOwner = flag.String("milestones-github-owner", "", "Owner of testing repository")
flagRepo = flag.String("milestones-github-repo", "", "Testing repository")
)
func TestMilestones(t *testing.T) {
ctx := &workflow.TaskContext{
Context: context.Background(),
Logger: &testLogger{t, ""},
}
if !*flagRun {
t.Skip("Not enabled by flags")
}
if *flagOwner == "golang" {
t.Fatal("This is a destructive test! Don't run it on a real repository.")
}
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
)
httpClient := oauth2.NewClient(ctx, src)
client3 := github.NewClient(httpClient)
client4 := githubv4.NewClient(httpClient)
normal, blocker, err := resetRepo(ctx, client3)
if err != nil {
t.Fatal(err)
}
tasks := &MilestoneTasks{
Client: &GitHubClient{
V3: client3,
V4: client4,
},
RepoOwner: *flagOwner,
RepoName: *flagRepo,
ApproveAction: func(*workflow.TaskContext) error {
return fmt.Errorf("not approved")
},
}
milestones, err := tasks.FetchMilestones(ctx, "go1.20", KindMajor)
if err != nil {
t.Fatalf("GetMilestones: %v", err)
}
if err := tasks.PushIssues(ctx, milestones, "go1.20beta1", KindBeta); err != nil {
t.Fatalf("Pushing issues for beta release: %v", err)
}
pushedBlocker, _, err := client3.Issues.Get(ctx, *flagOwner, *flagRepo, blocker.GetNumber())
if err != nil {
t.Fatal(err)
}
if len(pushedBlocker.Labels) != 1 || *pushedBlocker.Labels[0].Name != "release-blocker" {
t.Errorf("release blocking issue has labels %#v, should only have release-blocker", pushedBlocker.Labels)
}
err = tasks.CheckBlockers(ctx, milestones, "go1.20", KindMajor)
if err == nil || !strings.Contains(err.Error(), "open release blockers") {
t.Fatalf("CheckBlockers with an open release blocker didn't give expected error: %v", err)
}
if _, _, err := client3.Issues.Edit(ctx, *flagOwner, *flagRepo, *blocker.Number, &github.IssueRequest{State: github.String("closed")}); err != nil {
t.Fatal(err)
}
if err := tasks.CheckBlockers(ctx, milestones, "go1.20", KindMajor); err != nil {
t.Fatalf("CheckBlockers with no release blockers failed: %v", err)
}
if err := tasks.PushIssues(ctx, milestones, "go1.20", KindMajor); err != nil {
t.Fatalf("PushIssues for major release failed: %v", err)
}
milestone, _, err := client3.Issues.GetMilestone(ctx, *flagOwner, *flagRepo, milestones.Current)
if err != nil {
t.Fatal(err)
}
if milestone.GetState() != "closed" {
t.Errorf("current milestone is %q, should be closed", milestone.GetState())
}
pushedNormal, _, err := client3.Issues.Get(ctx, *flagOwner, *flagRepo, normal.GetNumber())
if err != nil {
t.Fatal(err)
}
if pushedNormal.GetMilestone().GetNumber() != milestones.Next {
t.Errorf("issue %v is on milestone %v, should have been pushed to %v", normal.GetNumber(), pushedNormal.GetMilestone().GetNumber(), milestones.Next)
}
}
// resetRepo clears out the test repository and sets it to have:
// - a single milestone, Go1.20
// - a normal issue in that milestone
// - an okay-after-beta1 release blocking issue in that milestone, which is returned.
func resetRepo(ctx context.Context, client *github.Client) (normal, blocker *github.Issue, err error) {
milestones, _, err := client.Issues.ListMilestones(ctx, *flagOwner, *flagRepo, &github.MilestoneListOptions{State: "all"})
if err != nil {
return nil, nil, err
}
for _, m := range milestones {
if _, err := client.Issues.DeleteMilestone(ctx, *flagOwner, *flagRepo, *m.Number); err != nil {
return nil, nil, err
}
}
issues, _, err := client.Issues.ListByRepo(ctx, *flagOwner, *flagRepo, nil)
if err != nil {
return nil, nil, err
}
for _, i := range issues {
if _, _, err := client.Issues.Edit(ctx, *flagOwner, *flagRepo, *i.Number, &github.IssueRequest{
State: github.String("CLOSED"),
}); err != nil {
return nil, nil, err
}
}
currentMilestone, _, err := client.Issues.CreateMilestone(ctx, *flagOwner, *flagRepo, &github.Milestone{Title: github.String("Go1.20")})
if err != nil {
return nil, nil, err
}
normal, _, err = client.Issues.Create(ctx, *flagOwner, *flagRepo, &github.IssueRequest{
Title: github.String("Non-release-blocker"),
Milestone: currentMilestone.Number,
})
if err != nil {
return nil, nil, err
}
blocker, _, err = client.Issues.Create(ctx, *flagOwner, *flagRepo, &github.IssueRequest{
Title: github.String("Release-blocker"),
Milestone: currentMilestone.Number,
Labels: &[]string{"release-blocker", "okay-after-beta1"},
})
return normal, blocker, err
}