Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/config/prow-config-documented.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,15 @@ tide:
org: ' '
repos:
- ""
# GitHubMergeBlocksPolicyMap configures on org or org/repo level how Tide should handle
# GitHub's mergeStateStatus (BLOCKED state from branch protection rules, rulesets,
# required reviews, etc.).
# Use '*' as key to set this globally. Defaults to "permit" which allows merging but logs warnings.
# Valid values: "ignore", "permit", "block"
# Note: "ignore" may cause issues with repos using GitHub Rulesets that restrict updates
# but have Tide on the bypass list.
github_merge_blocks_policy:
"": ""
# A key/value pair of an org/repo as the key and Go template to override
# the default merge commit title and/or message. Template is passed the
# PullRequest struct (prow/github/types.go#PullRequest)
Expand Down
43 changes: 43 additions & 0 deletions pkg/config/tide.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ import (
// TideQueries is a TideQuery slice.
type TideQueries []TideQuery

// GitHubMergeBlocksPolicy describes how Tide should handle GitHub's merge blocking conditions.
type GitHubMergeBlocksPolicy string

const (
// GitHubMergeBlocksIgnore ignores GitHub's BLOCKED status entirely in merge decisions.
// Tide will attempt to merge PRs even if GitHub reports them as blocked.
GitHubMergeBlocksIgnore GitHubMergeBlocksPolicy = "ignore"

// GitHubMergeBlocksPermit allows merging of BLOCKED PRs but logs warnings and may
// surface the blocked state in PR status messages. This is useful for monitoring
// repos that need branch protection or ruleset fixes.
GitHubMergeBlocksPermit GitHubMergeBlocksPolicy = "permit"

// GitHubMergeBlocksBlock respects GitHub's BLOCKED status and prevents Tide from
// attempting to merge PRs that are blocked by branch protection rules, rulesets,
// required reviews, or other GitHub-side blocking conditions.
GitHubMergeBlocksBlock GitHubMergeBlocksPolicy = "block"
)

type TideBranchMergeType struct {
MergeType types.PullRequestMergeType
Regexpr *regexp.Regexp
Expand Down Expand Up @@ -227,6 +246,14 @@ type Tide struct {
// starting a new one requires to start new instances of all tests.
// Use '*' as key to set this globally. Defaults to true.
PrioritizeExistingBatchesMap map[string]bool `json:"prioritize_existing_batches,omitempty"`
// GitHubMergeBlocksPolicyMap configures on org or org/repo level how Tide should handle
// GitHub's mergeStateStatus (BLOCKED state from branch protection rules, rulesets,
// required reviews, etc.).
// Use '*' as key to set this globally. Defaults to "permit" which allows merging but logs warnings.
// Valid values: "ignore", "permit", "block"
// Note: "ignore" may cause issues with repos using GitHub Rulesets that restrict updates
// but have Tide on the bypass list.
GitHubMergeBlocksPolicyMap map[string]GitHubMergeBlocksPolicy `json:"github_merge_blocks_policy,omitempty"`

TideGitHubConfig `json:",inline"`
}
Expand Down Expand Up @@ -370,6 +397,22 @@ func (t *Tide) BatchSizeLimit(repo OrgRepo) int {
return t.BatchSizeLimitMap["*"]
}

// GitHubMergeBlocksPolicy returns the policy for handling GitHub's merge blocking conditions.
// The default is "permit" which allows merging but logs warnings for monitoring.
func (t *Tide) GitHubMergeBlocksPolicy(repo OrgRepo) GitHubMergeBlocksPolicy {
if val, set := t.GitHubMergeBlocksPolicyMap[repo.String()]; set {
return val
}
if val, set := t.GitHubMergeBlocksPolicyMap[repo.Org]; set {
return val
}
if val, set := t.GitHubMergeBlocksPolicyMap["*"]; set {
return val
}
// Default to "permit" to allow merging while surfacing the blocked state for monitoring
return GitHubMergeBlocksPermit
}

// MergeMethod returns the merge method to use for a repo. The default of merge is
// returned when not overridden.
func (t *Tide) MergeMethod(repo OrgRepo) types.PullRequestMergeType {
Expand Down
14 changes: 14 additions & 0 deletions pkg/tide/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,20 @@ func (m *mergeChecker) isAllowedToMerge(crc *CodeReviewCommon) (string, error) {
} else if !allowed {
return fmt.Sprintf("Merge type %q disallowed by repo settings", *mergeMethod), nil
}
// Check GitHub's mergeStateStatus which reflects all GitHub-side merge blocking conditions
// including branch protection rules, rulesets, required reviews, status checks, etc.
// This is controlled by the github_merge_blocks_policy configuration.
if pr.MergeStateStatus == "BLOCKED" {
policy := m.config().Tide.GitHubMergeBlocksPolicy(orgRepo)
switch policy {
case config.GitHubMergeBlocksBlock:
return "PR is blocked from merging by GitHub (check branch protection, required reviews, or rulesets)", nil
case config.GitHubMergeBlocksPermit:
// Allow merge but the warning will be surfaced in PR status by requirementDiff
case config.GitHubMergeBlocksIgnore:
// Ignore BLOCKED status entirely
}
}
return "", nil
}

Expand Down
57 changes: 51 additions & 6 deletions pkg/tide/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ import (
)

const (
statusContext = "tide"
statusInPool = "In merge pool."
statusContext = "tide"
statusInPool = "In merge pool."
statusInPoolDespiteBlocked = "In merge pool (despite BLOCKED)."
// statusNotInPool is a format string used when a PR is not in a tide pool.
// The '%s' field is populated with the reason why the PR is not in a
// tide pool or the empty string if the reason is unknown. See requirementDiff.
Expand Down Expand Up @@ -125,7 +126,7 @@ func (sc *statusController) shutdown() {
// Note: an empty diff can be returned if the reason that the PR does not match
// the TideQuery is unknown. This can happen if this function's logic
// does not match GitHub's and does not indicate that the PR matches the query.
func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker) (string, int) {
func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker, tide *config.Tide) (string, int) {
const maxLabelChars = 50
var desc string
var diff int
Expand Down Expand Up @@ -260,6 +261,27 @@ func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker) (s
desc = " PullRequest is missing sufficient approving GitHub review(s)"
}
}
// Check GitHub's mergeStateStatus which reflects all GitHub-side blocking conditions
// This is controlled by the github_merge_blocks_policy configuration.
if tide != nil && pr.MergeStateStatus == "BLOCKED" {
orgRepo := config.OrgRepo{
Org: string(pr.Repository.Owner.Login),
Repo: string(pr.Repository.Name),
}
policy := tide.GitHubMergeBlocksPolicy(orgRepo)
switch policy {
case config.GitHubMergeBlocksBlock:
// Block merge by adding to diff
diff += 100
if desc == "" {
desc = " Blocked by GitHub (branch rulesets or protection)"
}
case config.GitHubMergeBlocksPermit:
// Allow merge but don't add to diff. Warning will be shown in pool status.
case config.GitHubMergeBlocksIgnore:
// Ignore BLOCKED status entirely
}
}
return desc, diff
}

Expand Down Expand Up @@ -315,7 +337,7 @@ func (sc *statusController) expectedStatus(log *logrus.Entry, queryMap *config.Q
minDiffCount := -1
var minDiff string
for _, q := range queryMap.ForRepo(repo) {
diff, diffCount := requirementDiff(pr, &q, cc)
diff, diffCount := requirementDiff(pr, &q, cc, &sc.config().Tide)
if diffCount == 0 {
hasFulfilledQuery = true
break
Expand Down Expand Up @@ -347,7 +369,7 @@ func (sc *statusController) expectedStatus(log *logrus.Entry, queryMap *config.Q
if err := sc.pjClient.List(context.Background(), passingUpToDatePJs, ctrlruntimeclient.MatchingFields{indexNamePassingJobs: indexKey}); err != nil {
// Just log the error and return success, as the PR is in the merge pool
log.WithError(err).Error("Failed to list ProwJobs.")
return github.StatusSuccess, statusInPool, nil
return github.StatusSuccess, poolStatus(pr, &sc.config().Tide, log), nil
}

var passingUpToDateContexts []string
Expand All @@ -357,7 +379,30 @@ func (sc *statusController) expectedStatus(log *logrus.Entry, queryMap *config.Q
if diff := cc.MissingRequiredContexts(passingUpToDateContexts); len(diff) > 0 {
return github.StatePending, retestingStatus(diff), nil
}
return github.StatusSuccess, statusInPool, nil
return github.StatusSuccess, poolStatus(pr, &sc.config().Tide, log), nil
}

// poolStatus returns the appropriate status message for a PR that is in the merge pool.
// If the PR has BLOCKED merge state and the policy is "permit", it returns a warning message.
// This also logs a warning for monitoring purposes.
func poolStatus(pr *PullRequest, tide *config.Tide, log *logrus.Entry) string {
if pr.MergeStateStatus == "BLOCKED" && tide != nil {
repo := config.OrgRepo{
Org: string(pr.Repository.Owner.Login),
Repo: string(pr.Repository.Name),
}
if tide.GitHubMergeBlocksPolicy(repo) == config.GitHubMergeBlocksPermit {
log.WithFields(logrus.Fields{
"org": repo.Org,
"repo": repo.Repo,
"pr": pr.Number,
"merge_state": pr.MergeStateStatus,
"policy": config.GitHubMergeBlocksPermit,
}).Warning("PR is in merge pool despite GitHub BLOCKED status (policy: permit)")
return statusInPoolDespiteBlocked
}
}
return statusInPool
}

func retestingStatus(retested []string) string {
Expand Down
Loading