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  }