github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/override/override.go (about)

     1  /*
     2  Copyright 2018 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 override supports the /override context command.
    18  package override
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  
    27  	"github.com/sirupsen/logrus"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  
    31  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    32  	"sigs.k8s.io/prow/pkg/config"
    33  	"sigs.k8s.io/prow/pkg/git/v2"
    34  	"sigs.k8s.io/prow/pkg/github"
    35  	"sigs.k8s.io/prow/pkg/pjutil"
    36  	"sigs.k8s.io/prow/pkg/pluginhelp"
    37  	"sigs.k8s.io/prow/pkg/plugins"
    38  	"sigs.k8s.io/prow/pkg/repoowners"
    39  )
    40  
    41  const pluginName = "override"
    42  
    43  var (
    44  	overrideRe = regexp.MustCompile(`(?mi)^/override( ([^\r\n]+))?[\r\n]?$`)
    45  )
    46  
    47  type Context struct {
    48  	Context     string
    49  	Description string
    50  	State       string
    51  }
    52  
    53  type githubClient interface {
    54  	CreateComment(owner, repo string, number int, comment string) error
    55  	CreateStatus(org, repo, ref string, s github.Status) error
    56  	GetPullRequest(org, repo string, number int) (*github.PullRequest, error)
    57  	GetRef(org, repo, ref string) (string, error)
    58  	HasPermission(org, repo, user string, role ...string) (bool, error)
    59  	ListStatuses(org, repo, ref string) ([]github.Status, error)
    60  	GetBranchProtection(org, repo, branch string) (*github.BranchProtection, error)
    61  	ListTeams(org string) ([]github.Team, error)
    62  	ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error)
    63  	ListCheckRuns(org, repo, ref string) (*github.CheckRunList, error)
    64  	CreateCheckRun(org, repo string, checkRun github.CheckRun) error
    65  	UsesAppAuth() bool
    66  }
    67  
    68  type prowJobClient interface {
    69  	Create(context.Context, *prowapi.ProwJob, metav1.CreateOptions) (*prowapi.ProwJob, error)
    70  }
    71  
    72  type ownersClient interface {
    73  	LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error)
    74  }
    75  
    76  type overrideClient interface {
    77  	githubClient
    78  	prowJobClient
    79  	ownersClient
    80  	presubmits(org, repo string, baseSHAGetter config.RefGetter, headSHA string) ([]config.Presubmit, error)
    81  }
    82  
    83  type client struct {
    84  	ghc           githubClient
    85  	gc            git.ClientFactory
    86  	config        *config.Config
    87  	ownersClient  ownersClient
    88  	prowJobClient prowJobClient
    89  }
    90  
    91  func (c client) CreateComment(owner, repo string, number int, comment string) error {
    92  	return c.ghc.CreateComment(owner, repo, number, comment)
    93  }
    94  func (c client) CreateStatus(org, repo, ref string, s github.Status) error {
    95  	return c.ghc.CreateStatus(org, repo, ref, s)
    96  }
    97  
    98  func (c client) GetRef(org, repo, ref string) (string, error) {
    99  	return c.ghc.GetRef(org, repo, ref)
   100  }
   101  
   102  func (c client) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) {
   103  	return c.ghc.GetPullRequest(org, repo, number)
   104  }
   105  func (c client) ListStatuses(org, repo, ref string) ([]github.Status, error) {
   106  	return c.ghc.ListStatuses(org, repo, ref)
   107  }
   108  func (c client) GetBranchProtection(org, repo, branch string) (*github.BranchProtection, error) {
   109  	return c.ghc.GetBranchProtection(org, repo, branch)
   110  }
   111  func (c client) HasPermission(org, repo, user string, role ...string) (bool, error) {
   112  	return c.ghc.HasPermission(org, repo, user, role...)
   113  }
   114  func (c client) ListTeams(org string) ([]github.Team, error) {
   115  	return c.ghc.ListTeams(org)
   116  }
   117  func (c client) ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error) {
   118  	return c.ghc.ListTeamMembersBySlug(org, teamSlug, role)
   119  }
   120  func (c client) ListCheckRuns(org, teamSlug, role string) (*github.CheckRunList, error) {
   121  	return c.ghc.ListCheckRuns(org, teamSlug, role)
   122  }
   123  
   124  func (c client) CreateCheckRun(org, repo string, checkRun github.CheckRun) error {
   125  	return c.ghc.CreateCheckRun(org, repo, checkRun)
   126  }
   127  
   128  func (c client) UsesAppAuth() bool {
   129  	return c.ghc.UsesAppAuth()
   130  }
   131  
   132  func (c client) Create(ctx context.Context, pj *prowapi.ProwJob, o metav1.CreateOptions) (*prowapi.ProwJob, error) {
   133  	return c.prowJobClient.Create(ctx, pj, o)
   134  }
   135  
   136  func (c client) presubmits(org, repo string, baseSHAGetter config.RefGetter, headSHA string) ([]config.Presubmit, error) {
   137  	headSHAGetter := func() (string, error) {
   138  		return headSHA, nil
   139  	}
   140  	presubmits, err := c.config.GetPresubmits(c.gc, org+"/"+repo, "", baseSHAGetter, headSHAGetter)
   141  	if err != nil {
   142  		return nil, fmt.Errorf("failed to get presubmits: %w", err)
   143  	}
   144  	return presubmits, nil
   145  }
   146  
   147  func presubmitForContext(presubmits []config.Presubmit, context string) *config.Presubmit {
   148  	for _, p := range presubmits {
   149  		if p.Context == context {
   150  			return &p
   151  		}
   152  	}
   153  	return nil
   154  }
   155  
   156  func (c client) LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error) {
   157  	return c.ownersClient.LoadRepoOwners(org, repo, base)
   158  }
   159  
   160  func init() {
   161  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
   162  }
   163  
   164  func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
   165  	yamlSnippet, err := plugins.CommentMap.GenYaml(&plugins.Configuration{
   166  		Override: plugins.Override{
   167  			AllowTopLevelOwners: true,
   168  			AllowedGitHubTeams: map[string][]string{
   169  				"kubernetes/kubernetes": {"team1", "team2"},
   170  			},
   171  		},
   172  	})
   173  	if err != nil {
   174  		logrus.WithError(err).Warnf("cannot generate comments for %s plugin", pluginName)
   175  	}
   176  	pluginHelp := &pluginhelp.PluginHelp{
   177  		Description: "The override plugin allows repo admins to force a github status context to pass",
   178  		Snippet:     yamlSnippet,
   179  	}
   180  	overrideConfig := plugins.Override{}
   181  	if config != nil {
   182  		overrideConfig = config.Override
   183  	}
   184  	pluginHelp.AddCommand(pluginhelp.Command{
   185  		Usage:       "/override [context1] [context2]",
   186  		Description: "Forces github status contexts to green (multiple can be given). If the desired context has spaces, it must be quoted.",
   187  		Featured:    false,
   188  		WhoCanUse:   whoCanUse(overrideConfig, "", ""),
   189  		Examples:    []string{"/override pull-repo-whatever", "/override \"test / Unit Tests\"", "/override ci/circleci", "/override deleted-job other-job"},
   190  	})
   191  	return pluginHelp, nil
   192  }
   193  
   194  func whoCanUse(overrideConfig plugins.Override, org, repo string) string {
   195  	admins := "Repo administrators"
   196  	owners := ""
   197  	teams := ""
   198  
   199  	if overrideConfig.AllowTopLevelOwners {
   200  		owners = ", approvers in top level OWNERS file"
   201  	}
   202  
   203  	if len(overrideConfig.AllowedGitHubTeams) > 0 {
   204  		repoRef := fmt.Sprintf("%s/%s", org, repo)
   205  		var allTeams []string
   206  		for r, allowedTeams := range overrideConfig.AllowedGitHubTeams {
   207  			if repoRef == "/" || r == repoRef || r == org {
   208  				allTeams = append(allTeams, fmt.Sprintf("%s: %s", r, strings.Join(allowedTeams, " ")))
   209  			}
   210  		}
   211  		sort.Strings(allTeams)
   212  		teams = ", and the following github teams:" + strings.Join(allTeams, ", ")
   213  	}
   214  
   215  	return admins + owners + teams + "."
   216  }
   217  
   218  func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error {
   219  	c := client{
   220  		gc:            pc.GitClient,
   221  		ghc:           pc.GitHubClient,
   222  		config:        pc.Config,
   223  		prowJobClient: pc.ProwJobClient,
   224  		ownersClient:  pc.OwnersClient,
   225  	}
   226  	return handle(c, pc.Logger, &e, pc.PluginConfig.Override)
   227  }
   228  
   229  func authorizedUser(gc githubClient, log *logrus.Entry, org, repo, user string) bool {
   230  	ok, err := gc.HasPermission(org, repo, user, github.RoleAdmin)
   231  	if err != nil {
   232  		log.WithError(err).Warnf("cannot determine whether %s is an admin of %s/%s", user, org, repo)
   233  		return false
   234  	}
   235  	return ok
   236  }
   237  
   238  func authorizedTopLevelOwner(oc ownersClient, allowTopLevelOwners bool, log *logrus.Entry, org, repo, user string, pr *github.PullRequest) bool {
   239  	if allowTopLevelOwners {
   240  		owners, err := oc.LoadRepoOwners(org, repo, pr.Base.Ref)
   241  		if err != nil {
   242  			log.WithError(err).Warnf("cannot determine whether %s is a top level owner of %s/%s", user, org, repo)
   243  			return false
   244  		}
   245  		return owners.TopLevelApprovers().Has(github.NormLogin(user))
   246  	}
   247  	return false
   248  }
   249  
   250  func validateGitHubTeamSlugs(teamSlugs map[string][]string, org, repo string, githubTeams []github.Team) error {
   251  	validSlugs := sets.New[string]()
   252  	for _, team := range githubTeams {
   253  		validSlugs.Insert(team.Slug)
   254  	}
   255  	invalidSlugs := sets.New[string](teamSlugs[fmt.Sprintf("%s/%s", org, repo)]...).Difference(validSlugs)
   256  
   257  	if invalidSlugs.Len() > 0 {
   258  		return fmt.Errorf("invalid team slug(s): %s", strings.Join(sets.List(invalidSlugs), ","))
   259  	}
   260  	return nil
   261  }
   262  
   263  func authorizedGitHubTeamMember(gc githubClient, log *logrus.Entry, teamSlugs map[string][]string, org, repo, user string) bool {
   264  	teams, err := gc.ListTeams(org)
   265  	if err != nil {
   266  		log.WithError(err).Warnf("cannot get list of teams for org %s", org)
   267  		return false
   268  	}
   269  	if err := validateGitHubTeamSlugs(teamSlugs, org, repo, teams); err != nil {
   270  		log.WithError(err).Warnf("invalid team slug(s)")
   271  	}
   272  
   273  	slugs := teamSlugs[fmt.Sprintf("%s/%s", org, repo)]
   274  	slugs = append(slugs, teamSlugs[org]...)
   275  	for _, slug := range slugs {
   276  		members, err := gc.ListTeamMembersBySlug(org, slug, github.RoleAll)
   277  		if err != nil {
   278  			log.WithError(err).Warnf("cannot find members of team %s in org %s", slug, org)
   279  			continue
   280  		}
   281  		for _, member := range members {
   282  			if member.Login == user {
   283  				return true
   284  			}
   285  		}
   286  	}
   287  	return false
   288  }
   289  
   290  func description(user string) string {
   291  	return fmt.Sprintf("Overridden by %s", user)
   292  }
   293  
   294  func formatList(list []string) string {
   295  	var lines []string
   296  	for _, item := range list {
   297  		lines = append(lines, fmt.Sprintf(" - `%s`", item))
   298  	}
   299  	return strings.Join(lines, "\n")
   300  }
   301  
   302  type descriptionAndState struct {
   303  	description string
   304  	state       string
   305  }
   306  
   307  // Parses /override foo something
   308  func parseOverrideInput(in string) []string {
   309  	quoted := false
   310  	f := strings.FieldsFunc(in, func(r rune) bool {
   311  		if r == '"' {
   312  			quoted = !quoted
   313  		}
   314  		return !quoted && r == ' '
   315  	})
   316  	var retval []string
   317  	for _, val := range f {
   318  		retval = append(retval, strings.Trim(strings.TrimSpace(val), `"`))
   319  	}
   320  
   321  	return retval
   322  }
   323  
   324  func handle(oc overrideClient, log *logrus.Entry, e *github.GenericCommentEvent, options plugins.Override) error {
   325  
   326  	if !e.IsPR || e.IssueState != "open" || e.Action != github.GenericCommentActionCreated {
   327  		return nil
   328  	}
   329  
   330  	mat := overrideRe.FindAllStringSubmatch(e.Body, -1)
   331  	if len(mat) == 0 {
   332  		return nil // no /override commands given in the comment
   333  	}
   334  
   335  	org := e.Repo.Owner.Login
   336  	repo := e.Repo.Name
   337  	number := e.Number
   338  	user := e.User.Login
   339  
   340  	overrides := sets.New[string]()
   341  	for _, m := range mat {
   342  		if m[1] == "" {
   343  			resp := "/override requires failed status contexts to operate on, but none was given"
   344  			log.Debug(resp)
   345  			return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   346  		}
   347  		overrides.Insert(parseOverrideInput(m[2])...)
   348  	}
   349  
   350  	authorized := authorizedUser(oc, log, org, repo, user)
   351  	if !authorized && len(options.AllowedGitHubTeams) > 0 {
   352  		authorized = authorizedGitHubTeamMember(oc, log, options.AllowedGitHubTeams, org, repo, user)
   353  	}
   354  	if !authorized && !options.AllowTopLevelOwners {
   355  		resp := fmt.Sprintf("%s unauthorized: /override is restricted to %s", user, whoCanUse(options, org, repo))
   356  		log.Debug(resp)
   357  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   358  	}
   359  
   360  	pr, err := oc.GetPullRequest(org, repo, number)
   361  	if err != nil {
   362  		resp := fmt.Sprintf("Cannot get PR #%d in %s/%s", number, org, repo)
   363  		log.WithError(err).Warn(resp)
   364  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   365  	}
   366  
   367  	if !authorized && !authorizedTopLevelOwner(oc, options.AllowTopLevelOwners, log, org, repo, user, pr) {
   368  		resp := fmt.Sprintf("%s unauthorized: /override is restricted to %s", user, whoCanUse(options, org, repo))
   369  		log.Debug(resp)
   370  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   371  	}
   372  
   373  	sha := pr.Head.SHA
   374  	statuses, err := oc.ListStatuses(org, repo, sha)
   375  	if err != nil {
   376  		resp := fmt.Sprintf("Cannot get commit statuses for PR #%d in %s/%s", number, org, repo)
   377  		log.WithError(err).Warn(resp)
   378  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   379  	}
   380  
   381  	// Get CheckRuns and add them to contexts
   382  	var checkruns *github.CheckRunList
   383  	var checkrunContexts []Context
   384  	if oc.UsesAppAuth() {
   385  		checkruns, err = oc.ListCheckRuns(org, repo, sha)
   386  		if err != nil {
   387  			resp := fmt.Sprintf("Cannot get commit checkruns for PR #%d in %s/%s", number, org, repo)
   388  			log.WithError(err).Warn(resp)
   389  			return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   390  		}
   391  
   392  		checkrunContexts = make([]Context, len(checkruns.CheckRuns))
   393  		for _, checkrun := range checkruns.CheckRuns {
   394  			var state string
   395  			if checkrun.CompletedAt == "" {
   396  				state = "PENDING"
   397  			} else if strings.ToUpper(checkrun.Conclusion) == "NEUTRAL" {
   398  				state = "SUCCESS"
   399  			} else {
   400  				state = strings.ToUpper(checkrun.Conclusion)
   401  			}
   402  			checkrunContexts = append(checkrunContexts, Context{
   403  				Context:     checkrun.Name,
   404  				Description: checkrun.DetailsURL,
   405  				State:       state,
   406  			})
   407  		}
   408  
   409  		// dedupe checkruns and pick the best one
   410  		checkrunContexts = deduplicateContexts(checkrunContexts)
   411  	}
   412  
   413  	baseSHAGetter := shaGetterFactory(oc, org, repo, pr.Base.Ref)
   414  	presubmits, err := oc.presubmits(org, repo, baseSHAGetter, sha)
   415  	if err != nil {
   416  		msg := "Failed to get presubmits"
   417  		log.WithError(err).Error(msg)
   418  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, msg))
   419  	}
   420  
   421  	contexts := sets.New[string]()
   422  	for _, status := range statuses {
   423  		if status.State == github.StatusSuccess {
   424  			continue
   425  		}
   426  
   427  		contexts.Insert(status.Context)
   428  
   429  		for _, job := range presubmits {
   430  			if job.Context == status.Context {
   431  				contexts.Insert(job.Name)
   432  				break
   433  			}
   434  		}
   435  	}
   436  
   437  	// add all checkruns that are not successful or pending to the list of contexts being tracked
   438  	for _, cr := range checkrunContexts {
   439  		if cr.Context != "" && cr.State != "SUCCESS" && cr.State != "PENDING" {
   440  			contexts.Insert(cr.Context)
   441  		}
   442  	}
   443  
   444  	branch := pr.Base.Ref
   445  	branchProtection, err := oc.GetBranchProtection(org, repo, branch)
   446  	if err != nil {
   447  		resp := fmt.Sprintf("Cannot get branch protection for branch %s in %s/%s", branch, org, repo)
   448  		log.WithError(err).Warn(resp)
   449  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   450  	}
   451  
   452  	if branchProtection != nil && branchProtection.RequiredStatusChecks != nil {
   453  		for _, context := range branchProtection.RequiredStatusChecks.Contexts {
   454  			if !contexts.Has(context) {
   455  				contexts.Insert(context)
   456  				statuses = append(statuses, github.Status{Context: context})
   457  			}
   458  		}
   459  	}
   460  
   461  	if unknown := overrides.Difference(contexts); unknown.Len() > 0 {
   462  		resp := fmt.Sprintf(`/override requires failed status contexts, check run or a prowjob name to operate on.
   463  The following unknown contexts/checkruns were given:
   464  %s
   465  
   466  Only the following failed contexts/checkruns were expected:
   467  %s
   468  
   469  If you are trying to override a checkrun that has a space in it, you must put a double quote on the context.
   470  `, formatList(sets.List(unknown)), formatList(sets.List(contexts)))
   471  		log.Debug(resp)
   472  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   473  	}
   474  
   475  	done := sets.Set[string]{}
   476  	contextsWithCreatedJobs := sets.Set[string]{}
   477  
   478  	defer func() {
   479  		if len(done) == 0 {
   480  			return
   481  		}
   482  		msg := fmt.Sprintf("Overrode contexts on behalf of %s: %s", user, strings.Join(sets.List(done), ", "))
   483  		log.Info(msg)
   484  		oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, msg))
   485  	}()
   486  
   487  	for _, status := range statuses {
   488  		pre := presubmitForContext(presubmits, status.Context)
   489  		if status.State == github.StatusSuccess || !(overrides.Has(status.Context) || pre != nil && overrides.Has(pre.Name)) || contextsWithCreatedJobs.Has(status.Context) {
   490  			continue
   491  		}
   492  
   493  		// Create the overridden prow result if necessary
   494  		if pre != nil {
   495  			baseSHA, err := baseSHAGetter()
   496  			if err != nil {
   497  				resp := "Cannot get base ref of PR"
   498  				log.WithError(err).Warn(resp)
   499  				return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   500  			}
   501  
   502  			pj := pjutil.NewPresubmit(*pr, baseSHA, *pre, e.GUID, nil)
   503  			now := metav1.Now()
   504  			pj.Status = prowapi.ProwJobStatus{
   505  				StartTime:      now,
   506  				CompletionTime: &now,
   507  				State:          prowapi.SuccessState,
   508  				Description:    description(user),
   509  				URL:            e.HTMLURL,
   510  			}
   511  
   512  			log.WithFields(pjutil.ProwJobFields(&pj)).Info("Creating a new prowjob.")
   513  			if _, err := oc.Create(context.TODO(), &pj, metav1.CreateOptions{}); err != nil {
   514  				resp := fmt.Sprintf("Failed to create override job for %s", status.Context)
   515  				log.WithError(err).Warn(resp)
   516  				return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   517  			}
   518  			contextsWithCreatedJobs.Insert(status.Context)
   519  		}
   520  		status.State = github.StatusSuccess
   521  		status.Description = description(user)
   522  		if err := oc.CreateStatus(org, repo, sha, status); err != nil {
   523  			resp := fmt.Sprintf("Cannot update PR status for context %s", status.Context)
   524  			log.WithError(err).Warn(resp)
   525  			return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   526  		}
   527  		done.Insert(status.Context)
   528  	}
   529  
   530  	// We want to interate over the checkrunContexts, create a new checkrun with the same name as the context and mark it as successful.
   531  	// Tide has logic to pick the best checkrun result
   532  	// Checkruns have been converted to contexts and deduped
   533  	if oc.UsesAppAuth() {
   534  		for _, checkrun := range checkrunContexts {
   535  			if overrides.Has(checkrun.Context) {
   536  				prowOverrideCR := github.CheckRun{
   537  					Name:       checkrun.Context,
   538  					HeadSHA:    sha,
   539  					Status:     "completed",
   540  					Conclusion: "success",
   541  					Output: github.CheckRunOutput{
   542  						Title:   fmt.Sprintf("Prow override - %s", checkrun.Context),
   543  						Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", checkrun.Context),
   544  					},
   545  				}
   546  				if err := oc.CreateCheckRun(org, repo, prowOverrideCR); err != nil {
   547  					resp := fmt.Sprintf("cannot create prow-override CheckRun %v", prowOverrideCR)
   548  					log.WithError(err).Warn(resp)
   549  					return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   550  				}
   551  				done.Insert(checkrun.Context)
   552  			}
   553  		}
   554  	}
   555  
   556  	return nil
   557  }
   558  
   559  // shaGetterFactory is a closure to retrieve a sha once. It is not threadsafe.
   560  func shaGetterFactory(oc overrideClient, org, repo, ref string) func() (string, error) {
   561  	var baseSHA string
   562  	return func() (string, error) {
   563  		if baseSHA != "" {
   564  			return baseSHA, nil
   565  		}
   566  		var err error
   567  		baseSHA, err = oc.GetRef(org, repo, "heads/"+ref)
   568  		return baseSHA, err
   569  	}
   570  }
   571  
   572  func isStateBetter(previous, current string) bool {
   573  	if current == "SUCCESS" {
   574  		return true
   575  	}
   576  	if current == "PENDING" && (previous == "ERROR" || previous == "FAILURE" || previous == "EXPECTED") {
   577  		return true
   578  	}
   579  	if previous == "EXPECTED" && (current == "ERROR" || current == "FAILURE") {
   580  		return true
   581  	}
   582  
   583  	return false
   584  }
   585  
   586  // This function deduplicates checkruns and picks the best result
   587  func deduplicateContexts(contexts []Context) []Context {
   588  	result := map[string]descriptionAndState{}
   589  	for _, context := range contexts {
   590  		previousResult, found := result[context.Context]
   591  		if !found {
   592  			result[context.Context] = descriptionAndState{description: context.Description, state: context.State}
   593  			continue
   594  		}
   595  		if isStateBetter(previousResult.state, context.State) {
   596  			result[context.Context] = descriptionAndState{description: context.Description, state: context.State}
   597  		}
   598  	}
   599  
   600  	var resultSlice []Context
   601  	for name, descriptionAndState := range result {
   602  		resultSlice = append(resultSlice, Context{Context: name, Description: descriptionAndState.description, State: descriptionAndState.state})
   603  	}
   604  
   605  	return resultSlice
   606  }