github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/tide/blockers/blockers.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 blockers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"regexp"
    23  	"sort"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	githubql "github.com/shurcooL/githubv4"
    29  	"github.com/sirupsen/logrus"
    30  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  )
    33  
    34  var (
    35  	branchRE = regexp.MustCompile(`(?im)\bbranch:[^\w-]*([\w-./]+)\b`)
    36  )
    37  
    38  type githubClient interface {
    39  	QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error
    40  }
    41  
    42  // Blocker specifies an issue number that should block tide from merging.
    43  type Blocker struct {
    44  	Number     int
    45  	Title, URL string
    46  	// TODO: time blocked? (when blocker label was added)
    47  }
    48  
    49  type OrgRepo struct {
    50  	Org, Repo string
    51  }
    52  
    53  type OrgRepoBranch struct {
    54  	Org, Repo, Branch string
    55  }
    56  
    57  // Blockers holds maps of issues that are blocking various repos/branches.
    58  type Blockers struct {
    59  	Repo   map[OrgRepo][]Blocker       `json:"repo,omitempty"`
    60  	Branch map[OrgRepoBranch][]Blocker `json:"branch,omitempty"`
    61  }
    62  
    63  // GetApplicable returns the subset of blockers applicable to the specified branch.
    64  func (b Blockers) GetApplicable(org, repo, branch string) []Blocker {
    65  	var res []Blocker
    66  	res = append(res, b.Repo[OrgRepo{Org: org, Repo: repo}]...)
    67  	res = append(res, b.Branch[OrgRepoBranch{Org: org, Repo: repo, Branch: branch}]...)
    68  
    69  	sort.Slice(res, func(i, j int) bool {
    70  		return res[i].Number < res[j].Number
    71  	})
    72  	return res
    73  }
    74  
    75  // FindAll finds issues with label in the specified orgs/repos that should block tide.
    76  func FindAll(ghc githubClient, log *logrus.Entry, label string, orgRepoTokensByOrg map[string]string, splitQueryByOrg bool) (Blockers, error) {
    77  	queries := map[string]sets.Set[string]{}
    78  	for org, query := range orgRepoTokensByOrg {
    79  		if splitQueryByOrg {
    80  			queries[org] = sets.New[string](blockerQuery(label, query)...)
    81  		} else {
    82  			if queries[""] == nil {
    83  				queries[""] = sets.Set[string]{}
    84  			}
    85  			queries[""].Insert(blockerQuery(label, query)...)
    86  		}
    87  	}
    88  
    89  	var issues []Issue
    90  	var errs []error
    91  	var lock sync.Mutex
    92  	var wg sync.WaitGroup
    93  
    94  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    95  	defer cancel()
    96  	for org, query := range queries {
    97  		org, query := org, strings.Join(sets.List(query), " ")
    98  		wg.Add(1)
    99  
   100  		go func() {
   101  			defer wg.Done()
   102  			result, err := search(
   103  				ctx,
   104  				ghc,
   105  				org,
   106  				log,
   107  				query,
   108  			)
   109  			lock.Lock()
   110  			defer lock.Unlock()
   111  			if err != nil {
   112  				errs = append(errs, err)
   113  				return
   114  			}
   115  			issues = append(issues, result...)
   116  
   117  		}()
   118  
   119  	}
   120  	wg.Wait()
   121  
   122  	if err := utilerrors.NewAggregate(errs); err != nil {
   123  		return Blockers{}, fmt.Errorf("error searching for blocker issues: %w", err)
   124  	}
   125  
   126  	return fromIssues(issues, log), nil
   127  }
   128  
   129  func fromIssues(issues []Issue, log *logrus.Entry) Blockers {
   130  	log.Debugf("Finding blockers from %d issues.", len(issues))
   131  	res := Blockers{Repo: make(map[OrgRepo][]Blocker), Branch: make(map[OrgRepoBranch][]Blocker)}
   132  	for _, issue := range issues {
   133  		logger := log.WithFields(logrus.Fields{"org": issue.Repository.Owner.Login, "repo": issue.Repository.Name, "issue": issue.Number})
   134  		strippedTitle := branchRE.ReplaceAllLiteralString(string(issue.Title), "")
   135  		block := Blocker{
   136  			Number: int(issue.Number),
   137  			Title:  strippedTitle,
   138  			URL:    string(issue.URL),
   139  		}
   140  		if branches := parseBranches(string(issue.Title)); len(branches) > 0 {
   141  			for _, branch := range branches {
   142  				key := OrgRepoBranch{
   143  					Org:    string(issue.Repository.Owner.Login),
   144  					Repo:   string(issue.Repository.Name),
   145  					Branch: branch,
   146  				}
   147  				logger.WithField("branch", branch).Debug("Blocking merges to branch via issue.")
   148  				res.Branch[key] = append(res.Branch[key], block)
   149  			}
   150  		} else {
   151  			key := OrgRepo{
   152  				Org:  string(issue.Repository.Owner.Login),
   153  				Repo: string(issue.Repository.Name),
   154  			}
   155  			logger.Debug("Blocking merges to all branches via issue.")
   156  			res.Repo[key] = append(res.Repo[key], block)
   157  		}
   158  	}
   159  	return res
   160  }
   161  
   162  func blockerQuery(label, orgRepoTokens string) []string {
   163  	return append([]string{
   164  		"is:issue",
   165  		"state:open",
   166  		fmt.Sprintf("label:\"%s\"", label),
   167  	}, strings.Split(orgRepoTokens, " ")...)
   168  }
   169  
   170  func parseBranches(str string) []string {
   171  	var res []string
   172  	for _, match := range branchRE.FindAllStringSubmatch(str, -1) {
   173  		res = append(res, match[1])
   174  	}
   175  	return res
   176  }
   177  
   178  func search(ctx context.Context, ghc githubClient, githubOrg string, log *logrus.Entry, q string) ([]Issue, error) {
   179  	requestStart := time.Now()
   180  	var ret []Issue
   181  	vars := map[string]interface{}{
   182  		"query":        githubql.String(q),
   183  		"searchCursor": (*githubql.String)(nil),
   184  	}
   185  	var totalCost int
   186  	var remaining int
   187  	for {
   188  		sq := searchQuery{}
   189  		if err := ghc.QueryWithGitHubAppsSupport(ctx, &sq, vars, githubOrg); err != nil {
   190  			return nil, err
   191  		}
   192  		totalCost += int(sq.RateLimit.Cost)
   193  		remaining = int(sq.RateLimit.Remaining)
   194  		for _, n := range sq.Search.Nodes {
   195  			ret = append(ret, n.Issue)
   196  		}
   197  		if !sq.Search.PageInfo.HasNextPage {
   198  			break
   199  		}
   200  		vars["searchCursor"] = githubql.NewString(sq.Search.PageInfo.EndCursor)
   201  	}
   202  	log.WithFields(logrus.Fields{
   203  		"duration":       time.Since(requestStart).String(),
   204  		"pr_found_count": len(ret),
   205  		"query":          q,
   206  		"cost":           totalCost,
   207  		"remaining":      remaining,
   208  	}).Debug("Search for blocker query")
   209  	return ret, nil
   210  }
   211  
   212  // Issue holds graphql response data about issues
   213  type Issue struct {
   214  	Number     githubql.Int
   215  	Title      githubql.String
   216  	URL        githubql.String
   217  	Repository struct {
   218  		Name  githubql.String
   219  		Owner struct {
   220  			Login githubql.String
   221  		}
   222  	}
   223  }
   224  
   225  type searchQuery struct {
   226  	RateLimit struct {
   227  		Cost      githubql.Int
   228  		Remaining githubql.Int
   229  	}
   230  	Search struct {
   231  		PageInfo struct {
   232  			HasNextPage githubql.Boolean
   233  			EndCursor   githubql.String
   234  		}
   235  		Nodes []struct {
   236  			Issue Issue `graphql:"... on Issue"`
   237  		}
   238  	} `graphql:"search(type: ISSUE, first: 100, after: $searchCursor, query: $query)"`
   239  }