golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/milestones_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package task
     6  
     7  import (
     8  	"context"
     9  	"flag"
    10  	"fmt"
    11  	"os"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/google/go-github/github"
    16  	"github.com/shurcooL/githubv4"
    17  	"golang.org/x/build/internal/workflow"
    18  	"golang.org/x/oauth2"
    19  )
    20  
    21  func TestCheckBlockers(t *testing.T) {
    22  	var errManualApproval = fmt.Errorf("manual approval is required")
    23  	for _, tc := range [...]struct {
    24  		name            string
    25  		milestoneIssues map[int]map[string]bool
    26  		version         string
    27  		kind            ReleaseKind
    28  		want            error
    29  	}{
    30  		{
    31  			name:            "beta 1 with one hard blocker",
    32  			milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true}},
    33  			version:         "go1.20beta1", kind: KindBeta,
    34  			want: errManualApproval,
    35  		},
    36  		{
    37  			name:            "beta 1 with one blocker marked okay-after-beta1",
    38  			milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-beta1": true}},
    39  			version:         "go1.20beta1", kind: KindBeta,
    40  			want: nil, // Want no error.
    41  		},
    42  		{
    43  			name:            "beta 2 with one hard blocker and meaningless okay-after-beta1 label",
    44  			milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-beta1": true}},
    45  			version:         "go1.20beta2", kind: KindBeta,
    46  			want: errManualApproval,
    47  		},
    48  		{
    49  			name:            "RC 1 with one hard blocker",
    50  			milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true}},
    51  			version:         "go1.20rc1", kind: KindRC,
    52  			want: errManualApproval,
    53  		},
    54  		{
    55  			name:            "RC 1 with one blocker marked okay-after-rc1",
    56  			milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-rc1": true}},
    57  			version:         "go1.20rc1", kind: KindRC,
    58  			want: nil, // Want no error.
    59  		},
    60  		{
    61  			name:            "RC 2 with one hard blocker and meaningless okay-after-rc1 label",
    62  			milestoneIssues: map[int]map[string]bool{123: {"release-blocker": true, "okay-after-rc1": true}},
    63  			version:         "go1.20rc2", kind: KindRC,
    64  			want: errManualApproval,
    65  		},
    66  	} {
    67  		t.Run(tc.name, func(t *testing.T) {
    68  			tasks := &MilestoneTasks{
    69  				Client:        fakeGitHub{tc.milestoneIssues},
    70  				ApproveAction: func(*workflow.TaskContext) error { return errManualApproval },
    71  			}
    72  			ctx := &workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t: t}}
    73  			got := tasks.CheckBlockers(ctx, ReleaseMilestones{1, 2}, tc.version, tc.kind)
    74  			if got != tc.want {
    75  				t.Errorf("got %v, want %v", got, tc.want)
    76  			}
    77  		})
    78  	}
    79  }
    80  
    81  type fakeGitHub struct {
    82  	milestoneIssues map[int]map[string]bool
    83  }
    84  
    85  func (fakeGitHub) FetchMilestone(_ context.Context, owner, repo, name string, create bool) (int, error) {
    86  	return 0, nil
    87  }
    88  
    89  func (g fakeGitHub) FetchMilestoneIssues(_ context.Context, owner, repo string, milestoneID int) (map[int]map[string]bool, error) {
    90  	return g.milestoneIssues, nil
    91  }
    92  
    93  func (fakeGitHub) EditIssue(_ context.Context, owner string, repo string, number int, issue *github.IssueRequest) (*github.Issue, *github.Response, error) {
    94  	return nil, nil, nil
    95  }
    96  
    97  func (fakeGitHub) EditMilestone(_ context.Context, owner string, repo string, number int, milestone *github.Milestone) (*github.Milestone, *github.Response, error) {
    98  	return nil, nil, nil
    99  }
   100  
   101  func (fakeGitHub) PostComment(_ context.Context, _ githubv4.ID, _ string) error {
   102  	return fmt.Errorf("pretend that PostComment failed")
   103  }
   104  
   105  var (
   106  	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.")
   107  	flagOwner = flag.String("milestones-github-owner", "", "Owner of testing repository")
   108  	flagRepo  = flag.String("milestones-github-repo", "", "Testing repository")
   109  )
   110  
   111  func TestMilestones(t *testing.T) {
   112  	ctx := &workflow.TaskContext{
   113  		Context: context.Background(),
   114  		Logger:  &testLogger{t, ""},
   115  	}
   116  
   117  	if !*flagRun {
   118  		t.Skip("Not enabled by flags")
   119  	}
   120  	if *flagOwner == "golang" {
   121  		t.Fatal("This is a destructive test! Don't run it on a real repository.")
   122  	}
   123  
   124  	src := oauth2.StaticTokenSource(
   125  		&oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
   126  	)
   127  	httpClient := oauth2.NewClient(ctx, src)
   128  	client3 := github.NewClient(httpClient)
   129  	client4 := githubv4.NewClient(httpClient)
   130  
   131  	normal, blocker, err := resetRepo(ctx, client3)
   132  	if err != nil {
   133  		t.Fatal(err)
   134  	}
   135  
   136  	tasks := &MilestoneTasks{
   137  		Client: &GitHubClient{
   138  			V3: client3,
   139  			V4: client4,
   140  		},
   141  		RepoOwner: *flagOwner,
   142  		RepoName:  *flagRepo,
   143  		ApproveAction: func(*workflow.TaskContext) error {
   144  			return fmt.Errorf("not approved")
   145  		},
   146  	}
   147  	milestones, err := tasks.FetchMilestones(ctx, "go1.20", KindMajor)
   148  	if err != nil {
   149  		t.Fatalf("GetMilestones: %v", err)
   150  	}
   151  	if err := tasks.PushIssues(ctx, milestones, "go1.20beta1", KindBeta); err != nil {
   152  		t.Fatalf("Pushing issues for beta release: %v", err)
   153  	}
   154  	pushedBlocker, _, err := client3.Issues.Get(ctx, *flagOwner, *flagRepo, blocker.GetNumber())
   155  	if err != nil {
   156  		t.Fatal(err)
   157  	}
   158  	if len(pushedBlocker.Labels) != 1 || *pushedBlocker.Labels[0].Name != "release-blocker" {
   159  		t.Errorf("release blocking issue has labels %#v, should only have release-blocker", pushedBlocker.Labels)
   160  	}
   161  	err = tasks.CheckBlockers(ctx, milestones, "go1.20", KindMajor)
   162  	if err == nil || !strings.Contains(err.Error(), "open release blockers") {
   163  		t.Fatalf("CheckBlockers with an open release blocker didn't give expected error: %v", err)
   164  	}
   165  	if _, _, err := client3.Issues.Edit(ctx, *flagOwner, *flagRepo, *blocker.Number, &github.IssueRequest{State: github.String("closed")}); err != nil {
   166  		t.Fatal(err)
   167  	}
   168  	if err := tasks.CheckBlockers(ctx, milestones, "go1.20", KindMajor); err != nil {
   169  		t.Fatalf("CheckBlockers with no release blockers failed: %v", err)
   170  	}
   171  	if err := tasks.PushIssues(ctx, milestones, "go1.20", KindMajor); err != nil {
   172  		t.Fatalf("PushIssues for major release failed: %v", err)
   173  	}
   174  	milestone, _, err := client3.Issues.GetMilestone(ctx, *flagOwner, *flagRepo, milestones.Current)
   175  	if err != nil {
   176  		t.Fatal(err)
   177  	}
   178  	if milestone.GetState() != "closed" {
   179  		t.Errorf("current milestone is %q, should be closed", milestone.GetState())
   180  	}
   181  	pushedNormal, _, err := client3.Issues.Get(ctx, *flagOwner, *flagRepo, normal.GetNumber())
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  	if pushedNormal.GetMilestone().GetNumber() != milestones.Next {
   186  		t.Errorf("issue %v is on milestone %v, should have been pushed to %v", normal.GetNumber(), pushedNormal.GetMilestone().GetNumber(), milestones.Next)
   187  	}
   188  }
   189  
   190  // resetRepo clears out the test repository and sets it to have:
   191  // - a single milestone, Go1.20
   192  // - a normal issue in that milestone
   193  // - an okay-after-beta1 release blocking issue in that milestone, which is returned.
   194  func resetRepo(ctx context.Context, client *github.Client) (normal, blocker *github.Issue, err error) {
   195  	milestones, _, err := client.Issues.ListMilestones(ctx, *flagOwner, *flagRepo, &github.MilestoneListOptions{State: "all"})
   196  	if err != nil {
   197  		return nil, nil, err
   198  	}
   199  	for _, m := range milestones {
   200  		if _, err := client.Issues.DeleteMilestone(ctx, *flagOwner, *flagRepo, *m.Number); err != nil {
   201  			return nil, nil, err
   202  		}
   203  	}
   204  	issues, _, err := client.Issues.ListByRepo(ctx, *flagOwner, *flagRepo, nil)
   205  	if err != nil {
   206  		return nil, nil, err
   207  	}
   208  	for _, i := range issues {
   209  		if _, _, err := client.Issues.Edit(ctx, *flagOwner, *flagRepo, *i.Number, &github.IssueRequest{
   210  			State: github.String("CLOSED"),
   211  		}); err != nil {
   212  			return nil, nil, err
   213  		}
   214  	}
   215  	currentMilestone, _, err := client.Issues.CreateMilestone(ctx, *flagOwner, *flagRepo, &github.Milestone{Title: github.String("Go1.20")})
   216  	if err != nil {
   217  		return nil, nil, err
   218  	}
   219  	normal, _, err = client.Issues.Create(ctx, *flagOwner, *flagRepo, &github.IssueRequest{
   220  		Title:     github.String("Non-release-blocker"),
   221  		Milestone: currentMilestone.Number,
   222  	})
   223  	if err != nil {
   224  		return nil, nil, err
   225  	}
   226  	blocker, _, err = client.Issues.Create(ctx, *flagOwner, *flagRepo, &github.IssueRequest{
   227  		Title:     github.String("Release-blocker"),
   228  		Milestone: currentMilestone.Number,
   229  		Labels:    &[]string{"release-blocker", "okay-after-beta1"},
   230  	})
   231  	return normal, blocker, err
   232  }