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 }