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 }