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