sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/branch_protection.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 config 18 19 import ( 20 "errors" 21 "fmt" 22 "strings" 23 24 "github.com/sirupsen/logrus" 25 apiequality "k8s.io/apimachinery/pkg/api/equality" 26 utilerrors "k8s.io/apimachinery/pkg/util/errors" 27 "k8s.io/apimachinery/pkg/util/sets" 28 ) 29 30 // Policy for the config/org/repo/branch. 31 // When merging policies, a nil value results in inheriting the parent policy. 32 type Policy struct { 33 // Unmanaged makes us not manage the branchprotection. 34 Unmanaged *bool `json:"unmanaged,omitempty"` 35 // Protect overrides whether branch protection is enabled if set. 36 Protect *bool `json:"protect,omitempty"` 37 // RequiredStatusChecks configures github contexts 38 RequiredStatusChecks *ContextPolicy `json:"required_status_checks,omitempty"` 39 // Admins overrides whether protections apply to admins if set. 40 Admins *bool `json:"enforce_admins,omitempty"` 41 // Restrictions limits who can merge 42 Restrictions *Restrictions `json:"restrictions,omitempty"` 43 // RequireManuallyTriggeredJobs enforces a context presence when job runs conditionally, but not automatically, 44 // that results in params always_run: false, optional: false, and skip_if_only_change, run_if_changed not present. 45 RequireManuallyTriggeredJobs *bool `json:"require_manually_triggered_jobs,omitempty"` 46 // RequiredPullRequestReviews specifies github approval/review criteria. 47 RequiredPullRequestReviews *ReviewPolicy `json:"required_pull_request_reviews,omitempty"` 48 // RequiredLinearHistory enforces a linear commit Git history, which prevents anyone from pushing merge commits to a branch. 49 RequiredLinearHistory *bool `json:"required_linear_history,omitempty"` 50 // AllowForcePushes permits force pushes to the protected branch by anyone with write access to the repository. 51 AllowForcePushes *bool `json:"allow_force_pushes,omitempty"` 52 // AllowDeletions allows deletion of the protected branch by anyone with write access to the repository. 53 AllowDeletions *bool `json:"allow_deletions,omitempty"` 54 // Exclude specifies a set of regular expressions which identify branches 55 // that should be excluded from the protection policy, mutually exclusive with Include 56 Exclude []string `json:"exclude,omitempty"` 57 // Include specifies a set of regular expressions which identify branches 58 // that should be included from the protection policy, mutually exclusive with Exclude 59 Include []string `json:"include,omitempty"` 60 } 61 62 // Managed returns true if Unmanaged is false in the policy 63 func (p Policy) Managed() bool { 64 return p.Unmanaged != nil && !*p.Unmanaged 65 } 66 67 func (p Policy) defined() bool { 68 return p.Protect != nil || p.RequiredStatusChecks != nil || p.Admins != nil || p.Restrictions != nil || p.RequireManuallyTriggeredJobs != nil || 69 p.RequiredPullRequestReviews != nil || p.RequiredLinearHistory != nil || p.AllowForcePushes != nil || p.AllowDeletions != nil 70 } 71 72 // ContextPolicy configures required github contexts. 73 // When merging policies, contexts are appended to context list from parent. 74 // Strict determines whether merging to the branch invalidates existing contexts. 75 type ContextPolicy struct { 76 // Contexts appends required contexts that must be green to merge 77 Contexts []string `json:"contexts,omitempty"` 78 // Strict overrides whether new commits in the base branch require updating the PR if set 79 Strict *bool `json:"strict,omitempty"` 80 } 81 82 // ReviewPolicy specifies github approval/review criteria. 83 // Any nil values inherit the policy from the parent, otherwise bool/ints are overridden. 84 // Non-empty lists are appended to parent lists. 85 type ReviewPolicy struct { 86 // DismissalRestrictions appends users/teams that are allowed to merge 87 DismissalRestrictions *DismissalRestrictions `json:"dismissal_restrictions,omitempty"` 88 // DismissStale overrides whether new commits automatically dismiss old reviews if set 89 DismissStale *bool `json:"dismiss_stale_reviews,omitempty"` 90 // RequireOwners overrides whether CODEOWNERS must approve PRs if set 91 RequireOwners *bool `json:"require_code_owner_reviews,omitempty"` 92 // Approvals overrides the number of approvals required if set 93 Approvals *int `json:"required_approving_review_count,omitempty"` 94 // BypassRestrictions appends users/teams that are allowed to bypass PR restrictions 95 BypassRestrictions *BypassRestrictions `json:"bypass_pull_request_allowances,omitempty"` 96 } 97 98 // DismissalRestrictions limits who can merge 99 // Users and Teams items are appended to parent lists. 100 type DismissalRestrictions struct { 101 Users []string `json:"users,omitempty"` 102 Teams []string `json:"teams,omitempty"` 103 } 104 105 // BypassRestrictions defines who can bypass PR restrictions 106 // Users and Teams items are appended to parent lists. 107 type BypassRestrictions struct { 108 Users []string `json:"users,omitempty"` 109 Teams []string `json:"teams,omitempty"` 110 } 111 112 // Restrictions limits who can merge 113 // Apps, Users and Teams items are appended to parent lists. 114 type Restrictions struct { 115 Apps []string `json:"apps,omitempty"` 116 Users []string `json:"users,omitempty"` 117 Teams []string `json:"teams,omitempty"` 118 } 119 120 // selectInt returns the child if set, else parent 121 func selectInt(parent, child *int) *int { 122 if child != nil { 123 return child 124 } 125 return parent 126 } 127 128 // selectBool returns the child argument if set, otherwise the parent 129 func selectBool(parent, child *bool) *bool { 130 if child != nil { 131 return child 132 } 133 return parent 134 } 135 136 // unionStrings merges the parent and child items together 137 func unionStrings(parent, child []string) []string { 138 if child == nil { 139 return parent 140 } 141 if parent == nil { 142 return child 143 } 144 s := sets.New[string](parent...) 145 s.Insert(child...) 146 return sets.List(s) 147 } 148 149 func mergeContextPolicy(parent, child *ContextPolicy) *ContextPolicy { 150 if child == nil { 151 return parent 152 } 153 if parent == nil { 154 return child 155 } 156 return &ContextPolicy{ 157 Contexts: unionStrings(parent.Contexts, child.Contexts), 158 Strict: selectBool(parent.Strict, child.Strict), 159 } 160 } 161 162 func mergeReviewPolicy(parent, child *ReviewPolicy) *ReviewPolicy { 163 if child == nil { 164 return parent 165 } 166 if parent == nil { 167 return child 168 } 169 return &ReviewPolicy{ 170 DismissalRestrictions: mergeDismissalRestrictions(parent.DismissalRestrictions, child.DismissalRestrictions), 171 DismissStale: selectBool(parent.DismissStale, child.DismissStale), 172 RequireOwners: selectBool(parent.RequireOwners, child.RequireOwners), 173 Approvals: selectInt(parent.Approvals, child.Approvals), 174 BypassRestrictions: mergeBypassRestrictions(parent.BypassRestrictions, child.BypassRestrictions), 175 } 176 } 177 178 func mergeDismissalRestrictions(parent, child *DismissalRestrictions) *DismissalRestrictions { 179 if child == nil { 180 return parent 181 } 182 if parent == nil { 183 return child 184 } 185 return &DismissalRestrictions{ 186 Users: unionStrings(parent.Users, child.Users), 187 Teams: unionStrings(parent.Teams, child.Teams), 188 } 189 } 190 191 func mergeBypassRestrictions(parent, child *BypassRestrictions) *BypassRestrictions { 192 if child == nil { 193 return parent 194 } 195 if parent == nil { 196 return child 197 } 198 return &BypassRestrictions{ 199 Users: unionStrings(parent.Users, child.Users), 200 Teams: unionStrings(parent.Teams, child.Teams), 201 } 202 } 203 204 func mergeRestrictions(parent, child *Restrictions) *Restrictions { 205 if child == nil { 206 return parent 207 } 208 if parent == nil { 209 return child 210 } 211 return &Restrictions{ 212 Apps: unionStrings(parent.Apps, child.Apps), 213 Users: unionStrings(parent.Users, child.Users), 214 Teams: unionStrings(parent.Teams, child.Teams), 215 } 216 } 217 218 // Apply returns a policy that merges the child into the parent 219 func (p Policy) Apply(child Policy) Policy { 220 return Policy{ 221 Unmanaged: selectBool(p.Unmanaged, child.Unmanaged), 222 Protect: selectBool(p.Protect, child.Protect), 223 RequiredStatusChecks: mergeContextPolicy(p.RequiredStatusChecks, child.RequiredStatusChecks), 224 Admins: selectBool(p.Admins, child.Admins), 225 RequiredLinearHistory: selectBool(p.RequiredLinearHistory, child.RequiredLinearHistory), 226 AllowForcePushes: selectBool(p.AllowForcePushes, child.AllowForcePushes), 227 AllowDeletions: selectBool(p.AllowDeletions, child.AllowDeletions), 228 RequireManuallyTriggeredJobs: selectBool(p.RequireManuallyTriggeredJobs, child.RequireManuallyTriggeredJobs), 229 Restrictions: mergeRestrictions(p.Restrictions, child.Restrictions), 230 RequiredPullRequestReviews: mergeReviewPolicy(p.RequiredPullRequestReviews, child.RequiredPullRequestReviews), 231 Exclude: unionStrings(p.Exclude, child.Exclude), 232 Include: unionStrings(p.Include, child.Include), 233 } 234 } 235 236 // BranchProtection specifies the global branch protection policy 237 type BranchProtection struct { 238 Policy `json:",inline"` 239 // ProtectTested determines if branch protection rules are set for all repos 240 // that Prow has registered jobs for, regardless of if those repos are in the 241 // branch protection config. 242 ProtectTested *bool `json:"protect-tested-repos,omitempty"` 243 // Orgs holds branch protection options for orgs by name 244 Orgs map[string]Org `json:"orgs,omitempty"` 245 // AllowDisabledPolicies allows a child to disable all protection even if the 246 // branch has inherited protection options from a parent. 247 AllowDisabledPolicies *bool `json:"allow_disabled_policies,omitempty"` 248 // AllowDisabledJobPolicies allows a branch to choose to opt out of branch protection 249 // even if Prow has registered required jobs for that branch. 250 AllowDisabledJobPolicies *bool `json:"allow_disabled_job_policies,omitempty"` 251 // ProtectReposWithOptionalJobs will make the Branchprotector manage required status 252 // contexts on repositories that only have optional jobs (default: false) 253 ProtectReposWithOptionalJobs *bool `json:"protect_repos_with_optional_jobs,omitempty"` 254 } 255 256 func isPolicySet(p Policy) bool { 257 return !apiequality.Semantic.DeepEqual(p, Policy{}) 258 } 259 260 // HasManagedBranches returns true if the global branch protector's config has managed branches 261 func (bp BranchProtection) HasManagedBranches() bool { 262 for _, org := range bp.Orgs { 263 if org.HasManagedBranches() { 264 return true 265 } 266 } 267 return false 268 } 269 270 // HasManagedOrgs returns true if the global branch protector's config has managed orgs 271 func (bp BranchProtection) HasManagedOrgs() bool { 272 for _, org := range bp.Orgs { 273 if org.Policy.Managed() { 274 return true 275 } 276 } 277 return false 278 } 279 280 // HasManagedRepos returns true if the global branch protector's config has managed repos 281 func (bp BranchProtection) HasManagedRepos() bool { 282 for _, org := range bp.Orgs { 283 for _, repo := range org.Repos { 284 if repo.Policy.Managed() { 285 return true 286 } 287 } 288 } 289 return false 290 } 291 292 func (bp *BranchProtection) merge(additional *BranchProtection) error { 293 var errs []error 294 if isPolicySet(bp.Policy) && isPolicySet(additional.Policy) { 295 errs = append(errs, errors.New("both brachprotection configs set a top-level policy")) 296 } else if isPolicySet(additional.Policy) { 297 bp.Policy = additional.Policy 298 } 299 if bp.ProtectTested != nil && additional.ProtectTested != nil { 300 errs = append(errs, errors.New("both branchprotection configs set protect-tested-repos")) 301 } else if additional.ProtectTested != nil { 302 bp.ProtectTested = additional.ProtectTested 303 } 304 if bp.AllowDisabledPolicies != nil && additional.AllowDisabledPolicies != nil { 305 errs = append(errs, errors.New("both branchprotection configs set allow_disabled_policies")) 306 } else if additional.AllowDisabledPolicies != nil { 307 bp.AllowDisabledPolicies = additional.AllowDisabledPolicies 308 } 309 if bp.AllowDisabledJobPolicies != nil && additional.AllowDisabledJobPolicies != nil { 310 errs = append(errs, errors.New("both branchprotection configs set allow_disabled_job_policies")) 311 } else if additional.AllowDisabledJobPolicies != nil { 312 bp.AllowDisabledJobPolicies = additional.AllowDisabledJobPolicies 313 } 314 if bp.ProtectReposWithOptionalJobs != nil && additional.ProtectReposWithOptionalJobs != nil { 315 errs = append(errs, errors.New("both branchprotection configs set protect_repos_with_optional_jobs")) 316 } else if additional.ProtectReposWithOptionalJobs != nil { 317 bp.ProtectReposWithOptionalJobs = additional.ProtectReposWithOptionalJobs 318 } 319 for org := range additional.Orgs { 320 if bp.Orgs == nil { 321 bp.Orgs = map[string]Org{} 322 } 323 if isPolicySet(bp.Orgs[org].Policy) && isPolicySet(additional.Orgs[org].Policy) { 324 errs = append(errs, fmt.Errorf("both branchprotection configs define a policy for org %s", org)) 325 } else if _, ok := additional.Orgs[org]; ok && !isPolicySet(bp.Orgs[org].Policy) { 326 orgSettings := bp.Orgs[org] 327 orgSettings.Policy = additional.Orgs[org].Policy 328 bp.Orgs[org] = orgSettings 329 } 330 331 for repo := range additional.Orgs[org].Repos { 332 if bp.Orgs[org].Repos == nil { 333 orgSettings := bp.Orgs[org] 334 orgSettings.Repos = map[string]Repo{} 335 bp.Orgs[org] = orgSettings 336 } 337 if isPolicySet(bp.Orgs[org].Repos[repo].Policy) && isPolicySet(additional.Orgs[org].Repos[repo].Policy) { 338 errs = append(errs, fmt.Errorf("both branchprotection configs define a policy for repo %s/%s", org, repo)) 339 } else if _, ok := additional.Orgs[org].Repos[repo]; ok && !isPolicySet(bp.Orgs[org].Repos[repo].Policy) { 340 repoSettings := bp.Orgs[org].Repos[repo] 341 repoSettings.Policy = additional.Orgs[org].Repos[repo].Policy 342 bp.Orgs[org].Repos[repo] = repoSettings 343 } 344 345 for branch := range additional.Orgs[org].Repos[repo].Branches { 346 if bp.Orgs[org].Repos[repo].Branches == nil { 347 branchSettings := bp.Orgs[org].Repos[repo] 348 branchSettings.Branches = map[string]Branch{} 349 bp.Orgs[org].Repos[repo] = branchSettings 350 } 351 352 if isPolicySet(bp.Orgs[org].Repos[repo].Branches[branch].Policy) && isPolicySet(additional.Orgs[org].Repos[repo].Branches[branch].Policy) { 353 errs = append(errs, fmt.Errorf("both branchprotection configs define a policy for branch %s in repo %s/%s", branch, org, repo)) 354 } else if _, ok := additional.Orgs[org].Repos[repo].Branches[branch]; ok && !isPolicySet(bp.Orgs[org].Repos[repo].Branches[branch].Policy) { 355 branchSettings := bp.Orgs[org].Repos[repo].Branches[branch] 356 branchSettings.Policy = additional.Orgs[org].Repos[repo].Branches[branch].Policy 357 bp.Orgs[org].Repos[repo].Branches[branch] = branchSettings 358 } 359 } 360 } 361 } 362 363 return utilerrors.NewAggregate(errs) 364 } 365 366 // GetOrg returns the org config after merging in any global policies. 367 func (bp BranchProtection) GetOrg(name string) *Org { 368 o, ok := bp.Orgs[name] 369 if ok { 370 o.Policy = bp.Apply(o.Policy) 371 } else { 372 o.Policy = bp.Policy 373 } 374 return &o 375 } 376 377 // Org holds the default protection policy for an entire org, as well as any repo overrides. 378 type Org struct { 379 Policy `json:",inline"` 380 Repos map[string]Repo `json:"repos,omitempty"` 381 } 382 383 // HasManagedRepos returns true if the org has managed repos 384 func (o Org) HasManagedRepos() bool { 385 for _, repo := range o.Repos { 386 if repo.Policy.Managed() { 387 return true 388 } 389 } 390 return false 391 } 392 393 // HasManagedBranches returns true if the org has managed branches 394 func (o Org) HasManagedBranches() bool { 395 for _, repo := range o.Repos { 396 if repo.HasManagedBranches() { 397 return true 398 } 399 } 400 return false 401 } 402 403 // GetRepo returns the repo config after merging in any org policies. 404 func (o Org) GetRepo(name string) *Repo { 405 r, ok := o.Repos[name] 406 if ok { 407 r.Policy = o.Apply(r.Policy) 408 } else { 409 r.Policy = o.Policy 410 } 411 return &r 412 } 413 414 // Repo holds protection policy overrides for all branches in a repo, as well as specific branch overrides. 415 type Repo struct { 416 Policy `json:",inline"` 417 Branches map[string]Branch `json:"branches,omitempty"` 418 } 419 420 // HasManagedBranches returns true if the repo has managed branches 421 func (r Repo) HasManagedBranches() bool { 422 for _, branch := range r.Branches { 423 if branch.Policy.Managed() { 424 return true 425 } 426 } 427 return false 428 } 429 430 // GetBranch returns the branch config after merging in any repo policies. 431 func (r Repo) GetBranch(name string) (*Branch, error) { 432 b, ok := r.Branches[name] 433 if ok { 434 b.Policy = r.Apply(b.Policy) 435 if b.Protect == nil && (b.Unmanaged == nil || !*b.Unmanaged) { 436 return nil, errors.New("defined branch policies must set protect or unmanaged=true") 437 } 438 } else { 439 b.Policy = r.Policy 440 } 441 return &b, nil 442 } 443 444 // Branch holds protection policy overrides for a particular branch. 445 type Branch struct { 446 Policy `json:",inline"` 447 } 448 449 // GetBranchProtection returns the policy for a given branch. 450 // 451 // Handles merging any policies defined at repo/org/global levels into the branch policy. 452 func (c *Config) GetBranchProtection(org, repo, branch string, presubmits []Presubmit) (*Policy, error) { 453 if _, present := c.BranchProtection.Orgs[org]; !present { 454 return nil, nil // only consider branches in configured orgs 455 } 456 b, err := c.BranchProtection.GetOrg(org).GetRepo(repo).GetBranch(branch) 457 if err != nil { 458 return nil, err 459 } 460 461 return c.GetPolicy(org, repo, branch, *b, presubmits, nil) 462 } 463 464 // GetPolicy returns the protection policy for the branch, after merging in presubmits. 465 func (c *Config) GetPolicy(org, repo, branch string, b Branch, presubmits []Presubmit, protectedOnGitHub *bool) (*Policy, error) { 466 policy := b.Policy 467 468 // Automatically require contexts from prow which must always be present 469 if prowContexts, requiredIfPresentContexts, optionalContexts := BranchRequirements(branch, presubmits, policy.RequireManuallyTriggeredJobs); c.shouldManageRequiredStatusCheck(prowContexts, requiredIfPresentContexts, optionalContexts) { 470 // Error if protection is disabled 471 if policy.Protect != nil && !*policy.Protect { 472 if c.BranchProtection.AllowDisabledJobPolicies != nil && *c.BranchProtection.AllowDisabledJobPolicies { 473 if protectedOnGitHub != nil && *protectedOnGitHub { 474 logrus.WithField("branch", fmt.Sprintf("%s/%s/%s", org, repo, branch)). 475 Warn("'protect: false' configuration causes branchprotector to not manage branch protection settings at all. " + 476 "If this a desired behavior, use 'unmanaged: true' instead.") 477 } 478 return nil, nil 479 } 480 return nil, fmt.Errorf("required prow jobs require branch protection") 481 } 482 ps := Policy{ 483 RequiredStatusChecks: &ContextPolicy{ 484 Contexts: prowContexts, 485 }, 486 } 487 // Require protection by default if ProtectTested is true 488 if c.BranchProtection.ProtectTested != nil && *c.BranchProtection.ProtectTested { 489 yes := true 490 ps.Protect = &yes 491 } 492 policy = policy.Apply(ps) 493 } 494 495 if policy.Protect != nil && !*policy.Protect { 496 // Ensure that protection is false => no protection settings 497 var old *bool 498 old, policy.Protect = policy.Protect, old 499 if policy.defined() && !boolValFromPtr(c.BranchProtection.AllowDisabledPolicies) { 500 return nil, fmt.Errorf("%s/%s=%s defines a policy, which requires protect: true", org, repo, branch) 501 } 502 policy.Protect = old 503 } 504 505 if !policy.defined() { 506 return nil, nil 507 } 508 return &policy, nil 509 } 510 511 func (c *Config) shouldManageRequiredStatusCheck(requiredContexts, requiredIfPresentContexts, optionalContexts []string) bool { 512 if len(requiredContexts) > 0 { 513 return true 514 } 515 if c.BranchProtection.ProtectReposWithOptionalJobs == nil || !*c.BranchProtection.ProtectReposWithOptionalJobs { 516 return false 517 } 518 return len(requiredIfPresentContexts) > 0 || len(optionalContexts) > 0 519 } 520 521 func isUnprotected(policy Policy, allowDisabledPolicies bool, hasRequiredContexts bool, allowDisabledJobPolicies bool) bool { 522 if policy.Protect != nil && !*policy.Protect { 523 if hasRequiredContexts && allowDisabledJobPolicies { 524 return true 525 } 526 if allowDisabledPolicies { 527 policy.Protect = nil 528 if policy.defined() { 529 return true 530 } 531 } 532 } 533 return false 534 } 535 536 func (c *Config) reposWithDisabledPolicy() []string { 537 repoWarns := sets.New[string]() 538 for orgName, org := range c.BranchProtection.Orgs { 539 for repoName := range org.Repos { 540 repoPolicy := c.BranchProtection.GetOrg(orgName).GetRepo(repoName) 541 if isUnprotected(repoPolicy.Policy, boolValFromPtr(c.BranchProtection.AllowDisabledPolicies), false, false) { 542 repoWarns.Insert(fmt.Sprintf("%s/%s", orgName, repoName)) 543 } 544 } 545 } 546 return sets.List(repoWarns) 547 } 548 549 // boolValFromPtr returns the bool value from a bool pointer. 550 // Nil counts as false. We need the boolpointers to be able 551 // to differentiate unset from false in the serialization. 552 func boolValFromPtr(b *bool) bool { 553 return b != nil && *b 554 } 555 556 // unprotectedBranches returns the set of names of branches 557 // which have protection flag set to false, but have either: 558 // a. a protection policy, or 559 // b. a required context 560 func (c *Config) unprotectedBranches(presubmits map[string][]Presubmit) []string { 561 branchWarns := sets.New[string]() 562 for orgName, org := range c.BranchProtection.Orgs { 563 for repoName, repo := range org.Repos { 564 branches := sets.New[string]() 565 for branchName := range repo.Branches { 566 b, err := c.BranchProtection.GetOrg(orgName).GetRepo(repoName).GetBranch(branchName) 567 if err != nil { 568 continue 569 } 570 policy, err := c.GetPolicy(orgName, repoName, branchName, *b, []Presubmit{}, nil) 571 if err != nil || policy == nil { 572 continue 573 } 574 requiredContexts, _, _ := BranchRequirements(branchName, presubmits[orgName+"/"+repoName], policy.RequireManuallyTriggeredJobs) 575 if isUnprotected(*policy, boolValFromPtr(c.BranchProtection.AllowDisabledPolicies), len(requiredContexts) > 0, boolValFromPtr(c.BranchProtection.AllowDisabledJobPolicies)) { 576 branches.Insert(branchName) 577 } 578 } 579 if branches.Len() > 0 { 580 branchWarns.Insert(fmt.Sprintf("%s/%s=%s", orgName, repoName, strings.Join(sets.List(branches), ","))) 581 } 582 } 583 } 584 return sets.List(branchWarns) 585 } 586 587 // BranchProtectionWarnings logs two sets of warnings: 588 // - The list of repos with unprotected branches, 589 // - The list of repos with disabled policies, i.e. Protect set to false, 590 // because any branches not explicitly specified in the configuration will be unprotected. 591 func (c *Config) BranchProtectionWarnings(logger *logrus.Entry, presubmits map[string][]Presubmit) { 592 if warnings := c.reposWithDisabledPolicy(); len(warnings) > 0 { 593 logger.WithField("repos", strings.Join(warnings, ",")).Debug("The following repos define a policy, but have protect: false") 594 } 595 if warnings := c.unprotectedBranches(presubmits); len(warnings) > 0 { 596 logger.WithField("repos", strings.Join(warnings, ",")).Debug("The following repos define a policy or require context(s), but have one or more branches with protect: false") 597 } 598 } 599 600 // BranchRequirements partitions status contexts for a given org, repo branch into three buckets: 601 // - contexts that are always required to be present 602 // - contexts that are required, _if_ present 603 // - contexts that are always optional 604 func BranchRequirements(branch string, jobs []Presubmit, requireManuallyTriggeredJobs *bool) ([]string, []string, []string) { 605 var required, requiredIfPresent, optional []string 606 var manuallyTriggeredJobs bool 607 if requireManuallyTriggeredJobs != nil && *requireManuallyTriggeredJobs { 608 manuallyTriggeredJobs = true 609 } 610 for _, j := range jobs { 611 if !j.CouldRun(branch) { 612 continue 613 } 614 if j.ContextRequired() { 615 switch { 616 case manuallyTriggeredJobs && j.NeedsExplicitTrigger(): 617 // require jobs marked as always_run: false, optional: false, 618 // with no skip_if_only_changed and no run_if_changed 619 required = append(required, j.Context) 620 case j.TriggersConditionally(): 621 // jobs that trigger conditionally cannot be 622 // required as their status may not exist on PRs 623 requiredIfPresent = append(requiredIfPresent, j.Context) 624 default: 625 // jobs that produce required contexts and will 626 // always run should be required at all times 627 required = append(required, j.Context) 628 } 629 } else { 630 optional = append(optional, j.Context) 631 } 632 } 633 return required, requiredIfPresent, optional 634 }