diff --git a/contrib/submit-queue/github/github.go b/contrib/submit-queue/github/github.go new file mode 100644 index 00000000000..63e598d3f5c --- /dev/null +++ b/contrib/submit-queue/github/github.go @@ -0,0 +1,283 @@ +/* +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" + + "github.com/GoogleCloudPlatform/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 +} + +// 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 + } + + // 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) + } + return nil +} diff --git a/contrib/submit-queue/github/github_test.go b/contrib/submit-queue/github/github_test.go new file mode 100644 index 00000000000..83449c9803a --- /dev/null +++ b/contrib/submit-queue/github/github_test.go @@ -0,0 +1,390 @@ +/* +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" + + "github.com/google/go-github/github" +) + +func stringPtr(val string) *string { 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) + } + } +} diff --git a/contrib/submit-queue/jenkins/jenkins.go b/contrib/submit-queue/jenkins/jenkins.go new file mode 100644 index 00000000000..a16d7abac21 --- /dev/null +++ b/contrib/submit-queue/jenkins/jenkins.go @@ -0,0 +1,91 @@ +/* +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 new file mode 100644 index 00000000000..b0735997a38 --- /dev/null +++ b/contrib/submit-queue/submit-queue.go @@ -0,0 +1,162 @@ +/* +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" + + "github.com/GoogleCloudPlatform/kubernetes/contrib/submit-queue/github" + "github.com/GoogleCloudPlatform/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 new file mode 100644 index 00000000000..05ffe27508d --- /dev/null +++ b/contrib/submit-queue/whitelist.txt @@ -0,0 +1,41 @@ +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