Browse Source

Don't match branch protection rule patterns locally

Branch protection rules in github are fn patterns which are currently
matched locally in zuul. This is error prone and can lead in edge
cases to wrong matches resulting in wrong enqueue decisions into gate
pipelines. When requesting branch protection rules in github we also
can request the matching refs along with the rules. This is much safer
since we can plain text match them against the change branch.

Change-Id: Ic995d4b2e16a5d741f0209fa9236959d8f4d10b9
changes/86/751686/2
Tobias Henkel 4 days ago
parent
commit
b9c2d25dce
No known key found for this signature in database GPG Key ID: 3750DEC158E5FA2
6 changed files with 51 additions and 12 deletions
  1. +0
    -1
      requirements.txt
  2. +20
    -0
      tests/fake_graphql.py
  3. +2
    -1
      zuul/driver/github/githubconnection.py
  4. +19
    -10
      zuul/driver/github/graphql/__init__.py
  5. +5
    -0
      zuul/driver/github/graphql/canmerge-legacy.graphql
  6. +5
    -0
      zuul/driver/github/graphql/canmerge.graphql

+ 0
- 1
requirements.txt View File

@@ -32,7 +32,6 @@ paho-mqtt
cherrypy
ws4py
routes
pathspec
jsonpath-rw
urllib3!=1.25.4,!=1.25.5 # https://github.com/urllib3/urllib3/pull/1684
cheroot!=8.1.*,!=8.2.*,!=8.3.0 # https://github.com/cherrypy/cheroot/issues/263

+ 20
- 0
tests/fake_graphql.py View File

@@ -26,11 +26,28 @@ class FakePageInfo(ObjectType):
return False


class FakeMatchingRef(ObjectType):
name = String()

def resolve_name(parent, info):
return parent


class FakeMatchingRefs(ObjectType):
nodes = List(FakeMatchingRef)

def resolve_nodes(parent, info):
# To simplify tests just return the pattern and a bogus ref that should
# not disturb zuul.
return [parent.pattern, 'bogus-ref']


class FakeBranchProtectionRule(ObjectType):
pattern = String()
requiredStatusCheckContexts = List(String)
requiresApprovingReviews = Boolean()
requiresCodeOwnerReviews = Boolean()
matchingRefs = Field(FakeMatchingRefs, first=Int())

def resolve_pattern(parent, info):
return parent.pattern
@@ -44,6 +61,9 @@ class FakeBranchProtectionRule(ObjectType):
def resolve_requiresCodeOwnerReviews(parent, info):
return parent.require_codeowners_review

def resolve_matchingRefs(parent, info, first=None):
return parent


class FakeBranchProtectionRules(ObjectType):
nodes = List(FakeBranchProtectionRule)


+ 2
- 1
zuul/driver/github/githubconnection.py View File

@@ -1635,7 +1635,8 @@ class GithubConnection(BaseConnection):

# For performance reasons fetch all needed data for canMerge upfront
# using a single graphql call.
canmerge_data = self.graphql_client.fetch_canmerge(github, change)
canmerge_data = self.graphql_client.fetch_canmerge(
github, change, zuul_event_id=event)

# If the PR is a draft it cannot be merged.
if canmerge_data.get('isDraft', False):


+ 19
- 10
zuul/driver/github/graphql/__init__.py View File

@@ -13,9 +13,10 @@
# under the License.
import logging

import pathspec
from pkg_resources import resource_string

from zuul.lib.logutil import get_annotated_logger


def nested_get(d, *keys, default=None):
temp = d
@@ -70,7 +71,8 @@ class GraphQLClient:
response = github.session.post(self.url, json=query)
return response.json()

def fetch_canmerge(self, github, change):
def fetch_canmerge(self, github, change, zuul_event_id=None):
log = get_annotated_logger(self.log, zuul_event_id)
owner, repo = change.project.name.split('/')

data = self._fetch_canmerge(github, owner, repo, change.number,
@@ -81,14 +83,21 @@ class GraphQLClient:
# Find corresponding rule to our branch
rules = nested_get(repository, 'branchProtectionRules', 'nodes',
default=[])
matching_rule = None
for rule in rules:
pattern = pathspec.patterns.GitWildMatchPattern(
rule.get('pattern'))
match = pathspec.match_files([pattern], [change.branch])
if match:
matching_rule = rule
break

# Filter branch protection rules for the one matching the change.
matching_rules = [
rule for rule in rules
for ref in nested_get(rule, 'matchingRefs', 'nodes', default=[])
if ref.get('name') == change.branch
]
if len(matching_rules) > 1:
log.warn('More than one branch protection rules match change %s',
change)
return result
elif len(matching_rules) == 1:
matching_rule = matching_rules[0]
else:
matching_rule = None

# If there is a matching rule, get required status checks
if matching_rule:


+ 5
- 0
zuul/driver/github/graphql/canmerge-legacy.graphql View File

@@ -11,6 +11,11 @@ query canMergeData(
requiredStatusCheckContexts
requiresApprovingReviews
requiresCodeOwnerReviews
matchingRefs(first: 100) {
nodes {
name
}
}
}
}
pullRequest(number: $pull) {


+ 5
- 0
zuul/driver/github/graphql/canmerge.graphql View File

@@ -11,6 +11,11 @@ query canMergeData(
requiredStatusCheckContexts
requiresApprovingReviews
requiresCodeOwnerReviews
matchingRefs(first: 100) {
nodes {
name
}
}
}
}
pullRequest(number: $pull) {


Loading…
Cancel
Save