sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/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 config 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "regexp" 24 "sort" 25 "strings" 26 "sync" 27 "text/template" 28 29 "github.com/sirupsen/logrus" 30 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 utilerrors "k8s.io/apimachinery/pkg/util/errors" 33 "k8s.io/apimachinery/pkg/util/sets" 34 "sigs.k8s.io/prow/pkg/git/types" 35 "sigs.k8s.io/prow/pkg/git/v2" 36 ) 37 38 // TideQueries is a TideQuery slice. 39 type TideQueries []TideQuery 40 41 type TideBranchMergeType struct { 42 MergeType types.PullRequestMergeType 43 Regexpr *regexp.Regexp 44 } 45 46 func (tbmt TideBranchMergeType) Match(branch string) bool { 47 return tbmt.Regexpr.MatchString(branch) 48 } 49 50 func (tbmt TideBranchMergeType) MarshalJSON() ([]byte, error) { 51 return json.Marshal(tbmt.MergeType) 52 } 53 54 func (tbmt *TideBranchMergeType) UnmarshalJSON(b []byte) error { 55 return json.Unmarshal(b, &tbmt.MergeType) 56 } 57 58 type TideRepoMergeType struct { 59 Branches map[string]TideBranchMergeType 60 MergeType types.PullRequestMergeType 61 } 62 63 // When TideRepoMergeType.MergeType is present, unmarshal into: 64 // 65 // kubernetes: squash 66 // 67 // when TideRepoMergeType.Branches is not empty, unmarshal into: 68 // 69 // kubernetes: 70 // main: squash 71 func (trmt TideRepoMergeType) MarshalJSON() ([]byte, error) { 72 if trmt.MergeType != "" { 73 return json.Marshal(trmt.MergeType) 74 } 75 if trmt.Branches == nil || len(trmt.Branches) == 0 { 76 return json.Marshal("") 77 } 78 return json.Marshal(trmt.Branches) 79 } 80 81 // Full configuration: 82 // 83 // test-infra: 84 // main: merge 85 // 86 // unmarshal into map[string][TideBranchMergeType] 87 // 88 // Repo-wide configuration: 89 // 90 // test-infra: merge 91 // 92 // unmarshal into types.PullRequestMergeType 93 func (trmt *TideRepoMergeType) UnmarshalJSON(b []byte) error { 94 var mt types.PullRequestMergeType 95 if err := json.Unmarshal(b, &mt); err == nil { 96 trmt.MergeType = mt 97 return nil 98 } 99 var branches map[string]TideBranchMergeType 100 if err := json.Unmarshal(b, &branches); err != nil { 101 return err 102 } 103 trmt.Branches = branches 104 return nil 105 } 106 107 type TideOrgMergeType struct { 108 Repos map[string]TideRepoMergeType 109 MergeType types.PullRequestMergeType 110 } 111 112 // When TideOrgMergeType.MergeType is present, unmarshal into: 113 // 114 // kubernetes: squash 115 // 116 // when TideOrgMergeType.Repos is not empty, unmarshal into: 117 // 118 // kubernetes: 119 // test-infra: squash 120 func (tomt TideOrgMergeType) MarshalJSON() ([]byte, error) { 121 if tomt.MergeType != "" { 122 return json.Marshal(tomt.MergeType) 123 } 124 if tomt.Repos == nil || len(tomt.Repos) == 0 { 125 return json.Marshal("") 126 } 127 return json.Marshal(tomt.Repos) 128 } 129 130 // Org-wide configuration: 131 // 132 // kubernetes: merge 133 // 134 // unmarshal into types.PullRequestMergeType. 135 // 136 // Full configuration: 137 // 138 // kubernetes: 139 // test-infra: 140 // main: merge 141 // 142 // unmarshal into map[string][TideRepoMergeType]: 143 func (tomt *TideOrgMergeType) UnmarshalJSON(b []byte) error { 144 var mt types.PullRequestMergeType 145 if err := json.Unmarshal(b, &mt); err == nil { 146 tomt.MergeType = mt 147 return nil 148 } 149 var repos map[string]TideRepoMergeType 150 if err := json.Unmarshal(b, &repos); err != nil { 151 return err 152 } 153 tomt.Repos = repos 154 return nil 155 } 156 157 // TideContextPolicy configures options about how to handle various contexts. 158 type TideContextPolicy struct { 159 // whether to consider unknown contexts optional (skip) or required. 160 SkipUnknownContexts *bool `json:"skip-unknown-contexts,omitempty"` 161 RequiredContexts []string `json:"required-contexts,omitempty"` 162 RequiredIfPresentContexts []string `json:"required-if-present-contexts,omitempty"` 163 OptionalContexts []string `json:"optional-contexts,omitempty"` 164 // Infer required and optional jobs from Branch Protection configuration 165 FromBranchProtection *bool `json:"from-branch-protection,omitempty"` 166 } 167 168 // TideOrgContextPolicy overrides the policy for an org, and any repo overrides. 169 type TideOrgContextPolicy struct { 170 TideContextPolicy `json:",inline"` 171 Repos map[string]TideRepoContextPolicy `json:"repos,omitempty"` 172 } 173 174 // TideRepoContextPolicy overrides the policy for repo, and any branch overrides. 175 type TideRepoContextPolicy struct { 176 TideContextPolicy `json:",inline"` 177 Branches map[string]TideContextPolicy `json:"branches,omitempty"` 178 } 179 180 // TideContextPolicyOptions holds the default policy, and any org overrides. 181 type TideContextPolicyOptions struct { 182 TideContextPolicy `json:",inline"` 183 // GitHub Orgs 184 Orgs map[string]TideOrgContextPolicy `json:"orgs,omitempty"` 185 } 186 187 // TideMergeCommitTemplate holds templates to use for merge commits. 188 type TideMergeCommitTemplate struct { 189 TitleTemplate string `json:"title,omitempty"` 190 BodyTemplate string `json:"body,omitempty"` 191 192 Title *template.Template `json:"-"` 193 Body *template.Template `json:"-"` 194 } 195 196 // TidePriority contains a list of labels used to prioritize PRs in the merge pool 197 type TidePriority struct { 198 Labels []string `json:"labels,omitempty"` 199 } 200 201 // Tide is config for the tide pool. 202 type Tide struct { 203 Gerrit *TideGerritConfig `json:"gerrit,omitempty"` 204 // SyncPeriod specifies how often Tide will sync jobs with GitHub. Defaults to 1m. 205 SyncPeriod *metav1.Duration `json:"sync_period,omitempty"` 206 // MaxGoroutines is the maximum number of goroutines spawned inside the 207 // controller to handle org/repo:branch pools. Defaults to 20. Needs to be a 208 // positive number. 209 MaxGoroutines int `json:"max_goroutines,omitempty"` 210 // BatchSizeLimitMap is a key/value pair of an org or org/repo as the key and 211 // integer batch size limit as the value. Use "*" as key to set a global default. 212 // Special values: 213 // 0 => unlimited batch size 214 // -1 => batch merging disabled :( 215 BatchSizeLimitMap map[string]int `json:"batch_size_limit,omitempty"` 216 // PrioritizeExistingBatches configures on org or org/repo level if Tide should continue 217 // testing pre-existing batches instead of immediately including new PRs as they become 218 // eligible. Continuing on an old batch allows to re-use all existing test results whereas 219 // starting a new one requires to start new instances of all tests. 220 // Use '*' as key to set this globally. Defaults to true. 221 PrioritizeExistingBatchesMap map[string]bool `json:"prioritize_existing_batches,omitempty"` 222 223 TideGitHubConfig `json:",inline"` 224 } 225 226 // TideGitHubConfig is the tide config for GitHub. 227 type TideGitHubConfig struct { 228 // StatusUpdatePeriod specifies how often Tide will update GitHub status contexts. 229 // Defaults to the value of SyncPeriod. 230 StatusUpdatePeriod *metav1.Duration `json:"status_update_period,omitempty"` 231 // Queries represents a list of GitHub search queries that collectively 232 // specify the set of PRs that meet merge requirements. 233 Queries TideQueries `json:"queries,omitempty"` 234 235 // A key/value pair of an org/repo as the key and merge method to override 236 // the default method of merge. Valid options are squash, rebase, and merge. 237 MergeType map[string]TideOrgMergeType `json:"merge_method,omitempty"` 238 239 // A key/value pair of an org/repo as the key and Go template to override 240 // the default merge commit title and/or message. Template is passed the 241 // PullRequest struct (prow/github/types.go#PullRequest) 242 MergeTemplate map[string]TideMergeCommitTemplate `json:"merge_commit_template,omitempty"` 243 244 // URL for tide status contexts. 245 // We can consider allowing this to be set separately for separate repos, or 246 // allowing it to be a template. 247 TargetURL string `json:"target_url,omitempty"` 248 249 // TargetURLs is a map from "*", <org>, or <org/repo> to the URL for the tide status contexts. 250 // The most specific key that matches will be used. 251 // This field is mutually exclusive with TargetURL. 252 TargetURLs map[string]string `json:"target_urls,omitempty"` 253 254 // PRStatusBaseURL is the base URL for the PR status page. 255 // This is used to link to a merge requirements overview 256 // in the tide status context. 257 // Will be deprecated on June 2020. 258 PRStatusBaseURL string `json:"pr_status_base_url,omitempty"` 259 260 // PRStatusBaseURLs is the base URL for the PR status page 261 // mapped by org or org/repo level. 262 PRStatusBaseURLs map[string]string `json:"pr_status_base_urls,omitempty"` 263 264 // BlockerLabel is an optional label that is used to identify merge blocking 265 // GitHub issues. 266 // Leave this blank to disable this feature and save 1 API token per sync loop. 267 BlockerLabel string `json:"blocker_label,omitempty"` 268 269 // SquashLabel is an optional label that is used to identify PRs that should 270 // always be squash merged. 271 // Leave this blank to disable this feature. 272 SquashLabel string `json:"squash_label,omitempty"` 273 274 // RebaseLabel is an optional label that is used to identify PRs that should 275 // always be rebased and merged. 276 // Leave this blank to disable this feature. 277 RebaseLabel string `json:"rebase_label,omitempty"` 278 279 // MergeLabel is an optional label that is used to identify PRs that should 280 // always be merged with all individual commits from the PR. 281 // Leave this blank to disable this feature. 282 MergeLabel string `json:"merge_label,omitempty"` 283 284 // TideContextPolicyOptions defines merge options for context. If not set it will infer 285 // the required and optional contexts from the prow jobs configured and use the github 286 // combined status; otherwise it may apply the branch protection setting or let user 287 // define their own options in case branch protection is not used. 288 ContextOptions TideContextPolicyOptions `json:"context_options,omitempty"` 289 290 // BatchSizeLimitMap is a key/value pair of an org or org/repo as the key and 291 // integer batch size limit as the value. Use "*" as key to set a global default. 292 // Special values: 293 // 0 => unlimited batch size 294 // -1 => batch merging disabled :( 295 BatchSizeLimitMap map[string]int `json:"batch_size_limit,omitempty"` 296 297 // Priority is an ordered list of sets of labels that would be prioritized before other PRs 298 // PRs should match all labels contained in a set to be prioritized. The first entry has 299 // the highest priority. 300 Priority []TidePriority `json:"priority,omitempty"` 301 302 // DisplayAllQueriesInStatus controls if Tide should mention all queries in the status it 303 // creates. The default is to only mention the one to which we are closest (Calculated 304 // by total number of requirements - fulfilled number of requirements). 305 DisplayAllQueriesInStatus bool `json:"display_all_tide_queries_in_status,omitempty"` 306 } 307 308 // TideGerritConfig contains all Gerrit related configurations for tide. 309 type TideGerritConfig struct { 310 Queries GerritOrgRepoConfigs `json:"queries"` 311 // RateLimit defines how many changes to query per gerrit API call 312 // default is 5. 313 RateLimit int `json:"ratelimit,omitempty"` 314 } 315 316 func (t *Tide) mergeFrom(additional *Tide) error { 317 318 // Duplicate queries are pointless but not harmful, we 319 // have code to de-duplicate them down the line to not 320 // increase token usage needlessly. 321 t.Queries = append(t.Queries, additional.Queries...) 322 323 if t.MergeType == nil { 324 t.MergeType = additional.MergeType 325 return nil 326 } 327 328 var errs []error 329 for orgOrRepo, mergeMethod := range additional.MergeType { 330 if _, alreadyConfigured := t.MergeType[orgOrRepo]; alreadyConfigured { 331 errs = append(errs, fmt.Errorf("config for org or repo %s passed more than once", orgOrRepo)) 332 continue 333 } 334 t.MergeType[orgOrRepo] = mergeMethod 335 } 336 337 return utilerrors.NewAggregate(errs) 338 } 339 340 func (t *Tide) PrioritizeExistingBatches(repo OrgRepo) bool { 341 if val, set := t.PrioritizeExistingBatchesMap[repo.String()]; set { 342 return val 343 } 344 if val, set := t.PrioritizeExistingBatchesMap[repo.Org]; set { 345 return val 346 } 347 348 if val, set := t.PrioritizeExistingBatchesMap["*"]; set { 349 return val 350 } 351 352 return true 353 } 354 355 func (t *Tide) BatchSizeLimit(repo OrgRepo) int { 356 if limit, ok := t.BatchSizeLimitMap[repo.String()]; ok { 357 return limit 358 } 359 if limit, ok := t.BatchSizeLimitMap[repo.Org]; ok { 360 return limit 361 } 362 return t.BatchSizeLimitMap["*"] 363 } 364 365 // MergeMethod returns the merge method to use for a repo. The default of merge is 366 // returned when not overridden. 367 func (t *Tide) MergeMethod(repo OrgRepo) types.PullRequestMergeType { 368 return t.OrgRepoBranchMergeMethod(repo, "") 369 } 370 371 // OrgRepoBranchMergeMethod returns the merge method to use for a given triple: org, repo, branch. 372 // The following matching criteria apply, the priority goes from the highest to the lowest: 373 // 374 // 1. kubernetes/test-infra@main: rebase org/repo@branch shorthand 375 // 376 // 2. kubernetes: 377 // test-infra: 378 // ma(ster|in): rebase branch level regex 379 // 380 // 3. kubernetes/test-infra: rebase org/repo shorthand 381 // 382 // 4. kubernetes: 383 // test-infra: rebase repo-wide config 384 // 385 // 5. kubernetes: rebase org shorthand 386 // 387 // 6. default to "merge" 388 func (t *Tide) OrgRepoBranchMergeMethod(orgRepo OrgRepo, branch string) types.PullRequestMergeType { 389 isOrgSet, isRepoSet, isBranchSet := orgRepo.Org != "", orgRepo.Repo != "", branch != "" 390 var orgFound, repoFound bool 391 392 // The repository to look for can either be provided as an input or the "*" wildcard 393 repo := orgRepo.Repo 394 395 // Check if the org exists 396 if isOrgSet { 397 _, orgFound = t.MergeType[orgRepo.Org] 398 } 399 400 // Check if the repo exists 401 if isOrgSet && isRepoSet && orgFound { 402 _, repoFound = t.MergeType[orgRepo.Org].Repos[orgRepo.Repo] 403 _, wildcardRepoFound := t.MergeType[orgRepo.Org].Repos["*"] 404 if !repoFound && wildcardRepoFound { 405 repoFound = true 406 repo = "*" 407 } 408 } 409 410 // 1. "$org/$repo@$branch" shorthand 411 if isOrgSet && isRepoSet && isBranchSet { 412 orgRepoBranchShorthand := fmt.Sprintf("%s/%s@%s", orgRepo.Org, orgRepo.Repo, branch) 413 if orgRepoBranch, found := t.MergeType[orgRepoBranchShorthand]; found && orgRepoBranch.MergeType != "" { 414 return orgRepoBranch.MergeType 415 } 416 } 417 418 // 2. Branch level regex match 419 if orgFound && repoFound { 420 branches := t.MergeType[orgRepo.Org].Repos[repo].Branches 421 keys := make([]string, 0, len(branches)) 422 423 for k := range branches { 424 keys = append(keys, k) 425 } 426 sort.Strings(keys) 427 428 for _, key := range keys { 429 branchConfig := branches[key] 430 if branchConfig.Regexpr.MatchString(branch) { 431 return branchConfig.MergeType 432 } 433 } 434 } 435 436 // 3. "$org/$repo" shorthand 437 if isOrgSet && isRepoSet { 438 orgRepoShorthand := fmt.Sprintf("%s/%s", orgRepo.Org, orgRepo.Repo) 439 if orgRepo, found := t.MergeType[orgRepoShorthand]; found && orgRepo.MergeType != "" { 440 return orgRepo.MergeType 441 } 442 } 443 444 // 4. Repo-wide match 445 if orgFound && repoFound { 446 if t.MergeType[orgRepo.Org].Repos[repo].MergeType != "" { 447 return t.MergeType[orgRepo.Org].Repos[repo].MergeType 448 } 449 } 450 451 // 5. "$org" shorthand 452 if orgFound { 453 if t.MergeType[orgRepo.Org].MergeType != "" { 454 return t.MergeType[orgRepo.Org].MergeType 455 } 456 } 457 458 // 6. Default 459 return types.MergeMerge 460 } 461 462 // MergeCommitTemplate returns a struct with Go template string(s) or nil 463 func (t *Tide) MergeCommitTemplate(repo OrgRepo) TideMergeCommitTemplate { 464 v, ok := t.MergeTemplate[repo.String()] 465 if !ok { 466 return t.MergeTemplate[repo.Org] 467 } 468 469 return v 470 } 471 472 func (t *Tide) GetPRStatusBaseURL(repo OrgRepo) string { 473 if byOrgRepo, ok := t.PRStatusBaseURLs[repo.String()]; ok { 474 return byOrgRepo 475 } 476 if byOrg, ok := t.PRStatusBaseURLs[repo.Org]; ok { 477 return byOrg 478 } 479 480 return t.PRStatusBaseURLs["*"] 481 } 482 483 func (t *Tide) GetTargetURL(repo OrgRepo) string { 484 if byOrgRepo, ok := t.TargetURLs[repo.String()]; ok { 485 return byOrgRepo 486 } 487 if byOrg, ok := t.TargetURLs[repo.Org]; ok { 488 return byOrg 489 } 490 491 return t.TargetURLs["*"] 492 } 493 494 // TideQuery is turned into a GitHub search query. See the docs for details: 495 // https://help.github.com/articles/searching-issues-and-pull-requests/ 496 type TideQuery struct { 497 Author string `json:"author,omitempty"` 498 499 Labels []string `json:"labels,omitempty"` 500 MissingLabels []string `json:"missingLabels,omitempty"` 501 502 ExcludedBranches []string `json:"excludedBranches,omitempty"` 503 IncludedBranches []string `json:"includedBranches,omitempty"` 504 505 Milestone string `json:"milestone,omitempty"` 506 507 ReviewApprovedRequired bool `json:"reviewApprovedRequired,omitempty"` 508 509 Orgs []string `json:"orgs,omitempty"` 510 Repos []string `json:"repos,omitempty"` 511 ExcludedRepos []string `json:"excludedRepos,omitempty"` 512 } 513 514 func (q TideQuery) TenantIDs(cfg Config) []string { 515 res := sets.Set[string]{} 516 for _, org := range q.Orgs { 517 res.Insert(cfg.GetProwJobDefault(org, "*").TenantID) 518 } 519 for _, repo := range q.Repos { 520 res.Insert(cfg.GetProwJobDefault(repo, "*").TenantID) 521 } 522 return sets.List(res) 523 } 524 525 // tideQueryConfig contains the subset of attributes by which we de-duplicate 526 // tide queries. Together with tideQueryTarget it must contain the full set 527 // of all TideQuery properties. 528 type tideQueryConfig struct { 529 Author string 530 ExcludedBranches []string 531 IncludedBranches []string 532 Labels []string 533 MissingLabels []string 534 Milestone string 535 ReviewApprovedRequired bool 536 TenantIDs []string 537 } 538 539 type tideQueryTarget struct { 540 Orgs []string 541 Repos []string 542 ExcludedRepos []string 543 } 544 545 // constructQuery returns a map[org][]orgSpecificQueryParts (org, repo, -repo), remainingQueryString 546 func (tq *TideQuery) constructQuery() (map[string][]string, string) { 547 // map org->repo directives (if any) 548 orgScopedIdentifiers := map[string][]string{} 549 for _, o := range tq.Orgs { 550 if _, ok := orgScopedIdentifiers[o]; !ok { 551 orgScopedIdentifiers[o] = []string{fmt.Sprintf(`org:"%s"`, o)} 552 } 553 } 554 for _, r := range tq.Repos { 555 if org, _, ok := splitOrgRepoString(r); ok { 556 orgScopedIdentifiers[org] = append(orgScopedIdentifiers[org], fmt.Sprintf("repo:\"%s\"", r)) 557 } 558 } 559 560 for _, r := range tq.ExcludedRepos { 561 if org, _, ok := splitOrgRepoString(r); ok { 562 orgScopedIdentifiers[org] = append(orgScopedIdentifiers[org], fmt.Sprintf("-repo:\"%s\"", r)) 563 } 564 } 565 566 queryString := []string{"is:pr", "state:open", "archived:false"} 567 if tq.Author != "" { 568 queryString = append(queryString, fmt.Sprintf("author:\"%s\"", tq.Author)) 569 } 570 for _, b := range tq.ExcludedBranches { 571 queryString = append(queryString, fmt.Sprintf("-base:\"%s\"", b)) 572 } 573 for _, b := range tq.IncludedBranches { 574 queryString = append(queryString, fmt.Sprintf("base:\"%s\"", b)) 575 } 576 for _, l := range tq.Labels { 577 var orOperands []string 578 for _, alt := range strings.Split(l, ",") { 579 orOperands = append(orOperands, fmt.Sprintf("\"%s\"", alt)) 580 } 581 queryString = append(queryString, fmt.Sprintf("label:%s", strings.Join(orOperands, ","))) 582 } 583 for _, l := range tq.MissingLabels { 584 queryString = append(queryString, fmt.Sprintf("-label:\"%s\"", l)) 585 } 586 if tq.Milestone != "" { 587 queryString = append(queryString, fmt.Sprintf("milestone:\"%s\"", tq.Milestone)) 588 } 589 if tq.ReviewApprovedRequired { 590 queryString = append(queryString, "review:approved") 591 } 592 593 return orgScopedIdentifiers, strings.Join(queryString, " ") 594 } 595 596 func splitOrgRepoString(orgRepo string) (string, string, bool) { 597 split := strings.Split(orgRepo, "/") 598 if len(split) != 2 { 599 // Just do it like the github search itself and ignore invalid orgRepo identifiers 600 return "", "", false 601 } 602 return split[0], split[1], true 603 } 604 605 // OrgQueries returns the GitHub search string for the query, sharded 606 // by org. 607 func (tq *TideQuery) OrgQueries() map[string]string { 608 orgRepoIdentifiers, queryString := tq.constructQuery() 609 result := map[string]string{} 610 for org, repoIdentifiers := range orgRepoIdentifiers { 611 result[org] = queryString + " " + strings.Join(repoIdentifiers, " ") 612 } 613 614 return result 615 } 616 617 // Query returns the corresponding github search string for the tide query. 618 func (tq *TideQuery) Query() string { 619 orgRepoIdentifiers, queryString := tq.constructQuery() 620 toks := []string{queryString} 621 for _, repoIdentifiers := range orgRepoIdentifiers { 622 toks = append(toks, repoIdentifiers...) 623 } 624 return strings.Join(toks, " ") 625 } 626 627 // ForRepo indicates if the tide query applies to the specified repo. 628 func (tq TideQuery) ForRepo(repo OrgRepo) bool { 629 for _, queryOrg := range tq.Orgs { 630 if queryOrg != repo.Org { 631 continue 632 } 633 // Check for repos excluded from the org. 634 for _, excludedRepo := range tq.ExcludedRepos { 635 if excludedRepo == repo.String() { 636 return false 637 } 638 } 639 return true 640 } 641 for _, queryRepo := range tq.Repos { 642 if queryRepo == repo.String() { 643 return true 644 } 645 } 646 return false 647 } 648 649 func reposInOrg(org string, repos []string) []string { 650 prefix := org + "/" 651 var res []string 652 for _, repo := range repos { 653 if strings.HasPrefix(repo, prefix) { 654 res = append(res, repo) 655 } 656 } 657 return res 658 } 659 660 // OrgExceptionsAndRepos determines which orgs and repos a set of queries cover. 661 // Output is returned as a mapping from 'included org'->'repos excluded in the org' 662 // and a set of included repos. 663 func (tqs TideQueries) OrgExceptionsAndRepos() (map[string]sets.Set[string], sets.Set[string]) { 664 orgs := make(map[string]sets.Set[string]) 665 for i := range tqs { 666 for _, org := range tqs[i].Orgs { 667 applicableRepos := sets.New[string](reposInOrg(org, tqs[i].ExcludedRepos)...) 668 if excepts, ok := orgs[org]; !ok { 669 // We have not seen this org so the exceptions are just applicable 670 // members of 'excludedRepos'. 671 orgs[org] = applicableRepos 672 } else { 673 // We have seen this org so the exceptions are the applicable 674 // members of 'excludedRepos' intersected with existing exceptions. 675 orgs[org] = excepts.Intersection(applicableRepos) 676 } 677 } 678 } 679 repos := sets.New[string]() 680 for i := range tqs { 681 repos.Insert(tqs[i].Repos...) 682 } 683 // Remove any org exceptions that are explicitly included in a different query. 684 reposList := repos.UnsortedList() 685 for _, excepts := range orgs { 686 excepts.Delete(reposList...) 687 } 688 return orgs, repos 689 } 690 691 // QueryMap is a struct mapping from "org/repo" -> TideQueries that 692 // apply to that org or repo. It is lazily populated, but threadsafe. 693 type QueryMap struct { 694 queries TideQueries 695 696 cache map[string]TideQueries 697 sync.Mutex 698 } 699 700 // QueryMap creates a QueryMap from TideQueries 701 func (tqs TideQueries) QueryMap() *QueryMap { 702 return &QueryMap{ 703 queries: tqs, 704 cache: make(map[string]TideQueries), 705 } 706 } 707 708 // ForRepo returns the tide queries that apply to a repo. 709 func (qm *QueryMap) ForRepo(repo OrgRepo) TideQueries { 710 res := TideQueries(nil) 711 712 qm.Lock() 713 defer qm.Unlock() 714 715 if qs, ok := qm.cache[repo.String()]; ok { 716 return append(res, qs...) // Return a copy. 717 } 718 // Cache miss. Need to determine relevant queries. 719 720 for _, query := range qm.queries { 721 if query.ForRepo(repo) { 722 res = append(res, query) 723 } 724 } 725 qm.cache[repo.String()] = res 726 return res 727 } 728 729 // Validate returns an error if the query has any errors. 730 // 731 // Examples include: 732 // * an org name that is empty or includes a / 733 // * repos that are not org/repo 734 // * a label that is in both the labels and missing_labels section 735 // * a branch that is in both included and excluded branch set. 736 func (tq *TideQuery) Validate() error { 737 duplicates := func(field string, list []string) error { 738 dups := sets.New[string]() 739 seen := sets.New[string]() 740 for _, elem := range list { 741 if seen.Has(elem) { 742 dups.Insert(elem) 743 } else { 744 seen.Insert(elem) 745 } 746 } 747 dupCount := len(list) - seen.Len() 748 if dupCount == 0 { 749 return nil 750 } 751 return fmt.Errorf("%q contains %d duplicate entries: %s", field, dupCount, strings.Join(sets.List(dups), ", ")) 752 } 753 754 orgs := sets.New[string]() 755 for o := range tq.Orgs { 756 if strings.Contains(tq.Orgs[o], "/") { 757 return fmt.Errorf("orgs[%d]: %q contains a '/' which is not valid", o, tq.Orgs[o]) 758 } 759 if len(tq.Orgs[o]) == 0 { 760 return fmt.Errorf("orgs[%d]: is an empty string", o) 761 } 762 orgs.Insert(tq.Orgs[o]) 763 } 764 if err := duplicates("orgs", tq.Orgs); err != nil { 765 return err 766 } 767 768 for r := range tq.Repos { 769 parts := strings.Split(tq.Repos[r], "/") 770 if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { 771 return fmt.Errorf("repos[%d]: %q is not of the form \"org/repo\"", r, tq.Repos[r]) 772 } 773 if orgs.Has(parts[0]) { 774 return fmt.Errorf("repos[%d]: %q is already included via org: %q", r, tq.Repos[r], parts[0]) 775 } 776 } 777 if err := duplicates("repos", tq.Repos); err != nil { 778 return err 779 } 780 781 if len(tq.Orgs) == 0 && len(tq.Repos) == 0 { 782 return errors.New("'orgs' and 'repos' cannot both be empty") 783 } 784 785 for er := range tq.ExcludedRepos { 786 parts := strings.Split(tq.ExcludedRepos[er], "/") 787 if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { 788 return fmt.Errorf("excludedRepos[%d]: %q is not of the form \"org/repo\"", er, tq.ExcludedRepos[er]) 789 } 790 if !orgs.Has(parts[0]) { 791 return fmt.Errorf("excludedRepos[%d]: %q has no effect because org %q is not included", er, tq.ExcludedRepos[er], parts[0]) 792 } 793 // Note: At this point we also know that this excludedRepo is not found in 'repos'. 794 } 795 if err := duplicates("excludedRepos", tq.ExcludedRepos); err != nil { 796 return err 797 } 798 799 if invalids := sets.New[string](tq.Labels...).Intersection(sets.New[string](tq.MissingLabels...)); len(invalids) > 0 { 800 return fmt.Errorf("the labels: %q are both required and forbidden", sets.List(invalids)) 801 } 802 if err := duplicates("labels", tq.Labels); err != nil { 803 return err 804 } 805 if err := duplicates("missingLabels", tq.MissingLabels); err != nil { 806 return err 807 } 808 809 if len(tq.ExcludedBranches) > 0 && len(tq.IncludedBranches) > 0 { 810 return errors.New("both 'includedBranches' and 'excludedBranches' are specified ('excludedBranches' have no effect)") 811 } 812 if err := duplicates("includedBranches", tq.IncludedBranches); err != nil { 813 return err 814 } 815 if err := duplicates("excludedBranches", tq.ExcludedBranches); err != nil { 816 return err 817 } 818 819 return nil 820 } 821 822 // Validate returns an error if any contexts are listed more than once in the config. 823 func (cp *TideContextPolicy) Validate() error { 824 if inter := sets.New[string](cp.RequiredContexts...).Intersection(sets.New[string](cp.OptionalContexts...)); inter.Len() > 0 { 825 return fmt.Errorf("contexts %s are defined as required and optional", strings.Join(sets.List(inter), ", ")) 826 } 827 if inter := sets.New[string](cp.RequiredContexts...).Intersection(sets.New[string](cp.RequiredIfPresentContexts...)); inter.Len() > 0 { 828 return fmt.Errorf("contexts %s are defined as required and required if present", strings.Join(sets.List(inter), ", ")) 829 } 830 if inter := sets.New[string](cp.OptionalContexts...).Intersection(sets.New[string](cp.RequiredIfPresentContexts...)); inter.Len() > 0 { 831 return fmt.Errorf("contexts %s are defined as optional and required if present", strings.Join(sets.List(inter), ", ")) 832 } 833 return nil 834 } 835 836 func mergeTideContextPolicy(a, b TideContextPolicy) TideContextPolicy { 837 mergeBool := func(a, b *bool) *bool { 838 if b == nil { 839 return a 840 } 841 return b 842 } 843 c := TideContextPolicy{} 844 c.FromBranchProtection = mergeBool(a.FromBranchProtection, b.FromBranchProtection) 845 c.SkipUnknownContexts = mergeBool(a.SkipUnknownContexts, b.SkipUnknownContexts) 846 required := sets.New[string](a.RequiredContexts...) 847 requiredIfPresent := sets.New[string](a.RequiredIfPresentContexts...) 848 optional := sets.New[string](a.OptionalContexts...) 849 required.Insert(b.RequiredContexts...) 850 requiredIfPresent.Insert(b.RequiredIfPresentContexts...) 851 optional.Insert(b.OptionalContexts...) 852 if required.Len() > 0 { 853 c.RequiredContexts = sets.List(required) 854 } 855 if requiredIfPresent.Len() > 0 { 856 c.RequiredIfPresentContexts = sets.List(requiredIfPresent) 857 } 858 if optional.Len() > 0 { 859 c.OptionalContexts = sets.List(optional) 860 } 861 return c 862 } 863 864 func ParseTideContextPolicyOptions(org, repo, branch string, options TideContextPolicyOptions) TideContextPolicy { 865 option := options.TideContextPolicy 866 if o, ok := options.Orgs[org]; ok { 867 option = mergeTideContextPolicy(option, o.TideContextPolicy) 868 if r, ok := o.Repos[repo]; ok { 869 option = mergeTideContextPolicy(option, r.TideContextPolicy) 870 if b, ok := r.Branches[branch]; ok { 871 option = mergeTideContextPolicy(option, b) 872 } 873 } 874 } 875 return option 876 } 877 878 // GetTideContextPolicy parses the prow config to find context merge options. 879 // If none are set, it will use the prow jobs configured and use the default github combined status. 880 // Otherwise if set it will use the branch protection setting, or the listed jobs. 881 func (c Config) GetTideContextPolicy(gitClient git.ClientFactory, org, repo, branch string, baseSHAGetter RefGetter, headSHA string) (*TideContextPolicy, error) { 882 var requireManuallyTriggeredJobs *bool 883 options := ParseTideContextPolicyOptions(org, repo, branch, c.Tide.ContextOptions) 884 // Adding required and optional contexts from options 885 required := sets.New[string](options.RequiredContexts...) 886 requiredIfPresent := sets.New[string](options.RequiredIfPresentContexts...) 887 optional := sets.New[string](options.OptionalContexts...) 888 889 headSHAGetter := func() (string, error) { 890 return headSHA, nil 891 } 892 presubmits, err := c.GetPresubmits(gitClient, org+"/"+repo, branch, baseSHAGetter, headSHAGetter) 893 if err != nil { 894 return nil, fmt.Errorf("failed to get presubmits: %w", err) 895 } 896 897 // Using Branch protection configuration 898 if options.FromBranchProtection != nil && *options.FromBranchProtection { 899 bp, err := c.GetBranchProtection(org, repo, branch, presubmits) 900 if err != nil { 901 logrus.WithError(err).Warningf("Error getting branch protection for %s/%s+%s", org, repo, branch) 902 } else if bp != nil { 903 requireManuallyTriggeredJobs = bp.RequireManuallyTriggeredJobs 904 if bp.Protect != nil && *bp.Protect && bp.RequiredStatusChecks != nil { 905 required.Insert(bp.RequiredStatusChecks.Contexts...) 906 } 907 } 908 } 909 910 // automatically generate required and optional entries for Prow Jobs 911 prowRequired, prowRequiredIfPresent, prowOptional := BranchRequirements(branch, presubmits, requireManuallyTriggeredJobs) 912 required.Insert(prowRequired...) 913 requiredIfPresent.Insert(prowRequiredIfPresent...) 914 optional.Insert(prowOptional...) 915 916 t := &TideContextPolicy{ 917 RequiredContexts: sets.List(required), 918 RequiredIfPresentContexts: sets.List(requiredIfPresent), 919 OptionalContexts: sets.List(optional), 920 SkipUnknownContexts: options.SkipUnknownContexts, 921 } 922 if err := t.Validate(); err != nil { 923 return t, err 924 } 925 return t, nil 926 } 927 928 // IsOptional checks whether a context can be ignored. 929 // Will return true if 930 // - context is registered as optional 931 // - required contexts are registered and the context provided is not required 932 // Will return false otherwise. Every context is required. 933 func (cp *TideContextPolicy) IsOptional(c string) bool { 934 if sets.New[string](cp.OptionalContexts...).Has(c) { 935 return true 936 } 937 if sets.New[string](cp.RequiredContexts...).Has(c) { 938 return false 939 } 940 // assume if we're asking that the context is present on the PR 941 if sets.New[string](cp.RequiredIfPresentContexts...).Has(c) { 942 return false 943 } 944 if cp.SkipUnknownContexts != nil && *cp.SkipUnknownContexts { 945 return true 946 } 947 return false 948 } 949 950 // MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided. 951 func (cp *TideContextPolicy) MissingRequiredContexts(contexts []string) []string { 952 if len(cp.RequiredContexts) == 0 { 953 return nil 954 } 955 existingContexts := sets.New[string]() 956 for _, c := range contexts { 957 existingContexts.Insert(c) 958 } 959 var missingContexts []string 960 for c := range sets.New[string](cp.RequiredContexts...).Difference(existingContexts) { 961 missingContexts = append(missingContexts, c) 962 } 963 return missingContexts 964 }