mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Add an initial (simple) implementation of a submit queue.
This commit is contained in:
parent
0b7c2d5496
commit
9bec48298b
283
contrib/submit-queue/github/github.go
Normal file
283
contrib/submit-queue/github/github.go
Normal file
@ -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
|
||||||
|
}
|
390
contrib/submit-queue/github/github_test.go
Normal file
390
contrib/submit-queue/github/github_test.go
Normal file
@ -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("<https://api.github.com/?page=%d>; 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
contrib/submit-queue/jenkins/jenkins.go
Normal file
91
contrib/submit-queue/jenkins/jenkins.go
Normal file
@ -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
|
||||||
|
}
|
162
contrib/submit-queue/submit-queue.go
Normal file
162
contrib/submit-queue/submit-queue.go
Normal file
@ -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=<github-access-token> -user-whitelist=<file> --jenkins-host=http://some.host [-min-pr-number=<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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
contrib/submit-queue/whitelist.txt
Normal file
41
contrib/submit-queue/whitelist.txt
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user