diff --git a/contrib/submit-queue/github/github.go b/contrib/submit-queue/github/github.go deleted file mode 100644 index c36ecccaa99..00000000000 --- a/contrib/submit-queue/github/github.go +++ /dev/null @@ -1,339 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package github - -import ( - "fmt" - "time" - - "k8s.io/kubernetes/pkg/util" - - "github.com/golang/glog" - "github.com/google/go-github/github" - "golang.org/x/oauth2" -) - -func MakeClient(token string) *github.Client { - if len(token) > 0 { - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) - tc := oauth2.NewClient(oauth2.NoContext, ts) - return github.NewClient(tc) - } - return github.NewClient(nil) -} - -func hasLabel(labels []github.Label, name string) bool { - for i := range labels { - label := &labels[i] - if label.Name != nil && *label.Name == name { - return true - } - } - return false -} - -func hasLabels(labels []github.Label, names []string) bool { - for i := range names { - if !hasLabel(labels, names[i]) { - return false - } - } - return true -} - -func fetchAllPRs(client *github.Client, user, project string) ([]github.PullRequest, error) { - page := 1 - var result []github.PullRequest - for { - glog.V(4).Infof("Fetching page %d", page) - listOpts := &github.PullRequestListOptions{ - Sort: "desc", - ListOptions: github.ListOptions{PerPage: 100, Page: page}, - } - prs, response, err := client.PullRequests.List(user, project, listOpts) - if err != nil { - return nil, err - } - result = append(result, prs...) - if response.LastPage == 0 || response.LastPage == page { - break - } - page++ - } - return result, nil -} - -type PRFunction func(*github.Client, *github.PullRequest, *github.Issue) error - -type FilterConfig struct { - MinPRNumber int - UserWhitelist []string - WhitelistOverride string - RequiredStatusContexts []string -} - -func lastModifiedTime(client *github.Client, user, project string, pr *github.PullRequest) (*time.Time, error) { - list, _, err := client.PullRequests.ListCommits(user, project, *pr.Number, &github.ListOptions{}) - if err != nil { - return nil, err - } - var lastModified *time.Time - for ix := range list { - item := list[ix] - if lastModified == nil || item.Commit.Committer.Date.After(*lastModified) { - lastModified = item.Commit.Committer.Date - } - } - return lastModified, nil -} - -func validateLGTMAfterPush(client *github.Client, user, project string, pr *github.PullRequest, lastModifiedTime *time.Time) (bool, error) { - var lgtmTime *time.Time - events, _, err := client.Issues.ListIssueEvents(user, project, *pr.Number, &github.ListOptions{}) - if err != nil { - glog.Errorf("Error getting events for issue: %v", err) - return false, err - } - for ix := range events { - event := &events[ix] - if *event.Event == "labeled" && *event.Label.Name == "lgtm" { - if lgtmTime == nil || event.CreatedAt.After(*lgtmTime) { - lgtmTime = event.CreatedAt - } - } - } - if lgtmTime == nil { - return false, fmt.Errorf("Couldn't find time for LGTM label, this shouldn't happen, skipping PR: %d", *pr.Number) - } - return lastModifiedTime.Before(*lgtmTime), nil -} - -// For each PR in the project that matches: -// * pr.Number > minPRNumber -// * is mergeable -// * has labels "cla: yes", "lgtm" -// * combinedStatus = 'success' (e.g. all hooks have finished success in github) -// Run the specified function -func ForEachCandidatePRDo(client *github.Client, user, project string, fn PRFunction, once bool, config *FilterConfig) error { - // Get all PRs - prs, err := fetchAllPRs(client, user, project) - if err != nil { - return err - } - - userSet := util.StringSet{} - userSet.Insert(config.UserWhitelist...) - - for ix := range prs { - if prs[ix].User == nil || prs[ix].User.Login == nil { - glog.V(2).Infof("Skipping PR %d with no user info %v.", *prs[ix].Number, *prs[ix].User) - continue - } - if *prs[ix].Number < config.MinPRNumber { - glog.V(6).Infof("Dropping %d < %d", *prs[ix].Number, config.MinPRNumber) - continue - } - pr, _, err := client.PullRequests.Get(user, project, *prs[ix].Number) - if err != nil { - glog.Errorf("Error getting pull request: %v", err) - continue - } - glog.V(2).Infof("----==== %d ====----", *pr.Number) - - // Labels are actually stored in the Issues API, not the Pull Request API - issue, _, err := client.Issues.Get(user, project, *pr.Number) - if err != nil { - glog.Errorf("Failed to get issue for PR: %v", err) - continue - } - - glog.V(8).Infof("%v", issue.Labels) - if !hasLabels(issue.Labels, []string{"lgtm", "cla: yes"}) { - continue - } - if !hasLabel(issue.Labels, config.WhitelistOverride) && !userSet.Has(*prs[ix].User.Login) { - glog.V(4).Infof("Dropping %d since %s isn't in whitelist and %s isn't present", *prs[ix].Number, *prs[ix].User.Login, config.WhitelistOverride) - continue - } - - lastModifiedTime, err := lastModifiedTime(client, user, project, pr) - if err != nil { - glog.Errorf("Failed to get last modified time, skipping PR: %d", *pr.Number) - continue - } - if ok, err := validateLGTMAfterPush(client, user, project, pr, lastModifiedTime); err != nil { - glog.Errorf("Error validating LGTM: %v, Skipping: %d", err, *pr.Number) - continue - } else if !ok { - glog.Errorf("PR pushed after LGTM, attempting to remove LGTM and skipping") - staleLGTMBody := "LGTM was before last commit, removing LGTM" - if _, _, err := client.Issues.CreateComment(user, project, *pr.Number, &github.IssueComment{Body: &staleLGTMBody}); err != nil { - glog.Warningf("Failed to create remove label comment: %v", err) - } - if _, err := client.Issues.RemoveLabelForIssue(user, project, *pr.Number, "lgtm"); err != nil { - glog.Warningf("Failed to remove 'lgtm' label for stale lgtm on %d", *pr.Number) - } - continue - } - - // This is annoying, github appears to only temporarily cache mergeability, if it is nil, wait - // for an async refresh and retry. - if pr.Mergeable == nil { - glog.Infof("Waiting for mergeability on %s %d", *pr.Title, *pr.Number) - // TODO: determine what a good empirical setting for this is. - time.Sleep(10 * time.Second) - pr, _, err = client.PullRequests.Get(user, project, *prs[ix].Number) - } - if pr.Mergeable == nil { - glog.Errorf("No mergeability information for %s %d, Skipping.", *pr.Title, *pr.Number) - continue - } - if !*pr.Mergeable { - continue - } - - // Validate the status information for this PR - ok, err := ValidateStatus(client, user, project, *pr.Number, config.RequiredStatusContexts, false) - if err != nil { - glog.Errorf("Error validating PR status: %v", err) - continue - } - if !ok { - continue - } - if err := fn(client, pr, issue); err != nil { - glog.Errorf("Failed to run user function: %v", err) - continue - } - if once { - break - } - } - return nil -} - -func getCommitStatus(client *github.Client, user, project string, prNumber int) ([]*github.CombinedStatus, error) { - commits, _, err := client.PullRequests.ListCommits(user, project, prNumber, &github.ListOptions{}) - if err != nil { - return nil, err - } - commitStatus := make([]*github.CombinedStatus, len(commits)) - for ix := range commits { - commit := &commits[ix] - statusList, _, err := client.Repositories.GetCombinedStatus(user, project, *commit.SHA, &github.ListOptions{}) - if err != nil { - return nil, err - } - commitStatus[ix] = statusList - } - return commitStatus, nil -} - -// Gets the current status of a PR by introspecting the status of the commits in the PR. -// The rules are: -// * If any member of the 'requiredContexts' list is missing, it is 'incomplete' -// * If any commit is 'pending', the PR is 'pending' -// * If any commit is 'error', the PR is in 'error' -// * If any commit is 'failure', the PR is 'failure' -// * Otherwise the PR is 'success' -func GetStatus(client *github.Client, user, project string, prNumber int, requiredContexts []string) (string, error) { - statusList, err := getCommitStatus(client, user, project, prNumber) - if err != nil { - return "", err - } - return computeStatus(statusList, requiredContexts), nil -} - -func computeStatus(statusList []*github.CombinedStatus, requiredContexts []string) string { - states := util.StringSet{} - providers := util.StringSet{} - for ix := range statusList { - status := statusList[ix] - glog.V(8).Infof("Checking commit: %s", *status.SHA) - glog.V(8).Infof("Checking commit: %v", status) - states.Insert(*status.State) - - for _, subStatus := range status.Statuses { - glog.V(8).Infof("Found status from: %v", subStatus) - providers.Insert(*subStatus.Context) - } - } - for _, provider := range requiredContexts { - if !providers.Has(provider) { - glog.V(8).Infof("Failed to find %s in %v", provider, providers) - return "incomplete" - } - } - - switch { - case states.Has("pending"): - return "pending" - case states.Has("error"): - return "error" - case states.Has("failure"): - return "failure" - default: - return "success" - } -} - -// Make sure that the combined status for all commits in a PR is 'success' -// if 'waitForPending' is true, this function will wait until the PR is no longer pending (all checks have run) -func ValidateStatus(client *github.Client, user, project string, prNumber int, requiredContexts []string, waitOnPending bool) (bool, error) { - pending := true - for pending { - status, err := GetStatus(client, user, project, prNumber, requiredContexts) - if err != nil { - return false, err - } - switch status { - case "error", "failure": - return false, nil - case "pending": - if !waitOnPending { - return false, nil - } - pending = true - glog.V(4).Info("PR is pending, waiting for 30 seconds") - time.Sleep(30 * time.Second) - case "success": - return true, nil - case "incomplete": - return false, nil - default: - return false, fmt.Errorf("unknown status: %s", status) - } - } - return true, nil -} - -// Wait for a PR to move into Pending. This is useful because the request to test a PR again -// is asynchronous with the PR actually moving into a pending state -// TODO: add a timeout -func WaitForPending(client *github.Client, user, project string, prNumber int) error { - for { - status, err := GetStatus(client, user, project, prNumber, []string{}) - if err != nil { - return err - } - if status == "pending" { - return nil - } - glog.V(4).Info("PR is not pending, waiting for 30 seconds") - time.Sleep(30 * time.Second) - } -} diff --git a/contrib/submit-queue/github/github_test.go b/contrib/submit-queue/github/github_test.go deleted file mode 100644 index 84396b7e23c..00000000000 --- a/contrib/submit-queue/github/github_test.go +++ /dev/null @@ -1,624 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package github - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "testing" - "time" - - "github.com/google/go-github/github" -) - -func stringPtr(val string) *string { return &val } -func timePtr(val time.Time) *time.Time { return &val } -func intPtr(val int) *int { return &val } - -func TestHasLabel(t *testing.T) { - tests := []struct { - labels []github.Label - label string - hasLabel bool - }{ - { - labels: []github.Label{ - {Name: stringPtr("foo")}, - }, - label: "foo", - hasLabel: true, - }, - { - labels: []github.Label{ - {Name: stringPtr("bar")}, - }, - label: "foo", - hasLabel: false, - }, - { - labels: []github.Label{ - {Name: stringPtr("bar")}, - {Name: stringPtr("foo")}, - }, - label: "foo", - hasLabel: true, - }, - { - labels: []github.Label{ - {Name: stringPtr("bar")}, - {Name: stringPtr("baz")}, - }, - label: "foo", - hasLabel: false, - }, - } - - for _, test := range tests { - if test.hasLabel != hasLabel(test.labels, test.label) { - t.Errorf("Unexpected output: %v", test) - } - } -} - -func TestHasLabels(t *testing.T) { - tests := []struct { - labels []github.Label - seekLabels []string - hasLabel bool - }{ - { - labels: []github.Label{ - {Name: stringPtr("foo")}, - }, - seekLabels: []string{"foo"}, - hasLabel: true, - }, - { - labels: []github.Label{ - {Name: stringPtr("bar")}, - }, - seekLabels: []string{"foo"}, - hasLabel: false, - }, - { - labels: []github.Label{ - {Name: stringPtr("bar")}, - {Name: stringPtr("foo")}, - }, - seekLabels: []string{"foo"}, - hasLabel: true, - }, - { - labels: []github.Label{ - {Name: stringPtr("bar")}, - {Name: stringPtr("baz")}, - }, - seekLabels: []string{"foo"}, - hasLabel: false, - }, - { - labels: []github.Label{ - {Name: stringPtr("foo")}, - }, - seekLabels: []string{"foo", "bar"}, - hasLabel: false, - }, - } - - for _, test := range tests { - if test.hasLabel != hasLabels(test.labels, test.seekLabels) { - t.Errorf("Unexpected output: %v", test) - } - } -} - -func initTest() (*github.Client, *httptest.Server, *http.ServeMux) { - // test server - mux := http.NewServeMux() - server := httptest.NewServer(mux) - - // github client configured to use test server - client := github.NewClient(nil) - url, _ := url.Parse(server.URL) - client.BaseURL = url - client.UploadURL = url - - return client, server, mux -} - -func TestFetchAllPRs(t *testing.T) { - tests := []struct { - PullRequests [][]github.PullRequest - Pages []int - }{ - { - PullRequests: [][]github.PullRequest{ - { - {}, - }, - }, - Pages: []int{0}, - }, - { - PullRequests: [][]github.PullRequest{ - { - {}, - }, - { - {}, - }, - { - {}, - }, - { - {}, - }, - }, - Pages: []int{4, 4, 4, 0}, - }, - { - PullRequests: [][]github.PullRequest{ - { - {}, - }, - { - {}, - }, - { - {}, - {}, - {}, - }, - }, - Pages: []int{3, 3, 3, 0}, - }, - } - - for _, test := range tests { - client, server, mux := initTest() - count := 0 - prCount := 0 - mux.HandleFunc("/repos/foo/bar/pulls", func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - t.Errorf("Unexpected method: %s", r.Method) - } - if r.URL.Query().Get("page") != strconv.Itoa(count+1) { - t.Errorf("Unexpected page: %s", r.URL.Query().Get("page")) - } - if r.URL.Query().Get("sort") != "desc" { - t.Errorf("Unexpected sort: %s", r.URL.Query().Get("sort")) - } - if r.URL.Query().Get("per_page") != "100" { - t.Errorf("Unexpected per_page: %s", r.URL.Query().Get("per_page")) - } - w.Header().Add("Link", - fmt.Sprintf("; rel=\"last\"", test.Pages[count])) - w.WriteHeader(http.StatusOK) - data, err := json.Marshal(test.PullRequests[count]) - prCount += len(test.PullRequests[count]) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - w.Write(data) - count++ - }) - prs, err := fetchAllPRs(client, "foo", "bar") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(prs) != prCount { - t.Errorf("unexpected output %d vs %d", len(prs), prCount) - } - - if count != len(test.PullRequests) { - t.Errorf("unexpected number of fetches: %d", count) - } - server.Close() - } -} - -func TestComputeStatus(t *testing.T) { - tests := []struct { - statusList []*github.CombinedStatus - requiredContexts []string - expected string - }{ - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - expected: "success", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("error"), SHA: stringPtr("abcdef")}, - {State: stringPtr("pending"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - expected: "pending", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("pending"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - expected: "pending", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("failure"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - expected: "failure", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("failure"), SHA: stringPtr("abcdef")}, - {State: stringPtr("error"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - expected: "error", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "incomplete", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("pending"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "incomplete", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("failure"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "incomplete", - }, - { - statusList: []*github.CombinedStatus{ - {State: stringPtr("failure"), SHA: stringPtr("abcdef")}, - {State: stringPtr("error"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "incomplete", - }, - { - statusList: []*github.CombinedStatus{ - { - State: stringPtr("success"), - SHA: stringPtr("abcdef"), - Statuses: []github.RepoStatus{ - {Context: stringPtr("context")}, - }, - }, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "success", - }, - { - statusList: []*github.CombinedStatus{ - { - State: stringPtr("pending"), - SHA: stringPtr("abcdef"), - Statuses: []github.RepoStatus{ - {Context: stringPtr("context")}, - }, - }, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "pending", - }, - { - statusList: []*github.CombinedStatus{ - { - State: stringPtr("error"), - SHA: stringPtr("abcdef"), - Statuses: []github.RepoStatus{ - {Context: stringPtr("context")}, - }, - }, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "error", - }, - { - statusList: []*github.CombinedStatus{ - { - State: stringPtr("failure"), - SHA: stringPtr("abcdef"), - Statuses: []github.RepoStatus{ - {Context: stringPtr("context")}, - }, - }, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - {State: stringPtr("success"), SHA: stringPtr("abcdef")}, - }, - requiredContexts: []string{"context"}, - expected: "failure", - }, - } - - for _, test := range tests { - // ease of use, reduce boilerplate in test cases - if test.requiredContexts == nil { - test.requiredContexts = []string{} - } - status := computeStatus(test.statusList, test.requiredContexts) - if test.expected != status { - t.Errorf("expected: %s, saw %s", test.expected, status) - } - } -} - -func TestValidateLGTMAfterPush(t *testing.T) { - tests := []struct { - issueEvents []github.IssueEvent - shouldPass bool - lastModified time.Time - }{ - { - issueEvents: []github.IssueEvent{ - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(10, 0)), - }, - }, - lastModified: time.Unix(9, 0), - shouldPass: true, - }, - { - issueEvents: []github.IssueEvent{ - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(10, 0)), - }, - }, - lastModified: time.Unix(11, 0), - shouldPass: false, - }, - { - issueEvents: []github.IssueEvent{ - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(12, 0)), - }, - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(11, 0)), - }, - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(10, 0)), - }, - }, - lastModified: time.Unix(11, 0), - shouldPass: true, - }, - { - issueEvents: []github.IssueEvent{ - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(10, 0)), - }, - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(11, 0)), - }, - { - Event: stringPtr("labeled"), - Label: &github.Label{ - Name: stringPtr("lgtm"), - }, - CreatedAt: timePtr(time.Unix(12, 0)), - }, - }, - lastModified: time.Unix(11, 0), - shouldPass: true, - }, - } - for _, test := range tests { - client, server, mux := initTest() - mux.HandleFunc(fmt.Sprintf("/repos/o/r/issues/1/events"), func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - t.Errorf("Unexpected method: %s", r.Method) - } - w.WriteHeader(http.StatusOK) - data, err := json.Marshal(test.issueEvents) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - w.Write(data) - ok, err := validateLGTMAfterPush(client, "o", "r", &github.PullRequest{Number: intPtr(1)}, &test.lastModified) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if ok != test.shouldPass { - t.Errorf("expected: %v, saw: %v", test.shouldPass, ok) - } - }) - server.Close() - } -} - -func TestGetLastModified(t *testing.T) { - tests := []struct { - commits []github.RepositoryCommit - expectedTime *time.Time - }{ - { - commits: []github.RepositoryCommit{ - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(10, 0)), - }, - }, - }, - }, - expectedTime: timePtr(time.Unix(10, 0)), - }, - { - commits: []github.RepositoryCommit{ - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(10, 0)), - }, - }, - }, - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(11, 0)), - }, - }, - }, - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(12, 0)), - }, - }, - }, - }, - expectedTime: timePtr(time.Unix(12, 0)), - }, - { - commits: []github.RepositoryCommit{ - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(10, 0)), - }, - }, - }, - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(9, 0)), - }, - }, - }, - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(8, 0)), - }, - }, - }, - }, - expectedTime: timePtr(time.Unix(10, 0)), - }, - { - commits: []github.RepositoryCommit{ - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(9, 0)), - }, - }, - }, - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(10, 0)), - }, - }, - }, - { - Commit: &github.Commit{ - Committer: &github.CommitAuthor{ - Date: timePtr(time.Unix(9, 0)), - }, - }, - }, - }, - expectedTime: timePtr(time.Unix(10, 0)), - }, - } - for _, test := range tests { - client, server, mux := initTest() - mux.HandleFunc(fmt.Sprintf("/repos/o/r/pulls/1/commits"), func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - t.Errorf("Unexpected method: %s", r.Method) - } - w.WriteHeader(http.StatusOK) - data, err := json.Marshal(test.commits) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - w.Write(data) - ts, err := lastModifiedTime(client, "o", "r", &github.PullRequest{Number: intPtr(1)}) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if !ts.Equal(*test.expectedTime) { - t.Errorf("expected: %v, saw: %v", test.expectedTime, ts) - } - }) - server.Close() - } -} diff --git a/contrib/submit-queue/jenkins/jenkins.go b/contrib/submit-queue/jenkins/jenkins.go deleted file mode 100644 index 690f50c0ee2..00000000000 --- a/contrib/submit-queue/jenkins/jenkins.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package jenkins - -import ( - "encoding/json" - "io/ioutil" - "net/http" - - "github.com/golang/glog" -) - -type JenkinsClient struct { - Host string -} - -type Queue struct { - Builds []Build `json:"builds"` - LastCompletedBuild Build `json:"lastCompletedBuild"` - LastStableBuild Build `json:"lastStableBuild"` -} - -type Build struct { - Number int `json:"number"` - URL string `json:"url"` -} - -type Job struct { - Result string `json:"result"` - ID string `json:"id"` - Timestamp int `json:"timestamp"` -} - -func (j *JenkinsClient) request(path string) ([]byte, error) { - url := j.Host + path - glog.V(3).Infof("Hitting: %s", url) - res, err := http.Get(url) - if err != nil { - return nil, err - } - defer res.Body.Close() - return ioutil.ReadAll(res.Body) -} - -func (j *JenkinsClient) GetJob(name string) (*Queue, error) { - data, err := j.request("/job/" + name + "/api/json") - if err != nil { - return nil, err - } - glog.V(8).Infof("Got data: %s", string(data)) - q := &Queue{} - if err := json.Unmarshal(data, q); err != nil { - return nil, err - } - return q, nil -} - -func (j *JenkinsClient) GetLastCompletedBuild(name string) (*Job, error) { - data, err := j.request("/job/" + name + "/lastCompletedBuild/api/json") - if err != nil { - return nil, err - } - glog.V(8).Infof("Got data: %s", string(data)) - job := &Job{} - if err := json.Unmarshal(data, job); err != nil { - return nil, err - } - return job, nil -} - -func (j *JenkinsClient) IsBuildStable(name string) (bool, error) { - q, err := j.GetLastCompletedBuild(name) - if err != nil { - return false, err - } - return q.Result == "SUCCESS", nil -} diff --git a/contrib/submit-queue/submit-queue.go b/contrib/submit-queue/submit-queue.go deleted file mode 100644 index aa28c01dd4b..00000000000 --- a/contrib/submit-queue/submit-queue.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -// A simple binary for merging PR that match a criteria -// Usage: -// submit-queue -token= -user-whitelist= --jenkins-host=http://some.host [-min-pr-number=] [-dry-run] [-once] -// -// Details: -/* -Usage of ./submit-queue: - -alsologtostderr=false: log to standard error as well as files - -dry-run=false: If true, don't actually merge anything - -jenkins-job="kubernetes-e2e-gce,kubernetes-e2e-gke-ci,kubernetes-build": Comma separated list of jobs in Jenkins to use for stability testing - -log_backtrace_at=:0: when logging hits line file:N, emit a stack trace - -log_dir="": If non-empty, write log files in this directory - -logtostderr=false: log to standard error instead of files - -min-pr-number=0: The minimum PR to start with [default: 0] - -once=false: If true, only merge one PR, don't run forever - -stderrthreshold=0: logs at or above this threshold go to stderr - -token="": The OAuth Token to use for requests. - -user-whitelist="": Path to a whitelist file that contains users to auto-merge. Required. - -v=0: log level for V logs - -vmodule=: comma-separated list of pattern=N settings for file-filtered logging -*/ - -import ( - "bufio" - "errors" - "flag" - "os" - "strings" - - "k8s.io/kubernetes/contrib/submit-queue/github" - "k8s.io/kubernetes/contrib/submit-queue/jenkins" - - "github.com/golang/glog" - github_api "github.com/google/go-github/github" -) - -var ( - token = flag.String("token", "", "The OAuth Token to use for requests.") - minPRNumber = flag.Int("min-pr-number", 0, "The minimum PR to start with [default: 0]") - dryrun = flag.Bool("dry-run", false, "If true, don't actually merge anything") - oneOff = flag.Bool("once", false, "If true, only merge one PR, don't run forever") - jobs = flag.String("jenkins-jobs", "kubernetes-e2e-gce,kubernetes-e2e-gke-ci,kubernetes-build", "Comma separated list of jobs in Jenkins to use for stability testing") - jenkinsHost = flag.String("jenkins-host", "", "The URL for the jenkins job to watch") - userWhitelist = flag.String("user-whitelist", "", "Path to a whitelist file that contains users to auto-merge. Required.") - requiredContexts = flag.String("required-contexts", "cla/google,Shippable,continuous-integration/travis-ci/pr,Jenkins GCE e2e", "Comma separate list of status contexts required for a PR to be considered ok to merge") - whitelistOverride = flag.String("whitelist-override-label", "ok-to-merge", "Github label, if present on a PR it will be merged even if the author isn't in the whitelist") -) - -const ( - org = "GoogleCloudPlatform" - project = "kubernetes" -) - -// This is called on a potentially mergeable PR -func runE2ETests(client *github_api.Client, pr *github_api.PullRequest, issue *github_api.Issue) error { - // Test if the build is stable in Jenkins - jenkinsClient := &jenkins.JenkinsClient{Host: *jenkinsHost} - builds := strings.Split(*jobs, ",") - for _, build := range builds { - stable, err := jenkinsClient.IsBuildStable(build) - glog.V(2).Infof("Checking build stability for %s", build) - if err != nil { - return err - } - if !stable { - glog.Errorf("Build %s isn't stable, skipping!", build) - return errors.New("Unstable build") - } - } - glog.V(2).Infof("Build is stable.") - // Ask for a fresh build - glog.V(4).Infof("Asking PR builder to build %d", *pr.Number) - body := "@k8s-bot test this [testing build queue, sorry for the noise]" - if _, _, err := client.Issues.CreateComment(org, project, *pr.Number, &github_api.IssueComment{Body: &body}); err != nil { - return err - } - - // Wait for the build to start - err := github.WaitForPending(client, org, project, *pr.Number) - - // Wait for the status to go back to 'success' - ok, err := github.ValidateStatus(client, org, project, *pr.Number, []string{}, true) - if err != nil { - return err - } - if !ok { - glog.Infof("Status after build is not 'success', skipping PR %d", *pr.Number) - return nil - } - if !*dryrun { - glog.Infof("Merging PR: %d", *pr.Number) - mergeBody := "Automatic merge from SubmitQueue" - if _, _, err := client.Issues.CreateComment(org, project, *pr.Number, &github_api.IssueComment{Body: &mergeBody}); err != nil { - glog.Warningf("Failed to create merge comment: %v", err) - return err - } - _, _, err := client.PullRequests.Merge(org, project, *pr.Number, "Auto commit by PR queue bot") - return err - } - glog.Infof("Skipping actual merge because --dry-run is set") - return nil -} - -func loadWhitelist(file string) ([]string, error) { - fp, err := os.Open(file) - if err != nil { - return nil, err - } - defer fp.Close() - scanner := bufio.NewScanner(fp) - result := []string{} - for scanner.Scan() { - result = append(result, scanner.Text()) - } - return result, scanner.Err() -} - -func main() { - flag.Parse() - if len(*userWhitelist) == 0 { - glog.Fatalf("--user-whitelist is required.") - } - if len(*jenkinsHost) == 0 { - glog.Fatalf("--jenkins-host is required.") - } - client := github.MakeClient(*token) - - users, err := loadWhitelist(*userWhitelist) - if err != nil { - glog.Fatalf("error loading user whitelist: %v", err) - } - requiredContexts := strings.Split(*requiredContexts, ",") - config := &github.FilterConfig{ - MinPRNumber: *minPRNumber, - UserWhitelist: users, - RequiredStatusContexts: requiredContexts, - WhitelistOverride: *whitelistOverride, - } - for !*oneOff { - if err := github.ForEachCandidatePRDo(client, org, project, runE2ETests, *oneOff, config); err != nil { - glog.Fatalf("Error getting candidate PRs: %v", err) - } - } -} diff --git a/contrib/submit-queue/whitelist.txt b/contrib/submit-queue/whitelist.txt deleted file mode 100644 index 05ffe27508d..00000000000 --- a/contrib/submit-queue/whitelist.txt +++ /dev/null @@ -1,41 +0,0 @@ -brendandburns -thockin -mikedanese -a-robinson -saad-ali -lavalamp -smarterclayton -justinsb -satnam6502 -derekwaynecarr -dchen1107 -zmerlynn -erictune -eparis -caesarxuchao -wojtek-t -jlowdermilk -yifan-gu -nikhiljindal -markturansky -pmorie -yujuhong -roberthbailey -vishh -deads2k -bprashanth -cjcullen -liggitt -bgrant0607 -fgrzadkowski -jayunit100 -mbforbes -ArtfulCoder -piosz -davidopp -ixdy -marekbiskup -gmarek -ghodss -krousey -quinton-hoole diff --git a/docs/devel/pull-requests.md b/docs/devel/pull-requests.md index 6d2eb597e39..126b8996db0 100644 --- a/docs/devel/pull-requests.md +++ b/docs/devel/pull-requests.md @@ -52,14 +52,14 @@ Life of a Pull Request Unless in the last few weeks of a milestone when we need to reduce churn and stabilize, we aim to be always accepting pull requests. -Either the [on call](https://github.com/GoogleCloudPlatform/kubernetes/wiki/Kubernetes-on-call-rotation) manually or the [submit queue](../../contrib/submit-queue/) automatically will manage merging PRs. +Either the [on call](https://github.com/GoogleCloudPlatform/kubernetes/wiki/Kubernetes-on-call-rotation) manually or the [submit queue](https://github.com/contrib/tree/master/submit-queue) automatically will manage merging PRs. There are several requirements for the submit queue to work: * Author must have signed CLA ("cla: yes" label added to PR) * No changes can be made since last lgtm label was applied * k8s-bot must have reported the GCE E2E build and test steps passed (Travis, Shippable and Jenkins build) -Additionally, for infrequent or new contributors, we require the on call to apply the "ok-to-merge" label manually. This is gated by the [whitelist](../../contrib/submit-queue/whitelist.txt). +Additionally, for infrequent or new contributors, we require the on call to apply the "ok-to-merge" label manually. This is gated by the [whitelist](https://github.com/contrib/tree/master/submit-queue/whitelist.txt).