mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +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