sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/verify-owners/verify-owners.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 verifyowners
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  	"regexp"
    25  	"strconv"
    26  	"strings"
    27  
    28  	"github.com/sirupsen/logrus"
    29  
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  
    32  	"sigs.k8s.io/prow/pkg/config"
    33  	"sigs.k8s.io/prow/pkg/git/types"
    34  	"sigs.k8s.io/prow/pkg/git/v2"
    35  	"sigs.k8s.io/prow/pkg/github"
    36  	"sigs.k8s.io/prow/pkg/labels"
    37  	"sigs.k8s.io/prow/pkg/pluginhelp"
    38  	"sigs.k8s.io/prow/pkg/plugins"
    39  	"sigs.k8s.io/prow/pkg/plugins/golint"
    40  	"sigs.k8s.io/prow/pkg/plugins/ownersconfig"
    41  	"sigs.k8s.io/prow/pkg/plugins/trigger"
    42  	"sigs.k8s.io/prow/pkg/repoowners"
    43  )
    44  
    45  const (
    46  	// PluginName defines this plugin's registered name.
    47  	PluginName              = "verify-owners"
    48  	untrustedResponseFormat = `The following users are mentioned in %s file(s) but are untrusted for the following reasons. One way to make the user trusted is to add them as [members](%s) of the %s org. You can then trigger verification by writing ` + "`/verify-owners`" + ` in a comment.`
    49  )
    50  
    51  type nonTrustedReasons struct {
    52  	// files is a list of files they are being added in
    53  	files []string
    54  	// triggerReason is the reason that trigger's TrustedUser responds with for a failed trust check
    55  	triggerReason string
    56  }
    57  
    58  var (
    59  	verifyOwnersRe = regexp.MustCompile(`(?mi)^/verify-owners\s*$`)
    60  )
    61  
    62  func init() {
    63  	plugins.RegisterPullRequestHandler(PluginName, handlePullRequest, helpProvider)
    64  	plugins.RegisterGenericCommentHandler(PluginName, handleGenericCommentEvent, helpProvider)
    65  }
    66  
    67  func helpProvider(c *plugins.Configuration, orgRepo []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
    68  	pluginHelp := &pluginhelp.PluginHelp{
    69  		Description: fmt.Sprintf("The verify-owners plugin validates %s and %s files (by default) and ensures that they always contain collaborators of the org, if they are modified in a PR. On validation failure it automatically adds the '%s' label to the PR, and a review comment on the incriminating file(s). Per-repo configuration for filenames is possible.", ownersconfig.DefaultOwnersFile, ownersconfig.DefaultOwnersAliasesFile, labels.InvalidOwners),
    70  		Config:      map[string]string{},
    71  	}
    72  	defaultFilenames := c.OwnersFilenames("", "")
    73  	descriptionFor := func(filenames ownersconfig.Filenames) string {
    74  		description := fmt.Sprintf("%s and %s files are validated.", filenames.Owners, filenames.OwnersAliases)
    75  		if c.Owners.LabelsDenyList != nil {
    76  			description = fmt.Sprintf(`%s The verify-owners plugin will complain if %s files contain any of the following banned labels: %s.`,
    77  				description,
    78  				filenames.Owners,
    79  				strings.Join(c.Owners.LabelsDenyList, ", "))
    80  		}
    81  		return description
    82  	}
    83  	pluginHelp.Config["default"] = descriptionFor(defaultFilenames)
    84  	for _, item := range orgRepo {
    85  		filenames := c.OwnersFilenames(item.Org, item.Repo)
    86  		if !reflect.DeepEqual(filenames, defaultFilenames) {
    87  			pluginHelp.Config[item.String()] = descriptionFor(filenames)
    88  		}
    89  	}
    90  	pluginHelp.AddCommand(pluginhelp.Command{
    91  		Usage:       "/verify-owners",
    92  		Description: labels.InvalidOwners,
    93  		Featured:    false,
    94  		WhoCanUse:   "Anyone",
    95  		Examples:    []string{"/verify-owners"},
    96  	})
    97  	return pluginHelp, nil
    98  }
    99  
   100  type ownersClient interface {
   101  	ParseSimpleConfig(path string) (repoowners.SimpleConfig, error)
   102  	ParseFullConfig(path string) (repoowners.FullConfig, error)
   103  }
   104  
   105  type repoownersClient interface {
   106  	LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error)
   107  }
   108  
   109  type githubClient interface {
   110  	IsCollaborator(owner, repo, login string) (bool, error)
   111  	IsMember(org, user string) (bool, error)
   112  	AddLabel(org, repo string, number int, label string) error
   113  	CreateComment(owner, repo string, number int, comment string) error
   114  	CreateReview(org, repo string, number int, r github.DraftReview) error
   115  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
   116  	GetPullRequest(org, repo string, number int) (*github.PullRequest, error)
   117  	RemoveLabel(owner, repo string, number int, label string) error
   118  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
   119  	BotUserChecker() (func(candidate string) bool, error)
   120  }
   121  
   122  type commentPruner interface {
   123  	PruneComments(shouldPrune func(github.IssueComment) bool)
   124  }
   125  
   126  type info struct {
   127  	org          string
   128  	repo         string
   129  	repoFullName string
   130  	number       int
   131  }
   132  
   133  func handlePullRequest(pc plugins.Agent, pre github.PullRequestEvent) error {
   134  	if pre.Action != github.PullRequestActionOpened && pre.Action != github.PullRequestActionReopened && pre.Action != github.PullRequestActionSynchronize {
   135  		return nil
   136  	}
   137  
   138  	cp, err := pc.CommentPruner()
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	var skipTrustedUserCheck bool
   144  	for _, r := range pc.PluginConfig.Owners.SkipCollaborators {
   145  		if r == pre.Repo.FullName {
   146  			skipTrustedUserCheck = true
   147  			break
   148  		}
   149  	}
   150  
   151  	prInfo := info{
   152  		org:          pre.Repo.Owner.Login,
   153  		repo:         pre.Repo.Name,
   154  		repoFullName: pre.Repo.FullName,
   155  		number:       pre.Number,
   156  	}
   157  
   158  	return handle(pc.GitHubClient, pc.GitClient, pc.OwnersClient, pc.Logger, &pre.PullRequest, prInfo, pc.PluginConfig.Owners.LabelsDenyList, pc.PluginConfig.TriggerFor(pre.Repo.Owner.Login, pre.Repo.Name), skipTrustedUserCheck, cp, pc.PluginConfig.OwnersFilenames)
   159  }
   160  
   161  func handleGenericCommentEvent(pc plugins.Agent, e github.GenericCommentEvent) error {
   162  	cp, err := pc.CommentPruner()
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	var skipTrustedUserCheck bool
   168  	for _, r := range pc.PluginConfig.Owners.SkipCollaborators {
   169  		if r == e.Repo.FullName {
   170  			skipTrustedUserCheck = true
   171  			break
   172  		}
   173  	}
   174  
   175  	return handleGenericComment(pc.GitHubClient, pc.GitClient, pc.OwnersClient, pc.Logger, &e, pc.PluginConfig.Owners.LabelsDenyList, pc.PluginConfig.TriggerFor(e.Repo.Owner.Login, e.Repo.Name), skipTrustedUserCheck, cp, pc.PluginConfig.OwnersFilenames)
   176  }
   177  
   178  func handleGenericComment(ghc githubClient, gc git.ClientFactory, roc repoownersClient, log *logrus.Entry, ce *github.GenericCommentEvent, bannedLabels []string, triggerConfig plugins.Trigger, skipTrustedUserCheck bool, cp commentPruner, resolver ownersconfig.Resolver) error {
   179  	// Only consider open PRs and new comments.
   180  	if ce.IssueState != "open" || !ce.IsPR || ce.Action != github.GenericCommentActionCreated {
   181  		return nil
   182  	}
   183  
   184  	if !verifyOwnersRe.MatchString(ce.Body) {
   185  		return nil
   186  	}
   187  
   188  	prInfo := info{
   189  		org:          ce.Repo.Owner.Login,
   190  		repo:         ce.Repo.Name,
   191  		repoFullName: ce.Repo.FullName,
   192  		number:       ce.Number,
   193  	}
   194  
   195  	pr, err := ghc.GetPullRequest(ce.Repo.Owner.Login, ce.Repo.Name, ce.Number)
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	return handle(ghc, gc, roc, log, pr, prInfo, bannedLabels, triggerConfig, skipTrustedUserCheck, cp, resolver)
   201  }
   202  
   203  type messageWithLine struct {
   204  	line    int
   205  	message string
   206  }
   207  
   208  func handle(ghc githubClient, gc git.ClientFactory, roc repoownersClient, log *logrus.Entry, pr *github.PullRequest, info info, bannedLabels []string, triggerConfig plugins.Trigger, skipTrustedUserCheck bool, cp commentPruner, resolver ownersconfig.Resolver) error {
   209  	org := info.org
   210  	repo := info.repo
   211  	number := info.number
   212  	filenames := resolver(org, repo)
   213  	wrongOwnersFiles := map[string]messageWithLine{}
   214  
   215  	// Get changes.
   216  	changes, err := ghc.GetPullRequestChanges(org, repo, number)
   217  	if err != nil {
   218  		return fmt.Errorf("error getting PR changes: %w", err)
   219  	}
   220  
   221  	// List modified OWNERS files.
   222  	var modifiedOwnersFiles []github.PullRequestChange
   223  	for _, change := range changes {
   224  		if filepath.Base(change.Filename) == filenames.Owners && change.Status != github.PullRequestFileRemoved {
   225  			modifiedOwnersFiles = append(modifiedOwnersFiles, change)
   226  		}
   227  	}
   228  
   229  	// Check if the OWNERS_ALIASES file was modified.
   230  	var modifiedOwnerAliasesFile github.PullRequestChange
   231  	var ownerAliasesModified bool
   232  	for _, change := range changes {
   233  		if change.Filename == filenames.OwnersAliases {
   234  			modifiedOwnerAliasesFile = change
   235  			ownerAliasesModified = true
   236  			break
   237  		}
   238  	}
   239  
   240  	issueLabels, err := ghc.GetIssueLabels(org, repo, number)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	hasInvalidOwnersLabel := github.HasLabel(labels.InvalidOwners, issueLabels)
   245  
   246  	if len(modifiedOwnersFiles) == 0 && !ownerAliasesModified && !hasInvalidOwnersLabel {
   247  		return nil
   248  	}
   249  
   250  	// Clone the repo, checkout the PR.
   251  	r, err := gc.ClientFor(org, repo)
   252  	if err != nil {
   253  		return err
   254  	}
   255  	defer func() {
   256  		if err := r.Clean(); err != nil {
   257  			log.WithError(err).Error("Error cleaning up repo.")
   258  		}
   259  	}()
   260  	if err := r.Config("user.name", "prow"); err != nil {
   261  		return err
   262  	}
   263  	if err := r.Config("user.email", "prow@localhost"); err != nil {
   264  		return err
   265  	}
   266  	if err := r.Config("commit.gpgsign", "false"); err != nil {
   267  		log.WithError(err).Errorf("Cannot set gpgsign=false in gitconfig: %v", err)
   268  	}
   269  	if err := r.MergeAndCheckout(pr.Base.Ref, string(types.MergeMerge), pr.Head.SHA); err != nil {
   270  		return err
   271  	}
   272  	// If OWNERS_ALIASES file exists, get all aliases.
   273  	// If the file was modified, check for non trusted users in the newly added owners.
   274  	nonTrustedUsers, trustedUsers, repoAliases, err := nonTrustedUsersInOwnersAliases(ghc, log, triggerConfig, org, repo, r.Directory(), modifiedOwnerAliasesFile.Patch, ownerAliasesModified, skipTrustedUserCheck, filenames)
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	// Check if OWNERS files have the correct config and if they do,
   280  	// check if all newly added owners are trusted users.
   281  	oc, err := roc.LoadRepoOwners(org, repo, pr.Base.Ref)
   282  	if err != nil {
   283  		return fmt.Errorf("error loading RepoOwners: %w", err)
   284  	}
   285  
   286  	for _, c := range modifiedOwnersFiles {
   287  		path := filepath.Join(r.Directory(), c.Filename)
   288  		msg, owners := parseOwnersFile(oc, path, c, log, bannedLabels, filenames)
   289  		if msg != nil {
   290  			wrongOwnersFiles[c.Filename] = *msg
   291  			continue
   292  		}
   293  
   294  		if !skipTrustedUserCheck {
   295  			nonTrustedUsers, err = nonTrustedUsersInOwners(ghc, log, triggerConfig, org, repo, c.Patch, c.Filename, owners, nonTrustedUsers, trustedUsers, repoAliases)
   296  			if err != nil {
   297  				return err
   298  			}
   299  		}
   300  	}
   301  
   302  	if len(wrongOwnersFiles) > 0 {
   303  		s := "s"
   304  		if len(wrongOwnersFiles) == 1 {
   305  			s = ""
   306  		}
   307  		if !hasInvalidOwnersLabel {
   308  			if err := ghc.AddLabel(org, repo, number, labels.InvalidOwners); err != nil {
   309  				return err
   310  			}
   311  		}
   312  		log.Debugf("Creating a review for %d %s file%s.", len(wrongOwnersFiles), filenames.Owners, s)
   313  		var comments []github.DraftReviewComment
   314  		for errFile, err := range wrongOwnersFiles {
   315  			comments = append(comments, github.DraftReviewComment{
   316  				Path:     errFile,
   317  				Body:     err.message,
   318  				Position: err.line,
   319  			})
   320  		}
   321  		// Make the review body.
   322  		response := fmt.Sprintf("%d invalid %s file%s", len(wrongOwnersFiles), filenames.Owners, s)
   323  		draftReview := github.DraftReview{
   324  			Body:     plugins.FormatResponseRaw(pr.Body, pr.HTMLURL, pr.User.Login, response),
   325  			Action:   github.Comment,
   326  			Comments: comments,
   327  		}
   328  		if pr.Head.SHA != "" {
   329  			draftReview.CommitSHA = pr.Head.SHA
   330  		}
   331  		err := ghc.CreateReview(org, repo, number, draftReview)
   332  		if err != nil {
   333  			return fmt.Errorf("error creating a review for invalid %s file%s: %w", filenames.Owners, s, err)
   334  		}
   335  	}
   336  
   337  	var joinOrgURL string
   338  	if triggerConfig.JoinOrgURL != "" {
   339  		joinOrgURL = triggerConfig.JoinOrgURL
   340  	} else {
   341  		joinOrgURL = fmt.Sprintf("https://github.com/orgs/%s/people", org)
   342  	}
   343  
   344  	if len(nonTrustedUsers) > 0 {
   345  		if !hasInvalidOwnersLabel {
   346  			if err := ghc.AddLabel(org, repo, number, labels.InvalidOwners); err != nil {
   347  				return err
   348  			}
   349  		}
   350  
   351  		// prune old comments before adding a new one
   352  		cp.PruneComments(func(comment github.IssueComment) bool {
   353  			return strings.Contains(comment.Body, fmt.Sprintf(untrustedResponseFormat, filenames.Owners, joinOrgURL, org))
   354  		})
   355  		if err := ghc.CreateComment(org, repo, number, markdownFriendlyComment(org, joinOrgURL, nonTrustedUsers, filenames)); err != nil {
   356  			log.WithError(err).Errorf("Could not create comment for listing non-collaborators in %s files", filenames.Owners)
   357  		}
   358  	}
   359  
   360  	if len(wrongOwnersFiles) == 0 && len(nonTrustedUsers) == 0 {
   361  		// Don't bother checking if it has the label...it's a race, and we'll have
   362  		// to handle failure due to not being labeled anyway.
   363  		if err := ghc.RemoveLabel(org, repo, number, labels.InvalidOwners); err != nil {
   364  			return fmt.Errorf("failed removing %s label: %w", labels.InvalidOwners, err)
   365  		}
   366  		cp.PruneComments(func(comment github.IssueComment) bool {
   367  			return strings.Contains(comment.Body, fmt.Sprintf(untrustedResponseFormat, filenames.Owners, joinOrgURL, org))
   368  		})
   369  	}
   370  
   371  	return nil
   372  }
   373  
   374  func parseOwnersFile(oc ownersClient, path string, c github.PullRequestChange, log *logrus.Entry, bannedLabels []string, filenames ownersconfig.Filenames) (*messageWithLine, []string) {
   375  	var reviewers []string
   376  	var approvers []string
   377  	var labels []string
   378  
   379  	// by default we bind errors to line 1
   380  	lineNumber := 1
   381  	simple, err := oc.ParseSimpleConfig(path)
   382  	if err == filepath.SkipDir {
   383  		return nil, nil
   384  	}
   385  	if err != nil || simple.Empty() {
   386  		full, err := oc.ParseFullConfig(path)
   387  		if err == filepath.SkipDir {
   388  			return nil, nil
   389  		}
   390  		if err != nil {
   391  			lineNumberRe, _ := regexp.Compile(`line (\d+)`)
   392  			lineNumberMatches := lineNumberRe.FindStringSubmatch(err.Error())
   393  			// try to find a line number for the error
   394  			if len(lineNumberMatches) > 1 {
   395  				// we're sure it will convert as it passed the regexp already
   396  				absoluteLineNumber, _ := strconv.Atoi(lineNumberMatches[1])
   397  				// we need to convert it to a line number relative to the patch
   398  				al, err := golint.AddedLines(c.Patch)
   399  				if err != nil {
   400  					log.WithError(err).Errorf("Failed to compute added lines in %s: %v", c.Filename, err)
   401  				} else if val, ok := al[absoluteLineNumber]; ok {
   402  					lineNumber = val
   403  				}
   404  			}
   405  			return &messageWithLine{
   406  				lineNumber,
   407  				fmt.Sprintf("Cannot parse file: %v.", err),
   408  			}, nil
   409  		}
   410  		// it's a FullConfig
   411  		for _, config := range full.Filters {
   412  			reviewers = append(reviewers, config.Reviewers...)
   413  			approvers = append(approvers, config.Approvers...)
   414  			labels = append(labels, config.Labels...)
   415  		}
   416  	} else {
   417  		// it's a SimpleConfig
   418  		reviewers = simple.Config.Reviewers
   419  		approvers = simple.Config.Approvers
   420  		labels = simple.Config.Labels
   421  	}
   422  	// Check labels against ban list
   423  	if sets.New[string](labels...).HasAny(bannedLabels...) {
   424  		return &messageWithLine{
   425  			lineNumber,
   426  			fmt.Sprintf("File contains banned labels: %s.", sets.List(sets.New[string](labels...).Intersection(sets.New[string](bannedLabels...)))),
   427  		}, nil
   428  	}
   429  	// Check approvers isn't empty
   430  	if filepath.Dir(c.Filename) == "." && len(approvers) == 0 {
   431  		return &messageWithLine{
   432  			lineNumber,
   433  			fmt.Sprintf("No approvers defined in this root directory %s file.", filenames.Owners),
   434  		}, nil
   435  	}
   436  	owners := append(reviewers, approvers...)
   437  	return nil, owners
   438  }
   439  
   440  func markdownFriendlyComment(org, joinOrgURL string, nonTrustedUsers map[string]nonTrustedReasons, filenames ownersconfig.Filenames) string {
   441  	var commentLines []string
   442  	commentLines = append(commentLines, fmt.Sprintf(untrustedResponseFormat, filenames.Owners, joinOrgURL, org))
   443  
   444  	for user, reasons := range nonTrustedUsers {
   445  		commentLines = append(commentLines, fmt.Sprintf("- %s", user))
   446  		commentLines = append(commentLines, fmt.Sprintf("  - %s", reasons.triggerReason))
   447  		for _, filename := range reasons.files {
   448  			commentLines = append(commentLines, fmt.Sprintf("  - %s", filename))
   449  		}
   450  	}
   451  	return strings.Join(commentLines, "\n")
   452  }
   453  
   454  func nonTrustedUsersInOwnersAliases(ghc githubClient, log *logrus.Entry, triggerConfig plugins.Trigger, org, repo, dir, patch string, ownerAliasesModified, skipTrustedUserCheck bool, filenames ownersconfig.Filenames) (map[string]nonTrustedReasons, sets.Set[string], repoowners.RepoAliases, error) {
   455  	repoAliases := make(repoowners.RepoAliases)
   456  	// nonTrustedUsers is a map of non-trusted users to the reasons they were not trusted
   457  	nonTrustedUsers := map[string]nonTrustedReasons{}
   458  	trustedUsers := sets.Set[string]{}
   459  	var err error
   460  
   461  	// If OWNERS_ALIASES exists, get all aliases.
   462  	path := filepath.Join(dir, filenames.OwnersAliases)
   463  	if _, err := os.Stat(path); err == nil {
   464  		b, err := os.ReadFile(path)
   465  		if err != nil {
   466  			return nonTrustedUsers, trustedUsers, repoAliases, fmt.Errorf("Failed to read %s: %w", path, err)
   467  		}
   468  		repoAliases, err = repoowners.ParseAliasesConfig(b)
   469  		if err != nil {
   470  			return nonTrustedUsers, trustedUsers, repoAliases, fmt.Errorf("error parsing aliases config for %s file: %w", filenames.OwnersAliases, err)
   471  		}
   472  	}
   473  
   474  	// If OWNERS_ALIASES file was modified, check if newly added owners are trusted.
   475  	if ownerAliasesModified && !skipTrustedUserCheck {
   476  		allOwners := sets.List(repoAliases.ExpandAllAliases())
   477  		for _, owner := range allOwners {
   478  			nonTrustedUsers, err = checkIfTrustedUser(ghc, log, triggerConfig, owner, patch, filenames.OwnersAliases, org, repo, nonTrustedUsers, trustedUsers, repoAliases)
   479  			if err != nil {
   480  				return nonTrustedUsers, trustedUsers, repoAliases, err
   481  			}
   482  		}
   483  	}
   484  
   485  	return nonTrustedUsers, trustedUsers, repoAliases, nil
   486  }
   487  
   488  func nonTrustedUsersInOwners(ghc githubClient, log *logrus.Entry, triggerConfig plugins.Trigger, org, repo, patch, fileName string, owners []string, nonTrustedUsers map[string]nonTrustedReasons, trustedUsers sets.Set[string], repoAliases repoowners.RepoAliases) (map[string]nonTrustedReasons, error) {
   489  	var err error
   490  	for _, owner := range owners {
   491  		// ignore if owner is an alias
   492  		if _, ok := repoAliases[owner]; ok {
   493  			continue
   494  		}
   495  
   496  		nonTrustedUsers, err = checkIfTrustedUser(ghc, log, triggerConfig, owner, patch, fileName, org, repo, nonTrustedUsers, trustedUsers, repoAliases)
   497  		if err != nil {
   498  			return nonTrustedUsers, err
   499  		}
   500  	}
   501  	return nonTrustedUsers, nil
   502  }
   503  
   504  // checkIfTrustedUser looks for newly addded owners by checking if they are in the patch
   505  // and then checks if the owner is a trusted user.
   506  // returns a map from user to reasons for not being trusted
   507  func checkIfTrustedUser(ghc githubClient, log *logrus.Entry, triggerConfig plugins.Trigger, owner, patch, fileName, org, repo string, nonTrustedUsers map[string]nonTrustedReasons, trustedUsers sets.Set[string], repoAliases repoowners.RepoAliases) (map[string]nonTrustedReasons, error) {
   508  	// cap the number of checks to avoid exhausting tokens in case of large OWNERS refactors.
   509  	if len(nonTrustedUsers)+trustedUsers.Len() > 50 {
   510  		return nonTrustedUsers, nil
   511  	}
   512  	// only consider owners in the current patch
   513  	newOwnerRe, _ := regexp.Compile(fmt.Sprintf(`\+\s*-\s*\b%s\b`, owner))
   514  	if !newOwnerRe.MatchString(patch) {
   515  		return nonTrustedUsers, nil
   516  	}
   517  
   518  	// if we already flagged the owner for the current file, return early
   519  	if reasons, ok := nonTrustedUsers[owner]; ok {
   520  		for _, file := range reasons.files {
   521  			if file == fileName {
   522  				return nonTrustedUsers, nil
   523  			}
   524  		}
   525  		// have to separate assignment from map update due to map implementation (see "index expressions")
   526  		reasons.files = append(reasons.files, fileName)
   527  		nonTrustedUsers[owner] = reasons
   528  		return nonTrustedUsers, nil
   529  	}
   530  
   531  	isAlreadyTrusted := trustedUsers.Has(owner)
   532  	var err error
   533  	var triggerTrustedResponse trigger.TrustedUserResponse
   534  	if !isAlreadyTrusted {
   535  		triggerTrustedResponse, err = trigger.TrustedUser(ghc, triggerConfig.OnlyOrgMembers, triggerConfig.TrustedApps, triggerConfig.TrustedOrg, owner, org, repo)
   536  		if err != nil {
   537  			return nonTrustedUsers, err
   538  		}
   539  	}
   540  
   541  	if !isAlreadyTrusted && triggerTrustedResponse.IsTrusted {
   542  		trustedUsers.Insert(owner)
   543  	} else if !isAlreadyTrusted && !triggerTrustedResponse.IsTrusted {
   544  		if reasons, ok := nonTrustedUsers[owner]; ok {
   545  			reasons.triggerReason = triggerTrustedResponse.Reason
   546  			nonTrustedUsers[owner] = reasons
   547  		} else {
   548  			nonTrustedUsers[owner] = nonTrustedReasons{
   549  				// ensure that files is initialized to avoid nil pointer
   550  				files:         []string{},
   551  				triggerReason: triggerTrustedResponse.Reason,
   552  			}
   553  		}
   554  	}
   555  
   556  	return nonTrustedUsers, nil
   557  }