| // 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 |
| } |