github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/tide/tide.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package tide contains a controller for managing a tide pool of PRs. The
    18  // controller will automatically retest PRs in the pool and merge them if they
    19  // pass tests.
    20  package tide
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"net/http"
    27  	"sort"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/prometheus/client_golang/prometheus"
    33  	githubql "github.com/shurcooL/githubv4"
    34  	"github.com/sirupsen/logrus"
    35  
    36  	"k8s.io/apimachinery/pkg/util/sets"
    37  	"k8s.io/test-infra/prow/config"
    38  	"k8s.io/test-infra/prow/git"
    39  	"k8s.io/test-infra/prow/github"
    40  	"k8s.io/test-infra/prow/kube"
    41  	"k8s.io/test-infra/prow/pjutil"
    42  	"k8s.io/test-infra/prow/tide/blockers"
    43  	"k8s.io/test-infra/prow/tide/history"
    44  )
    45  
    46  type kubeClient interface {
    47  	ListProwJobs(string) ([]kube.ProwJob, error)
    48  	CreateProwJob(kube.ProwJob) (kube.ProwJob, error)
    49  }
    50  
    51  type githubClient interface {
    52  	CreateStatus(string, string, string, github.Status) error
    53  	GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error)
    54  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
    55  	GetRef(string, string, string) (string, error)
    56  	Merge(string, string, int, github.MergeDetails) error
    57  	Query(context.Context, interface{}, map[string]interface{}) error
    58  }
    59  
    60  type contextChecker interface {
    61  	// IsOptional tells whether a context is optional.
    62  	IsOptional(string) bool
    63  	// MissingRequiredContexts tells if required contexts are missing from the list of contexts provided.
    64  	MissingRequiredContexts([]string) []string
    65  }
    66  
    67  // Controller knows how to sync PRs and PJs.
    68  type Controller struct {
    69  	logger *logrus.Entry
    70  	ca     *config.Agent
    71  	ghc    githubClient
    72  	kc     kubeClient
    73  	gc     *git.Client
    74  
    75  	sc *statusController
    76  
    77  	m     sync.Mutex
    78  	pools []Pool
    79  
    80  	// changedFiles caches the names of files changed by PRs.
    81  	// Cache entries expire if they are not used during a sync loop.
    82  	changedFiles *changedFilesAgent
    83  
    84  	History *history.History
    85  }
    86  
    87  // Action represents what actions the controller can take. It will take
    88  // exactly one action each sync.
    89  type Action string
    90  
    91  // Constants for various actions the controller might take
    92  const (
    93  	Wait         Action = "WAIT"
    94  	Trigger             = "TRIGGER"
    95  	TriggerBatch        = "TRIGGER_BATCH"
    96  	Merge               = "MERGE"
    97  	MergeBatch          = "MERGE_BATCH"
    98  	PoolBlocked         = "BLOCKED"
    99  )
   100  
   101  // recordableActions is the subset of actions that we keep historical record of.
   102  // Ignore idle actions to avoid flooding the records with useless data.
   103  var recordableActions = map[Action]bool{
   104  	Trigger:      true,
   105  	TriggerBatch: true,
   106  	Merge:        true,
   107  	MergeBatch:   true,
   108  }
   109  
   110  // Pool represents information about a tide pool. There is one for every
   111  // org/repo/branch combination that has PRs in the pool.
   112  type Pool struct {
   113  	Org    string
   114  	Repo   string
   115  	Branch string
   116  
   117  	// PRs with passing tests, pending tests, and missing or failed tests.
   118  	// Note that these results are rolled up. If all tests for a PR are passing
   119  	// except for one pending, it will be in PendingPRs.
   120  	SuccessPRs []PullRequest
   121  	PendingPRs []PullRequest
   122  	MissingPRs []PullRequest
   123  
   124  	// Empty if there is no pending batch.
   125  	BatchPending []PullRequest
   126  
   127  	// Which action did we last take, and to what target(s), if any.
   128  	Action   Action
   129  	Target   []PullRequest
   130  	Blockers []blockers.Blocker
   131  	Error    string
   132  }
   133  
   134  // Prometheus Metrics
   135  var (
   136  	tideMetrics = struct {
   137  		// Per pool
   138  		pooledPRs  *prometheus.GaugeVec
   139  		updateTime *prometheus.GaugeVec
   140  		merges     *prometheus.HistogramVec
   141  
   142  		// Singleton
   143  		syncDuration         prometheus.Gauge
   144  		statusUpdateDuration prometheus.Gauge
   145  	}{
   146  		pooledPRs: prometheus.NewGaugeVec(prometheus.GaugeOpts{
   147  			Name: "pooledprs",
   148  			Help: "Number of PRs in each Tide pool.",
   149  		}, []string{
   150  			"org",
   151  			"repo",
   152  			"branch",
   153  		}),
   154  		updateTime: prometheus.NewGaugeVec(prometheus.GaugeOpts{
   155  			Name: "updatetime",
   156  			Help: "The last time each subpool was synced. (Used to determine 'pooledprs' freshness.)",
   157  		}, []string{
   158  			"org",
   159  			"repo",
   160  			"branch",
   161  		}),
   162  
   163  		merges: prometheus.NewHistogramVec(prometheus.HistogramOpts{
   164  			Name:    "merges",
   165  			Help:    "Histogram of merges where values are the number of PRs merged together.",
   166  			Buckets: []float64{1, 2, 3, 4, 5, 7, 10, 15, 25},
   167  		}, []string{
   168  			"org",
   169  			"repo",
   170  			"branch",
   171  		}),
   172  
   173  		syncDuration: prometheus.NewGauge(prometheus.GaugeOpts{
   174  			Name: "syncdur",
   175  			Help: "The duration of the last loop of the sync controller.",
   176  		}),
   177  
   178  		statusUpdateDuration: prometheus.NewGauge(prometheus.GaugeOpts{
   179  			Name: "statusupdatedur",
   180  			Help: "The duration of the last loop of the status update controller.",
   181  		}),
   182  	}
   183  )
   184  
   185  func init() {
   186  	prometheus.MustRegister(tideMetrics.pooledPRs)
   187  	prometheus.MustRegister(tideMetrics.updateTime)
   188  	prometheus.MustRegister(tideMetrics.merges)
   189  	prometheus.MustRegister(tideMetrics.syncDuration)
   190  	prometheus.MustRegister(tideMetrics.statusUpdateDuration)
   191  }
   192  
   193  // NewController makes a Controller out of the given clients.
   194  func NewController(ghcSync, ghcStatus *github.Client, kc *kube.Client, ca *config.Agent, gc *git.Client, logger *logrus.Entry) *Controller {
   195  	if logger == nil {
   196  		logger = logrus.NewEntry(logrus.StandardLogger())
   197  	}
   198  	sc := &statusController{
   199  		logger:         logger.WithField("controller", "status-update"),
   200  		ghc:            ghcStatus,
   201  		ca:             ca,
   202  		newPoolPending: make(chan bool, 1),
   203  		shutDown:       make(chan bool),
   204  
   205  		trackedOrgs:  sets.NewString(),
   206  		trackedRepos: sets.NewString(),
   207  	}
   208  	go sc.run()
   209  	return &Controller{
   210  		logger: logger.WithField("controller", "sync"),
   211  		ghc:    ghcSync,
   212  		kc:     kc,
   213  		ca:     ca,
   214  		gc:     gc,
   215  		sc:     sc,
   216  		changedFiles: &changedFilesAgent{
   217  			ghc:             ghcSync,
   218  			nextChangeCache: make(map[changeCacheKey][]string),
   219  		},
   220  		History: history.New(1000),
   221  	}
   222  }
   223  
   224  // Shutdown signals the statusController to stop working and waits for it to
   225  // finish its last update loop before terminating.
   226  // Controller.Sync() should not be used after this function is called.
   227  func (c *Controller) Shutdown() {
   228  	c.sc.shutdown()
   229  }
   230  
   231  func prKey(pr *PullRequest) string {
   232  	return fmt.Sprintf("%s#%d", string(pr.Repository.NameWithOwner), int(pr.Number))
   233  }
   234  
   235  // org/repo#number -> pr
   236  func byRepoAndNumber(prs []PullRequest) map[string]PullRequest {
   237  	m := make(map[string]PullRequest)
   238  	for _, pr := range prs {
   239  		key := prKey(&pr)
   240  		m[key] = pr
   241  	}
   242  	return m
   243  }
   244  
   245  // newExpectedContext creates a Context with Expected state.
   246  func newExpectedContext(c string) Context {
   247  	return Context{
   248  		Context:     githubql.String(c),
   249  		State:       githubql.StatusStateExpected,
   250  		Description: githubql.String(""),
   251  	}
   252  }
   253  
   254  // contextsToStrings converts a list Context to a list of string
   255  func contextsToStrings(contexts []Context) []string {
   256  	var names []string
   257  	for _, c := range contexts {
   258  		names = append(names, string(c.Context))
   259  	}
   260  	return names
   261  }
   262  
   263  // Sync runs one sync iteration.
   264  func (c *Controller) Sync() error {
   265  	start := time.Now()
   266  	defer func() {
   267  		duration := time.Since(start)
   268  		c.logger.WithField("duration", duration.String()).Info("Synced")
   269  		tideMetrics.syncDuration.Set(duration.Seconds())
   270  	}()
   271  	defer c.changedFiles.prune()
   272  
   273  	ctx := context.Background()
   274  	c.logger.Debug("Building tide pool.")
   275  	prs := make(map[string]PullRequest)
   276  	for _, q := range c.ca.Config().Tide.Queries {
   277  		results, err := newSearchExecutor(ctx, c.ghc, c.logger, q.Query()).search()
   278  		if err != nil {
   279  			return err
   280  		}
   281  		for _, pr := range results {
   282  			prs[prKey(&pr)] = pr
   283  		}
   284  	}
   285  
   286  	var pjs []kube.ProwJob
   287  	var blocks blockers.Blockers
   288  	var err error
   289  	if len(prs) > 0 {
   290  		pjs, err = c.kc.ListProwJobs(kube.EmptySelector)
   291  		if err != nil {
   292  			return err
   293  		}
   294  
   295  		if label := c.ca.Config().Tide.BlockerLabel; label != "" {
   296  			c.logger.Debugf("Searching for blocking issues (label %q).", label)
   297  			orgExcepts, repos := c.ca.Config().Tide.Queries.OrgExceptionsAndRepos()
   298  			orgs := make([]string, 0, len(orgExcepts))
   299  			for org := range orgExcepts {
   300  				orgs = append(orgs, org)
   301  			}
   302  			orgRepoQuery := orgRepoQueryString(orgs, repos.UnsortedList(), orgExcepts)
   303  			blocks, err = blockers.FindAll(c.ghc, c.logger, label, orgRepoQuery)
   304  			if err != nil {
   305  				return err
   306  			}
   307  		}
   308  	}
   309  	// Partition PRs into subpools and filter out non-pool PRs.
   310  	rawPools, err := c.dividePool(prs, pjs)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	filteredPools := c.filterSubpools(c.ca.Config().Tide.MaxGoroutines, rawPools)
   315  
   316  	// Notify statusController about the new pool.
   317  	c.sc.Lock()
   318  	c.sc.poolPRs = poolPRMap(filteredPools)
   319  	select {
   320  	case c.sc.newPoolPending <- true:
   321  	default:
   322  	}
   323  	c.sc.Unlock()
   324  
   325  	// Sync subpools in parallel.
   326  	poolChan := make(chan Pool, len(filteredPools))
   327  	subpoolsInParallel(
   328  		c.ca.Config().Tide.MaxGoroutines,
   329  		filteredPools,
   330  		func(sp *subpool) {
   331  			pool, err := c.syncSubpool(*sp, blocks.GetApplicable(sp.org, sp.repo, sp.branch))
   332  			if err != nil {
   333  				sp.log.WithError(err).Errorf("Error syncing subpool.")
   334  			}
   335  			poolChan <- pool
   336  		},
   337  	)
   338  
   339  	close(poolChan)
   340  	pools := make([]Pool, 0, len(poolChan))
   341  	for pool := range poolChan {
   342  		pools = append(pools, pool)
   343  	}
   344  	sortPools(pools)
   345  	c.m.Lock()
   346  	defer c.m.Unlock()
   347  	c.pools = pools
   348  	return nil
   349  }
   350  
   351  func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   352  	c.m.Lock()
   353  	defer c.m.Unlock()
   354  	b, err := json.Marshal(c.pools)
   355  	if err != nil {
   356  		c.logger.WithError(err).Error("Encoding JSON.")
   357  		b = []byte("[]")
   358  	}
   359  	if _, err = w.Write(b); err != nil {
   360  		c.logger.WithError(err).Error("Writing JSON response.")
   361  	}
   362  }
   363  
   364  func subpoolsInParallel(goroutines int, sps map[string]*subpool, process func(*subpool)) {
   365  	// Load the subpools into a channel for use as a work queue.
   366  	queue := make(chan *subpool, len(sps))
   367  	for _, sp := range sps {
   368  		queue <- sp
   369  	}
   370  	close(queue)
   371  
   372  	if goroutines > len(queue) {
   373  		goroutines = len(queue)
   374  	}
   375  	wg := &sync.WaitGroup{}
   376  	wg.Add(goroutines)
   377  	for i := 0; i < goroutines; i++ {
   378  		go func() {
   379  			defer wg.Done()
   380  			for sp := range queue {
   381  				process(sp)
   382  			}
   383  		}()
   384  	}
   385  	wg.Wait()
   386  }
   387  
   388  // filterSubpools filters non-pool PRs out of the initially identified subpools,
   389  // deleting any pools that become empty.
   390  // See filterSubpool for filtering details.
   391  func (c *Controller) filterSubpools(goroutines int, raw map[string]*subpool) map[string]*subpool {
   392  	filtered := make(map[string]*subpool)
   393  	var lock sync.Mutex
   394  
   395  	subpoolsInParallel(
   396  		goroutines,
   397  		raw,
   398  		func(sp *subpool) {
   399  			if err := c.initSubpoolData(sp); err != nil {
   400  				sp.log.WithError(err).Error("Error initializing subpool.")
   401  				return
   402  			}
   403  			key := poolKey(sp.org, sp.repo, sp.branch)
   404  			if spFiltered := filterSubpool(c.ghc, sp); spFiltered != nil {
   405  				sp.log.WithField("key", key).WithField("pool", spFiltered).Debug("filtered sub-pool")
   406  
   407  				lock.Lock()
   408  				filtered[key] = spFiltered
   409  				lock.Unlock()
   410  			} else {
   411  				sp.log.WithField("key", key).WithField("pool", spFiltered).Debug("filtering sub-pool removed all PRs")
   412  			}
   413  		},
   414  	)
   415  	return filtered
   416  }
   417  
   418  func (c *Controller) initSubpoolData(sp *subpool) error {
   419  	var err error
   420  	sp.presubmitContexts, err = c.presubmitsByPull(sp)
   421  	if err != nil {
   422  		return fmt.Errorf("error determining required presubmit prowjobs: %v", err)
   423  	}
   424  	sp.cc, err = c.ca.Config().GetTideContextPolicy(sp.org, sp.repo, sp.branch)
   425  	if err != nil {
   426  		return fmt.Errorf("error setting up context checker: %v", err)
   427  	}
   428  	return nil
   429  }
   430  
   431  // filterSubpool filters PRs from an initially identified subpool, returning the
   432  // filtered subpool.
   433  // If the subpool becomes empty 'nil' is returned to indicate that the subpool
   434  // should be deleted.
   435  func filterSubpool(ghc githubClient, sp *subpool) *subpool {
   436  	var toKeep []PullRequest
   437  	for _, pr := range sp.prs {
   438  		if !filterPR(ghc, sp, &pr) {
   439  			toKeep = append(toKeep, pr)
   440  		}
   441  	}
   442  	if len(toKeep) == 0 {
   443  		return nil
   444  	}
   445  	sp.prs = toKeep
   446  	return sp
   447  }
   448  
   449  // filterPR indicates if a PR should be filtered out of the subpool.
   450  // Specifically we filter out PRs that:
   451  // - Have known merge conflicts.
   452  // - Have failing or missing status contexts.
   453  // - Have pending required status contexts that are not associated with a
   454  //   ProwJob. (This ensures that the 'tide' context indicates that the pending
   455  //   status is preventing merge. Required ProwJob statuses are allowed to be
   456  //   'pending' because this prevents kicking PRs from the pool when Tide is
   457  //   retesting them.)
   458  func filterPR(ghc githubClient, sp *subpool, pr *PullRequest) bool {
   459  	log := sp.log.WithFields(pr.logFields())
   460  	// Skip PRs that are known to be unmergeable.
   461  	if pr.Mergeable == githubql.MergeableStateConflicting {
   462  		log.Debug("filtering out PR as it is unmergeable")
   463  		return true
   464  	}
   465  	// Filter out PRs with unsuccessful contexts unless the only unsuccessful
   466  	// contexts are pending required prowjobs.
   467  	contexts, err := headContexts(log, ghc, pr)
   468  	if err != nil {
   469  		log.WithError(err).Error("Getting head contexts.")
   470  		return true
   471  	}
   472  	pjContexts := sp.presubmitContexts[int(pr.Number)]
   473  	for _, ctx := range unsuccessfulContexts(contexts, sp.cc, log) {
   474  		if ctx.State != githubql.StatusStatePending || !pjContexts.Has(string(ctx.Context)) {
   475  			log.WithField("context", ctx.Context).Debug("filtering out PR as unsuccessful context is not a pending Prow-controlled context")
   476  			return true
   477  		}
   478  	}
   479  
   480  	return false
   481  }
   482  
   483  // poolPRMap collects all subpool PRs into a map containing all pooled PRs.
   484  func poolPRMap(subpoolMap map[string]*subpool) map[string]PullRequest {
   485  	prs := make(map[string]PullRequest)
   486  	for _, sp := range subpoolMap {
   487  		for _, pr := range sp.prs {
   488  			prs[prKey(&pr)] = pr
   489  		}
   490  	}
   491  	return prs
   492  }
   493  
   494  type simpleState string
   495  
   496  const (
   497  	noneState    simpleState = "none"
   498  	pendingState simpleState = "pending"
   499  	successState simpleState = "success"
   500  )
   501  
   502  func toSimpleState(s kube.ProwJobState) simpleState {
   503  	if s == kube.TriggeredState || s == kube.PendingState {
   504  		return pendingState
   505  	} else if s == kube.SuccessState {
   506  		return successState
   507  	}
   508  	return noneState
   509  }
   510  
   511  // isPassingTests returns whether or not all contexts set on the PR except for
   512  // the tide pool context are passing.
   513  func isPassingTests(log *logrus.Entry, ghc githubClient, pr PullRequest, cc contextChecker) bool {
   514  	log = log.WithFields(pr.logFields())
   515  	contexts, err := headContexts(log, ghc, &pr)
   516  	if err != nil {
   517  		log.WithError(err).Error("Getting head commit status contexts.")
   518  		// If we can't get the status of the commit, assume that it is failing.
   519  		return false
   520  	}
   521  	unsuccessful := unsuccessfulContexts(contexts, cc, log)
   522  	return len(unsuccessful) == 0
   523  }
   524  
   525  // unsuccessfulContexts determines which contexts from the list that we care about are
   526  // failed. For instance, we do not care about our own context.
   527  // If the branchProtection is set to only check for required checks, we will skip
   528  // all non-required tests. If required tests are missing from the list, they will be
   529  // added to the list of failed contexts.
   530  func unsuccessfulContexts(contexts []Context, cc contextChecker, log *logrus.Entry) []Context {
   531  	var failed []Context
   532  	for _, ctx := range contexts {
   533  		if string(ctx.Context) == statusContext {
   534  			continue
   535  		}
   536  		if cc.IsOptional(string(ctx.Context)) {
   537  			continue
   538  		}
   539  		if ctx.State != githubql.StatusStateSuccess {
   540  			failed = append(failed, ctx)
   541  		}
   542  	}
   543  	for _, c := range cc.MissingRequiredContexts(contextsToStrings(contexts)) {
   544  		failed = append(failed, newExpectedContext(c))
   545  	}
   546  
   547  	log.Debugf("from %d total contexts (%v) found %d failing contexts: %v", len(contexts), contextsToStrings(contexts), len(failed), contextsToStrings(failed))
   548  	return failed
   549  }
   550  
   551  func pickSmallestPassingNumber(log *logrus.Entry, ghc githubClient, prs []PullRequest, cc contextChecker) (bool, PullRequest) {
   552  	smallestNumber := -1
   553  	var smallestPR PullRequest
   554  	for _, pr := range prs {
   555  		if smallestNumber != -1 && int(pr.Number) >= smallestNumber {
   556  			continue
   557  		}
   558  		if len(pr.Commits.Nodes) < 1 {
   559  			continue
   560  		}
   561  		if !isPassingTests(log, ghc, pr, cc) {
   562  			continue
   563  		}
   564  		smallestNumber = int(pr.Number)
   565  		smallestPR = pr
   566  	}
   567  	return smallestNumber > -1, smallestPR
   568  }
   569  
   570  // accumulateBatch returns a list of PRs that can be merged after passing batch
   571  // testing, if any exist. It also returns a list of PRs currently being batch
   572  // tested.
   573  func accumulateBatch(presubmits map[int]sets.String, prs []PullRequest, pjs []kube.ProwJob, log *logrus.Entry) ([]PullRequest, []PullRequest) {
   574  	log.Debug("accumulating PRs for batch testing")
   575  	if len(presubmits) == 0 {
   576  		log.Debug("no presubmits configured, no batch can be triggered")
   577  		return nil, nil
   578  	}
   579  	prNums := make(map[int]PullRequest)
   580  	for _, pr := range prs {
   581  		prNums[int(pr.Number)] = pr
   582  	}
   583  	type accState struct {
   584  		prs       []PullRequest
   585  		jobStates map[string]simpleState
   586  		// Are the pull requests in the ref still acceptable? That is, do they
   587  		// still point to the heads of the PRs?
   588  		validPulls bool
   589  	}
   590  	states := make(map[string]*accState)
   591  	for _, pj := range pjs {
   592  		if pj.Spec.Type != kube.BatchJob {
   593  			continue
   594  		}
   595  		// First validate the batch job's refs.
   596  		ref := pj.Spec.Refs.String()
   597  		if _, ok := states[ref]; !ok {
   598  			state := &accState{
   599  				jobStates:  make(map[string]simpleState),
   600  				validPulls: true,
   601  			}
   602  			for _, pull := range pj.Spec.Refs.Pulls {
   603  				if pr, ok := prNums[pull.Number]; ok && string(pr.HeadRefOID) == pull.SHA {
   604  					state.prs = append(state.prs, pr)
   605  				} else if !ok {
   606  					state.validPulls = false
   607  					log.WithField("batch", ref).WithFields(pr.logFields()).Debug("batch job invalid, PR left pool")
   608  					break
   609  				} else {
   610  					state.validPulls = false
   611  					log.WithField("batch", ref).WithFields(pr.logFields()).Debug("batch job invalid, PR HEAD changed")
   612  					break
   613  				}
   614  			}
   615  			states[ref] = state
   616  		}
   617  		if !states[ref].validPulls {
   618  			// The batch contains a PR ref that has changed. Skip it.
   619  			continue
   620  		}
   621  
   622  		// Batch job refs are valid. Now accumulate job states by batch ref.
   623  		context := pj.Spec.Context
   624  		jobState := toSimpleState(pj.Status.State)
   625  		// Store the best result for this ref+context.
   626  		if s, ok := states[ref].jobStates[context]; !ok || s == noneState || jobState == successState {
   627  			states[ref].jobStates[context] = jobState
   628  		}
   629  	}
   630  	var pendingBatch, successBatch []PullRequest
   631  	for ref, state := range states {
   632  		if !state.validPulls {
   633  			continue
   634  		}
   635  		requiredPresubmits := sets.NewString()
   636  		for _, pr := range state.prs {
   637  			requiredPresubmits = requiredPresubmits.Union(presubmits[int(pr.Number)])
   638  		}
   639  		overallState := successState
   640  		for _, p := range requiredPresubmits.List() {
   641  			if s, ok := state.jobStates[p]; !ok || s == noneState {
   642  				overallState = noneState
   643  				log.WithField("batch", ref).Debugf("batch invalid, required presubmit %s is not passing", p)
   644  				break
   645  			} else if s == pendingState && overallState == successState {
   646  				overallState = pendingState
   647  			}
   648  		}
   649  		switch overallState {
   650  		// Currently we only consider 1 pending batch and 1 success batch at a time.
   651  		// If more are somehow present they will be ignored.
   652  		case pendingState:
   653  			pendingBatch = state.prs
   654  		case successState:
   655  			successBatch = state.prs
   656  		}
   657  	}
   658  	return successBatch, pendingBatch
   659  }
   660  
   661  // accumulate returns the supplied PRs sorted into three buckets based on their
   662  // accumulated state across the presubmits.
   663  func accumulate(presubmits map[int]sets.String, prs []PullRequest, pjs []kube.ProwJob, log *logrus.Entry) (successes, pendings, nones []PullRequest) {
   664  	for _, pr := range prs {
   665  		// Accumulate the best result for each job.
   666  		psStates := make(map[string]simpleState)
   667  		for _, pj := range pjs {
   668  			if pj.Spec.Type != kube.PresubmitJob {
   669  				continue
   670  			}
   671  			if pj.Spec.Refs.Pulls[0].Number != int(pr.Number) {
   672  				continue
   673  			}
   674  			if pj.Spec.Refs.Pulls[0].SHA != string(pr.HeadRefOID) {
   675  				continue
   676  			}
   677  
   678  			name := pj.Spec.Context
   679  			oldState := psStates[name]
   680  			newState := toSimpleState(pj.Status.State)
   681  			if oldState == noneState || oldState == "" {
   682  				psStates[name] = newState
   683  			} else if oldState == pendingState && newState == successState {
   684  				psStates[name] = successState
   685  			}
   686  		}
   687  		// The overall result is the worst of the best.
   688  		overallState := successState
   689  		for _, ps := range presubmits[int(pr.Number)].List() {
   690  			if s, ok := psStates[ps]; !ok {
   691  				overallState = noneState
   692  				log.WithFields(pr.logFields()).Debugf("missing presubmit %s", ps)
   693  				break
   694  			} else if s == noneState {
   695  				overallState = noneState
   696  				log.WithFields(pr.logFields()).Debugf("presubmit %s not passing", ps)
   697  				break
   698  			} else if s == pendingState {
   699  				log.WithFields(pr.logFields()).Debugf("presubmit %s pending", ps)
   700  				overallState = pendingState
   701  			}
   702  		}
   703  		if overallState == successState {
   704  			successes = append(successes, pr)
   705  		} else if overallState == pendingState {
   706  			pendings = append(pendings, pr)
   707  		} else {
   708  			nones = append(nones, pr)
   709  		}
   710  	}
   711  	return
   712  }
   713  
   714  func prNumbers(prs []PullRequest) []int {
   715  	var nums []int
   716  	for _, pr := range prs {
   717  		nums = append(nums, int(pr.Number))
   718  	}
   719  	return nums
   720  }
   721  
   722  func (c *Controller) pickBatch(sp subpool, cc contextChecker) ([]PullRequest, error) {
   723  	// we must choose the oldest PRs for the batch
   724  	sort.Slice(sp.prs, func(i, j int) bool { return sp.prs[i].Number < sp.prs[j].Number })
   725  
   726  	var candidates []PullRequest
   727  	for _, pr := range sp.prs {
   728  		if isPassingTests(sp.log, c.ghc, pr, cc) {
   729  			candidates = append(candidates, pr)
   730  		}
   731  	}
   732  
   733  	if len(candidates) == 0 {
   734  		sp.log.Debugf("of %d possible PRs, none were passing tests, no batch will be created", len(sp.prs))
   735  		return nil, nil
   736  	}
   737  	sp.log.Debugf("of %d possible PRs, %d are passing tests", len(sp.prs), len(candidates))
   738  
   739  	r, err := c.gc.Clone(sp.org + "/" + sp.repo)
   740  	if err != nil {
   741  		return nil, err
   742  	}
   743  	defer r.Clean()
   744  	if err := r.Config("user.name", "prow"); err != nil {
   745  		return nil, err
   746  	}
   747  	if err := r.Config("user.email", "prow@localhost"); err != nil {
   748  		return nil, err
   749  	}
   750  	if err := r.Config("commit.gpgsign", "false"); err != nil {
   751  		sp.log.Warningf("Cannot set gpgsign=false in gitconfig: %v", err)
   752  	}
   753  	if err := r.Checkout(sp.sha); err != nil {
   754  		return nil, err
   755  	}
   756  
   757  	var res []PullRequest
   758  	for _, pr := range candidates {
   759  		if ok, err := r.Merge(string(pr.HeadRefOID)); err != nil {
   760  			// we failed to abort the merge and our git client is
   761  			// in a bad state; it must be cleaned before we try again
   762  			return nil, err
   763  		} else if ok {
   764  			res = append(res, pr)
   765  			// TODO: Make this configurable per subpool.
   766  			if len(res) == 5 {
   767  				break
   768  			}
   769  		}
   770  	}
   771  	return res, nil
   772  }
   773  
   774  func (c *Controller) mergePRs(sp subpool, prs []PullRequest) error {
   775  	var merged []int
   776  	defer func() {
   777  		if len(merged) == 0 {
   778  			return
   779  		}
   780  		tideMetrics.merges.WithLabelValues(sp.org, sp.repo, sp.branch).Observe(float64(len(merged)))
   781  	}()
   782  
   783  	augmentError := func(e error, pr PullRequest) error {
   784  		var batch string
   785  		if len(prs) > 1 {
   786  			batch = fmt.Sprintf(" from batch %v", prNumbers(prs))
   787  			if len(merged) > 0 {
   788  				batch = fmt.Sprintf("%s, partial merge %v", batch, merged)
   789  			}
   790  		}
   791  		return fmt.Errorf("failed merging #%d%s: %v", int(pr.Number), batch, e)
   792  	}
   793  
   794  	log := sp.log.WithField("merge-targets", prNumbers(prs))
   795  	maxRetries := 3
   796  	for i, pr := range prs {
   797  		backoff := time.Second * 4
   798  		log := log.WithFields(pr.logFields())
   799  		mergeMethod := c.ca.Config().Tide.MergeMethod(sp.org, sp.repo)
   800  		if squashLabel := c.ca.Config().Tide.SquashLabel; squashLabel != "" {
   801  			for _, prlabel := range pr.Labels.Nodes {
   802  				if string(prlabel.Name) == squashLabel {
   803  					mergeMethod = github.MergeSquash
   804  					break
   805  				}
   806  			}
   807  		}
   808  		for retry := 0; retry < maxRetries; retry++ {
   809  			if err := c.ghc.Merge(sp.org, sp.repo, int(pr.Number), github.MergeDetails{
   810  				SHA:         string(pr.HeadRefOID),
   811  				MergeMethod: string(mergeMethod),
   812  			}); err != nil {
   813  				// TODO: Add a config option to abort batches if a PR in the batch
   814  				// cannot be merged for any reason. This would skip merging
   815  				// not just the changed PR, but also the other PRs in the batch.
   816  				// This shouldn't be the default behavior as merging batches is high
   817  				// priority and this is unlikely to be problematic.
   818  				// Note: We would also need to be able to roll back any merges for the
   819  				// batch that were already successfully completed before the failure.
   820  				// Ref: https://github.com/kubernetes/test-infra/issues/10621
   821  				if _, ok := err.(github.ModifiedHeadError); ok {
   822  					// This is a possible source of incorrect behavior. If someone
   823  					// modifies their PR as we try to merge it in a batch then we
   824  					// end up in an untested state. This is unlikely to cause any
   825  					// real problems.
   826  					log.WithError(err).Warning("Merge failed: PR was modified.")
   827  					break
   828  				} else if _, ok = err.(github.UnmergablePRBaseChangedError); ok {
   829  					// Github complained that the base branch was modified. This is a
   830  					// strange error because the API doesn't even allow the request to
   831  					// specify the base branch sha, only the head sha.
   832  					// We suspect that github is complaining because we are making the
   833  					// merge requests too rapidly and it cannot recompute mergability
   834  					// in time. https://github.com/kubernetes/test-infra/issues/5171
   835  					// We handle this by sleeping for a few seconds before trying to
   836  					// merge again.
   837  					log.WithError(err).Warning("Merge failed: Base branch was modified.")
   838  					if retry+1 < maxRetries {
   839  						time.Sleep(backoff)
   840  						backoff *= 2
   841  					}
   842  				} else if _, ok = err.(github.UnauthorizedToPushError); ok {
   843  					// Github let us know that the token used cannot push to the branch.
   844  					// Even if the robot is set up to have write access to the repo, an
   845  					// overzealous branch protection setting will not allow the robot to
   846  					// push to a specific branch.
   847  					log.WithError(err).Error("Merge failed: Branch needs to be configured to allow this robot to push.")
   848  					// We won't be able to merge the other PRs.
   849  					return augmentError(err, pr)
   850  				} else if _, ok = err.(github.MergeCommitsForbiddenError); ok {
   851  					// Github let us know that the merge method configured for this repo
   852  					// is not allowed by other repo settings, so we should let the admins
   853  					// know that the configuration needs to be updated.
   854  					log.WithError(err).Error("Merge failed: Tide needs to be configured to use the 'rebase' merge method for this repo or the repo needs to allow merge commits.")
   855  					// We won't be able to merge the other PRs.
   856  					return augmentError(err, pr)
   857  				} else if _, ok = err.(github.UnmergablePRError); ok {
   858  					log.WithError(err).Error("Merge failed: PR is unmergable. Do the Tide merge requirements match the GitHub settings for the repo?")
   859  					break
   860  				} else {
   861  					log.WithError(err).Error("Merge failed.")
   862  					return augmentError(err, pr)
   863  				}
   864  			} else {
   865  				log.Info("Merged.")
   866  				merged = append(merged, int(pr.Number))
   867  				// If we have more PRs to merge, sleep to give Github time to recalculate
   868  				// mergeability.
   869  				if i+1 < len(prs) {
   870  					time.Sleep(time.Second * 3)
   871  				}
   872  				break
   873  			}
   874  		}
   875  	}
   876  	return nil
   877  }
   878  
   879  func (c *Controller) trigger(sp subpool, presubmitContexts map[int]sets.String, prs []PullRequest) error {
   880  	requiredContexts := sets.NewString()
   881  	for _, pr := range prs {
   882  		requiredContexts = requiredContexts.Union(presubmitContexts[int(pr.Number)])
   883  	}
   884  
   885  	// TODO(cjwagner): DRY this out when generalizing triggering code (and code to determine required and to-run jobs).
   886  	for _, ps := range c.ca.Config().Presubmits[sp.org+"/"+sp.repo] {
   887  		if ps.SkipReport || !ps.RunsAgainstBranch(sp.branch) || !requiredContexts.Has(ps.Context) {
   888  			continue
   889  		}
   890  
   891  		refs := kube.Refs{
   892  			Org:     sp.org,
   893  			Repo:    sp.repo,
   894  			BaseRef: sp.branch,
   895  			BaseSHA: sp.sha,
   896  		}
   897  		for _, pr := range prs {
   898  			refs.Pulls = append(
   899  				refs.Pulls,
   900  				kube.Pull{
   901  					Number: int(pr.Number),
   902  					Author: string(pr.Author.Login),
   903  					SHA:    string(pr.HeadRefOID),
   904  				},
   905  			)
   906  		}
   907  		var spec kube.ProwJobSpec
   908  		if len(prs) == 1 {
   909  			spec = pjutil.PresubmitSpec(ps, refs)
   910  		} else {
   911  			spec = pjutil.BatchSpec(ps, refs)
   912  		}
   913  		pj := pjutil.NewProwJob(spec, ps.Labels)
   914  		if _, err := c.kc.CreateProwJob(pj); err != nil {
   915  			return fmt.Errorf("failed to create a ProwJob for job: %q, PRs: %v: %v", spec.Job, prNumbers(prs), err)
   916  		}
   917  	}
   918  	return nil
   919  }
   920  
   921  func (c *Controller) takeAction(sp subpool, batchPending, successes, pendings, nones, batchMerges []PullRequest) (Action, []PullRequest, error) {
   922  	// Merge the batch!
   923  	if len(batchMerges) > 0 {
   924  		return MergeBatch, batchMerges, c.mergePRs(sp, batchMerges)
   925  	}
   926  	// Do not merge PRs while waiting for a batch to complete. We don't want to
   927  	// invalidate the old batch result.
   928  	if len(successes) > 0 && len(batchPending) == 0 {
   929  		if ok, pr := pickSmallestPassingNumber(sp.log, c.ghc, successes, sp.cc); ok {
   930  			return Merge, []PullRequest{pr}, c.mergePRs(sp, []PullRequest{pr})
   931  		}
   932  	}
   933  	// If no presubmits are configured, just wait.
   934  	if len(sp.presubmitContexts) == 0 {
   935  		return Wait, nil, nil
   936  	}
   937  	// If we have no serial jobs pending or successful, trigger one.
   938  	if len(nones) > 0 && len(pendings) == 0 && len(successes) == 0 {
   939  		if ok, pr := pickSmallestPassingNumber(sp.log, c.ghc, nones, sp.cc); ok {
   940  			return Trigger, []PullRequest{pr}, c.trigger(sp, sp.presubmitContexts, []PullRequest{pr})
   941  		}
   942  	}
   943  	// If we have no batch, trigger one.
   944  	if len(sp.prs) > 1 && len(batchPending) == 0 {
   945  		batch, err := c.pickBatch(sp, sp.cc)
   946  		if err != nil {
   947  			return Wait, nil, err
   948  		}
   949  		if len(batch) > 1 {
   950  			return TriggerBatch, batch, c.trigger(sp, sp.presubmitContexts, batch)
   951  		}
   952  	}
   953  	return Wait, nil, nil
   954  }
   955  
   956  // changedFilesAgent queries and caches the names of files changed by PRs.
   957  // Cache entries expire if they are not used during a sync loop.
   958  type changedFilesAgent struct {
   959  	ghc         githubClient
   960  	changeCache map[changeCacheKey][]string
   961  	// nextChangeCache caches file change info that is relevant this sync for use next sync.
   962  	// This becomes the new changeCache when prune() is called at the end of each sync.
   963  	nextChangeCache map[changeCacheKey][]string
   964  	sync.RWMutex
   965  }
   966  
   967  type changeCacheKey struct {
   968  	org, repo string
   969  	number    int
   970  	sha       string
   971  }
   972  
   973  // prChanges gets the files changed by the PR, either from the cache or by
   974  // querying GitHub.
   975  func (c *changedFilesAgent) prChanges(pr *PullRequest) ([]string, error) {
   976  	cacheKey := changeCacheKey{
   977  		org:    string(pr.Repository.Owner.Login),
   978  		repo:   string(pr.Repository.Name),
   979  		number: int(pr.Number),
   980  		sha:    string(pr.HeadRefOID),
   981  	}
   982  
   983  	c.RLock()
   984  	changedFiles, ok := c.changeCache[cacheKey]
   985  	if ok {
   986  		c.RUnlock()
   987  		c.Lock()
   988  		c.nextChangeCache[cacheKey] = changedFiles
   989  		c.Unlock()
   990  		return changedFiles, nil
   991  	}
   992  	if changedFiles, ok = c.nextChangeCache[cacheKey]; ok {
   993  		c.RUnlock()
   994  		return changedFiles, nil
   995  	}
   996  	c.RUnlock()
   997  
   998  	// We need to query the changes from GitHub.
   999  	changes, err := c.ghc.GetPullRequestChanges(
  1000  		string(pr.Repository.Owner.Login),
  1001  		string(pr.Repository.Name),
  1002  		int(pr.Number),
  1003  	)
  1004  	if err != nil {
  1005  		return nil, fmt.Errorf("error getting PR changes for #%d: %v", int(pr.Number), err)
  1006  	}
  1007  	changedFiles = make([]string, 0, len(changes))
  1008  	for _, change := range changes {
  1009  		changedFiles = append(changedFiles, change.Filename)
  1010  	}
  1011  
  1012  	c.Lock()
  1013  	c.nextChangeCache[cacheKey] = changedFiles
  1014  	c.Unlock()
  1015  	return changedFiles, nil
  1016  }
  1017  
  1018  // prune removes any cached file changes that were not used since the last prune.
  1019  func (c *changedFilesAgent) prune() {
  1020  	c.Lock()
  1021  	defer c.Unlock()
  1022  	c.changeCache = c.nextChangeCache
  1023  	c.nextChangeCache = make(map[changeCacheKey][]string)
  1024  }
  1025  
  1026  func (c *Controller) presubmitsByPull(sp *subpool) (map[int]sets.String, error) {
  1027  	presubmits := make(map[int]sets.String, len(sp.prs))
  1028  	record := func(num int, context string) {
  1029  		if jobs, ok := presubmits[num]; ok {
  1030  			jobs.Insert(context)
  1031  		} else {
  1032  			presubmits[num] = sets.NewString(context)
  1033  		}
  1034  	}
  1035  
  1036  	for _, ps := range c.ca.Config().Presubmits[sp.org+"/"+sp.repo] {
  1037  		if !ps.ContextRequired() || !ps.RunsAgainstBranch(sp.branch) {
  1038  			continue
  1039  		}
  1040  
  1041  		if ps.AlwaysRun {
  1042  			// Every PR requires this job.
  1043  			for _, pr := range sp.prs {
  1044  				record(int(pr.Number), ps.Context)
  1045  			}
  1046  		} else if ps.RunIfChanged != "" {
  1047  			// This is a run if changed job so we need to check if each PR requires it.
  1048  			for _, pr := range sp.prs {
  1049  				changedFiles, err := c.changedFiles.prChanges(&pr)
  1050  				if err != nil {
  1051  					return nil, err
  1052  				}
  1053  				if ps.RunsAgainstChanges(changedFiles) {
  1054  					record(int(pr.Number), ps.Context)
  1055  				}
  1056  			}
  1057  		}
  1058  	}
  1059  	return presubmits, nil
  1060  }
  1061  
  1062  func (c *Controller) syncSubpool(sp subpool, blocks []blockers.Blocker) (Pool, error) {
  1063  	sp.log.Infof("Syncing subpool: %d PRs, %d PJs.", len(sp.prs), len(sp.pjs))
  1064  	successes, pendings, nones := accumulate(sp.presubmitContexts, sp.prs, sp.pjs, sp.log)
  1065  	batchMerge, batchPending := accumulateBatch(sp.presubmitContexts, sp.prs, sp.pjs, sp.log)
  1066  	sp.log.WithFields(logrus.Fields{
  1067  		"prs-passing":   prNumbers(successes),
  1068  		"prs-pending":   prNumbers(pendings),
  1069  		"prs-missing":   prNumbers(nones),
  1070  		"batch-passing": prNumbers(batchMerge),
  1071  		"batch-pending": prNumbers(batchPending),
  1072  	}).Info("Subpool accumulated.")
  1073  
  1074  	var act Action
  1075  	var targets []PullRequest
  1076  	var err error
  1077  	var errorString string
  1078  	if len(blocks) > 0 {
  1079  		act = PoolBlocked
  1080  	} else {
  1081  		act, targets, err = c.takeAction(sp, batchPending, successes, pendings, nones, batchMerge)
  1082  		if err != nil {
  1083  			errorString = err.Error()
  1084  		}
  1085  		if recordableActions[act] {
  1086  			c.History.Record(
  1087  				poolKey(sp.org, sp.repo, sp.branch),
  1088  				string(act),
  1089  				sp.sha,
  1090  				errorString,
  1091  				prMeta(targets...),
  1092  			)
  1093  		}
  1094  	}
  1095  
  1096  	sp.log.WithFields(logrus.Fields{
  1097  		"action":  string(act),
  1098  		"targets": prNumbers(targets),
  1099  	}).Info("Subpool synced.")
  1100  	tideMetrics.pooledPRs.WithLabelValues(sp.org, sp.repo, sp.branch).Set(float64(len(sp.prs)))
  1101  	tideMetrics.updateTime.WithLabelValues(sp.org, sp.repo, sp.branch).Set(float64(time.Now().Unix()))
  1102  	return Pool{
  1103  			Org:    sp.org,
  1104  			Repo:   sp.repo,
  1105  			Branch: sp.branch,
  1106  
  1107  			SuccessPRs: successes,
  1108  			PendingPRs: pendings,
  1109  			MissingPRs: nones,
  1110  
  1111  			BatchPending: batchPending,
  1112  
  1113  			Action:   act,
  1114  			Target:   targets,
  1115  			Blockers: blocks,
  1116  			Error:    errorString,
  1117  		},
  1118  		err
  1119  }
  1120  
  1121  func prMeta(prs ...PullRequest) []history.PRMeta {
  1122  	var res []history.PRMeta
  1123  	for _, pr := range prs {
  1124  		res = append(res, history.PRMeta{
  1125  			Num:    int(pr.Number),
  1126  			Author: string(pr.Author.Login),
  1127  			Title:  string(pr.Title),
  1128  			SHA:    string(pr.HeadRefOID),
  1129  		})
  1130  	}
  1131  	return res
  1132  }
  1133  
  1134  func sortPools(pools []Pool) {
  1135  	sort.Slice(pools, func(i, j int) bool {
  1136  		if string(pools[i].Org) != string(pools[j].Org) {
  1137  			return string(pools[i].Org) < string(pools[j].Org)
  1138  		}
  1139  		if string(pools[i].Repo) != string(pools[j].Repo) {
  1140  			return string(pools[i].Repo) < string(pools[j].Repo)
  1141  		}
  1142  		return string(pools[i].Branch) < string(pools[j].Branch)
  1143  	})
  1144  
  1145  	sortPRs := func(prs []PullRequest) {
  1146  		sort.Slice(prs, func(i, j int) bool { return int(prs[i].Number) < int(prs[j].Number) })
  1147  	}
  1148  	for i := range pools {
  1149  		sortPRs(pools[i].SuccessPRs)
  1150  		sortPRs(pools[i].PendingPRs)
  1151  		sortPRs(pools[i].MissingPRs)
  1152  		sortPRs(pools[i].BatchPending)
  1153  	}
  1154  }
  1155  
  1156  type subpool struct {
  1157  	log    *logrus.Entry
  1158  	org    string
  1159  	repo   string
  1160  	branch string
  1161  	sha    string
  1162  
  1163  	pjs []kube.ProwJob
  1164  	prs []PullRequest
  1165  
  1166  	cc                contextChecker
  1167  	presubmitContexts map[int]sets.String
  1168  }
  1169  
  1170  func poolKey(org, repo, branch string) string {
  1171  	return fmt.Sprintf("%s/%s:%s", org, repo, branch)
  1172  }
  1173  
  1174  // dividePool splits up the list of pull requests and prow jobs into a group
  1175  // per repo and branch. It only keeps ProwJobs that match the latest branch.
  1176  func (c *Controller) dividePool(pool map[string]PullRequest, pjs []kube.ProwJob) (map[string]*subpool, error) {
  1177  	sps := make(map[string]*subpool)
  1178  	for _, pr := range pool {
  1179  		org := string(pr.Repository.Owner.Login)
  1180  		repo := string(pr.Repository.Name)
  1181  		branch := string(pr.BaseRef.Name)
  1182  		branchRef := string(pr.BaseRef.Prefix) + string(pr.BaseRef.Name)
  1183  		fn := poolKey(org, repo, branch)
  1184  		if sps[fn] == nil {
  1185  			sha, err := c.ghc.GetRef(org, repo, strings.TrimPrefix(branchRef, "refs/"))
  1186  			if err != nil {
  1187  				return nil, err
  1188  			}
  1189  			sps[fn] = &subpool{
  1190  				log: c.logger.WithFields(logrus.Fields{
  1191  					"org":      org,
  1192  					"repo":     repo,
  1193  					"branch":   branch,
  1194  					"base-sha": sha,
  1195  				}),
  1196  				org:    org,
  1197  				repo:   repo,
  1198  				branch: branch,
  1199  				sha:    sha,
  1200  			}
  1201  		}
  1202  		sps[fn].prs = append(sps[fn].prs, pr)
  1203  	}
  1204  	for _, pj := range pjs {
  1205  		if pj.Spec.Type != kube.PresubmitJob && pj.Spec.Type != kube.BatchJob {
  1206  			continue
  1207  		}
  1208  		fn := poolKey(pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.BaseRef)
  1209  		if sps[fn] == nil || pj.Spec.Refs.BaseSHA != sps[fn].sha {
  1210  			continue
  1211  		}
  1212  		sps[fn].pjs = append(sps[fn].pjs, pj)
  1213  	}
  1214  	return sps, nil
  1215  }
  1216  
  1217  // PullRequest holds graphql data about a PR, including its commits and their contexts.
  1218  type PullRequest struct {
  1219  	Number githubql.Int
  1220  	Author struct {
  1221  		Login githubql.String
  1222  	}
  1223  	BaseRef struct {
  1224  		Name   githubql.String
  1225  		Prefix githubql.String
  1226  	}
  1227  	HeadRefName githubql.String `graphql:"headRefName"`
  1228  	HeadRefOID  githubql.String `graphql:"headRefOid"`
  1229  	Mergeable   githubql.MergeableState
  1230  	Repository  struct {
  1231  		Name          githubql.String
  1232  		NameWithOwner githubql.String
  1233  		Owner         struct {
  1234  			Login githubql.String
  1235  		}
  1236  	}
  1237  	Commits struct {
  1238  		Nodes []struct {
  1239  			Commit Commit
  1240  		}
  1241  		// Request the 'last' 4 commits hoping that one of them is the logically 'last'
  1242  		// commit with OID matching HeadRefOID. If we don't find it we have to use an
  1243  		// additional API token. (see the 'headContexts' func for details)
  1244  		// We can't raise this too much or we could hit the limit of 50,000 nodes
  1245  		// per query: https://developer.github.com/v4/guides/resource-limitations/#node-limit
  1246  	} `graphql:"commits(last: 4)"`
  1247  	Labels struct {
  1248  		Nodes []struct {
  1249  			Name githubql.String
  1250  		}
  1251  	} `graphql:"labels(first: 100)"`
  1252  	Milestone *struct {
  1253  		Title githubql.String
  1254  	}
  1255  	Title githubql.String
  1256  }
  1257  
  1258  // Commit holds graphql data about commits and which contexts they have
  1259  type Commit struct {
  1260  	Status struct {
  1261  		Contexts []Context
  1262  	}
  1263  	OID githubql.String `graphql:"oid"`
  1264  }
  1265  
  1266  // Context holds graphql response data for github contexts.
  1267  type Context struct {
  1268  	Context     githubql.String
  1269  	Description githubql.String
  1270  	State       githubql.StatusState
  1271  }
  1272  
  1273  type searchQuery struct {
  1274  	RateLimit struct {
  1275  		Cost      githubql.Int
  1276  		Remaining githubql.Int
  1277  	}
  1278  	Search struct {
  1279  		PageInfo struct {
  1280  			HasNextPage githubql.Boolean
  1281  			EndCursor   githubql.String
  1282  		}
  1283  		IssueCount githubql.Int
  1284  		Nodes      []struct {
  1285  			PullRequest PullRequest `graphql:"... on PullRequest"`
  1286  		}
  1287  	} `graphql:"search(type: ISSUE, first: 100, after: $searchCursor, query: $query)"`
  1288  }
  1289  
  1290  func (pr *PullRequest) logFields() logrus.Fields {
  1291  	return logrus.Fields{
  1292  		"org":  string(pr.Repository.Owner.Login),
  1293  		"repo": string(pr.Repository.Name),
  1294  		"pr":   int(pr.Number),
  1295  		"sha":  string(pr.HeadRefOID),
  1296  	}
  1297  }
  1298  
  1299  // headContexts gets the status contexts for the commit with OID == pr.HeadRefOID
  1300  //
  1301  // First, we try to get this value from the commits we got with the PR query.
  1302  // Unfortunately the 'last' commit ordering is determined by author date
  1303  // not commit date so if commits are reordered non-chronologically on the PR
  1304  // branch the 'last' commit isn't necessarily the logically last commit.
  1305  // We list multiple commits with the query to increase our chance of success,
  1306  // but if we don't find the head commit we have to ask Github for it
  1307  // specifically (this costs an API token).
  1308  func headContexts(log *logrus.Entry, ghc githubClient, pr *PullRequest) ([]Context, error) {
  1309  	for _, node := range pr.Commits.Nodes {
  1310  		if node.Commit.OID == pr.HeadRefOID {
  1311  			return node.Commit.Status.Contexts, nil
  1312  		}
  1313  	}
  1314  	// We didn't get the head commit from the query (the commits must not be
  1315  	// logically ordered) so we need to specifically ask Github for the status
  1316  	// and coerce it to a graphql type.
  1317  	org := string(pr.Repository.Owner.Login)
  1318  	repo := string(pr.Repository.Name)
  1319  	// Log this event so we can tune the number of commits we list to minimize this.
  1320  	log.Warnf("'last' %d commits didn't contain logical last commit. Querying Github...", len(pr.Commits.Nodes))
  1321  	combined, err := ghc.GetCombinedStatus(org, repo, string(pr.HeadRefOID))
  1322  	if err != nil {
  1323  		return nil, fmt.Errorf("failed to get the combined status: %v", err)
  1324  	}
  1325  	contexts := make([]Context, 0, len(combined.Statuses))
  1326  	for _, status := range combined.Statuses {
  1327  		contexts = append(
  1328  			contexts,
  1329  			Context{
  1330  				Context:     githubql.String(status.Context),
  1331  				Description: githubql.String(status.Description),
  1332  				State:       githubql.StatusState(strings.ToUpper(status.State)),
  1333  			},
  1334  		)
  1335  	}
  1336  	// Add a commit with these contexts to pr for future look ups.
  1337  	pr.Commits.Nodes = append(pr.Commits.Nodes,
  1338  		struct{ Commit Commit }{
  1339  			Commit: Commit{
  1340  				OID:    pr.HeadRefOID,
  1341  				Status: struct{ Contexts []Context }{Contexts: contexts},
  1342  			},
  1343  		},
  1344  	)
  1345  	return contexts, nil
  1346  }
  1347  
  1348  func orgRepoQueryString(orgs, repos []string, orgExceptions map[string]sets.String) string {
  1349  	toks := make([]string, 0, len(orgs))
  1350  	for _, o := range orgs {
  1351  		toks = append(toks, fmt.Sprintf("org:\"%s\"", o))
  1352  
  1353  		for _, e := range orgExceptions[o].UnsortedList() {
  1354  			toks = append(toks, fmt.Sprintf("-repo:\"%s\"", e))
  1355  		}
  1356  	}
  1357  	for _, r := range repos {
  1358  		toks = append(toks, fmt.Sprintf("repo:\"%s\"", r))
  1359  	}
  1360  	return strings.Join(toks, " ")
  1361  }