Add support for excluding locked branches

This adds support for excluding locked (read-only) branches.  This is
currently only supported by the Github driver.

Change-Id: I360edeb04c9734189396e8c5ddbed17e7f7464a8
This commit is contained in:
James E. Blair 2024-04-11 18:10:09 -07:00
parent a2459321bf
commit 49a9295c8f
17 changed files with 185 additions and 14 deletions

View File

@ -804,8 +804,9 @@ The rules prevent Pull requests to be merged on defined branches if they are
not met. For instance a branch might require that specific status are marked
as ``success`` before allowing the merge of the Pull request.
Zuul provides the attribute tenant.untrusted-projects.exclude-unprotected-branches.
This attribute is by default set to ``false`` but we recommend to set it to
Zuul provides the attribute
:attr:`tenant.untrusted-projects.<project>.exclude-unprotected-branches`. This
attribute is by default set to ``false`` but we recommend to set it to
``true`` for the whole tenant. By doing so Zuul will benefit from:
- exluding in-repo development branches used to open Pull requests. This will
@ -816,6 +817,10 @@ This attribute is by default set to ``false`` but we recommend to set it to
Zuul only takes in account "Require status checks to pass before merging" and
the checked status checkboxes.
Likewise, it is recommended to set the
:attr:`tenant.untrusted-projects.<project>.exclude-locked-branches` setting to
avoid expending resources on read-only branches.
With the use of the reference pipelines below, the Zuul project recommends to
set the minimum following settings:

View File

@ -210,13 +210,20 @@ configuration. Some examples of tenant definitions are:
exclude-unprotected-branches. This currently only affects
GitHub and GitLab projects.
.. attr:: exclude-locked-branches
Define if locked branches should be processed.
Defaults to the tenant wide setting of
exclude-locked-branches. This currently only affects
GitHub projects.
.. attr:: include-branches
A list of regexes matching branches which should be
processed. If omitted, all branches are included.
Operates after *exclude-unprotected-branches* and so may
be used to further reduce the set of branches (but not
increase it).
Operates after *exclude-unprotected-branches* and
*exclude-locked-branches* and so may be used to further
reduce the set of branches (but not increase it).
It has priority over *exclude-branches*.
@ -224,9 +231,9 @@ configuration. Some examples of tenant definitions are:
A list of regexes matching branches which should be
processed. If omitted, all branches are included.
Operates after *exclude-unprotected-branches* and so may
be used to further reduce the set of branches (but not
increase it).
Operates after *exclude-unprotected-branches* and
*exclude-locked-branches* and so may be used to further
reduce the set of branches (but not increase it).
It will not exclude a branch which already matched
*include-branches*.
@ -377,6 +384,15 @@ configuration. Some examples of tenant definitions are:
is a tenant wide setting and can be overridden per project.
This currently only affects GitHub and GitLab projects.
.. attr:: exclude-locked-branches
:default: false
Some code review systems support read-only, or "locked"
branches. Enabling this setting will cause Zuul to ignore these
branches. This is a tenant wide setting and can be overridden
per project. This currently only affects GitHub and GitLab
projects.
.. attr:: default-parent
:default: base

View File

@ -0,0 +1,5 @@
---
features:
- |
The GitHub driver now supports excluding locked (read-only) branches
with the `exclude-locked-branches` tenant configuration setting.

View File

@ -0,0 +1,23 @@
- pipeline:
name: check
manager: independent
trigger:
github:
- event: pull_request
action:
- opened
- changed
- reopened
success:
github:
status: 'success'
failure:
github:
status: 'failure'
start:
github:
comment: true
- job:
name: base
parent: null

View File

@ -0,0 +1 @@
test

View File

@ -0,0 +1,2 @@
- hosts: all
tasks: []

View File

@ -0,0 +1,9 @@
- job:
name: project-test
run: playbooks/project-test.yaml
- project:
name: org/project1
check:
jobs:
- project-test

View File

@ -0,0 +1 @@
test

View File

@ -0,0 +1,2 @@
- hosts: all
tasks: []

View File

@ -0,0 +1,8 @@
- job:
name: used-job
run: playbooks/used-job.yaml
- project:
check:
jobs:
- used-job

View File

@ -0,0 +1 @@
test

View File

@ -0,0 +1,13 @@
- tenant:
name: tenant-one
source:
github:
config-projects:
- org/common-config
untrusted-projects:
- org/project1
- org/project2:
exclude-locked-branches: true
- org/project3:
exclude-unprotected-branches: true
exclude-locked-branches: true

View File

@ -1954,6 +1954,71 @@ class TestGithubUnprotectedBranches(ZuulTestCase):
prev_layout = new_layout
class TestGithubLockedBranches(ZuulTestCase):
config_file = 'zuul-github-driver.conf'
tenant_config_file = 'config/locked-branches/main.yaml'
scheduler_count = 1
def test_exclude_locked_branches(self):
tenant = self.scheds.first.sched.abide.tenants\
.get('tenant-one')
project1 = tenant.untrusted_projects[0]
project2 = tenant.untrusted_projects[1]
project3 = tenant.untrusted_projects[2]
tpc1 = tenant.project_configs[project1.canonical_name]
tpc2 = tenant.project_configs[project2.canonical_name]
tpc3 = tenant.project_configs[project3.canonical_name]
# projects 1 and 2 should have parsed master
self.assertIn('master', tpc1.parsed_branch_config.keys())
self.assertIn('master', tpc2.parsed_branch_config.keys())
# project 3 should not because it excludes unprotected
self.assertEqual(0, len(tpc3.parsed_branch_config.keys()))
# now lock project2 and trigger reload
github = self.fake_github.getGithubClient()
repo = github.repo_from_project('org/project2')
rule = repo._set_branch_protection('master', True)
rule.lock_branch = True
pevent = self.fake_github.getPushEvent(project='org/project2',
ref='refs/heads/master')
self.fake_github.emitEvent(pevent)
self.waitUntilSettled()
tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
tpc1 = tenant.project_configs[project1.canonical_name]
tpc2 = tenant.project_configs[project2.canonical_name]
tpc3 = tenant.project_configs[project3.canonical_name]
# project2 should no longer have a master branch
self.assertIn('master', tpc1.parsed_branch_config.keys())
self.assertEqual(0, len(tpc2.parsed_branch_config.keys()))
self.assertEqual(0, len(tpc3.parsed_branch_config.keys()))
# Lock project 3 as well and ensure it's still excluded
repo = github.repo_from_project('org/project3')
rule = repo._set_branch_protection('master', True)
rule.lock_branch = True
pevent = self.fake_github.getPushEvent(project='org/project3',
ref='refs/heads/master')
self.fake_github.emitEvent(pevent)
self.waitUntilSettled()
tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
tpc1 = tenant.project_configs[project1.canonical_name]
tpc2 = tenant.project_configs[project2.canonical_name]
tpc3 = tenant.project_configs[project3.canonical_name]
# project3 is still excluded, but for a different reason
self.assertIn('master', tpc1.parsed_branch_config.keys())
self.assertEqual(0, len(tpc2.parsed_branch_config.keys()))
self.assertEqual(0, len(tpc3.parsed_branch_config.keys()))
class TestGithubWebhook(ZuulTestCase):
config_file = 'zuul-github-driver.conf'
scheduler_count = 1

View File

@ -1652,6 +1652,7 @@ class TenantParser(object):
'exclude': to_list(classes),
'shadow': to_list(str),
'exclude-unprotected-branches': bool,
'exclude-locked-branches': bool,
'extra-config-paths': no_dup_config_paths,
'load-branch': str,
'include-branches': to_list(str),
@ -1696,6 +1697,7 @@ class TenantParser(object):
'max-job-timeout': int,
'source': self.validateTenantSources(),
'exclude-unprotected-branches': bool,
'exclude-locked-branches': bool,
'allowed-triggers': to_list(str),
'allowed-reporters': to_list(str),
'allowed-labels': to_list(str),
@ -1736,6 +1738,9 @@ class TenantParser(object):
if conf.get('exclude-unprotected-branches') is not None:
tenant.exclude_unprotected_branches = \
conf['exclude-unprotected-branches']
if conf.get('exclude-locked-branches') is not None:
tenant.exclude_locked_branches = \
conf['exclude-locked-branches']
if conf.get('admin-rules') is not None:
tenant.admin_rules = as_list(conf['admin-rules'])
if conf.get('access-rules') is not None:
@ -1902,6 +1907,7 @@ class TenantParser(object):
project_include = current_include
shadow_projects = []
project_exclude_unprotected_branches = None
project_exclude_locked_branches = None
project_include_branches = None
project_exclude_branches = None
project_always_dynamic_branches = None
@ -1924,6 +1930,8 @@ class TenantParser(object):
project_include = frozenset(project_include - project_exclude)
project_exclude_unprotected_branches = conf[project_name].get(
'exclude-unprotected-branches', None)
project_exclude_locked_branches = conf[project_name].get(
'exclude-locked-branches', None)
project_include_branches = conf[project_name].get(
'include-branches', None)
if project_include_branches is not None:
@ -1969,6 +1977,8 @@ class TenantParser(object):
tenant_project_config.shadow_projects = shadow_projects
tenant_project_config.exclude_unprotected_branches = \
project_exclude_unprotected_branches
tenant_project_config.exclude_locked_branches = \
project_exclude_locked_branches
tenant_project_config.include_branches = project_include_branches
tenant_project_config.exclude_branches = project_exclude_branches
tenant_project_config.always_dynamic_branches = \

View File

@ -278,7 +278,7 @@ class ZKBranchCacheMixin:
:returns: The list of branch names.
"""
exclude_unprotected = tenant.getExcludeUnprotectedBranches(project)
exclude_locked = False
exclude_locked = tenant.getExcludeLockedBranches(project)
branches = None
required_flags = self._fetchProjectBranchesRequiredFlags(

View File

@ -1857,14 +1857,16 @@ class GithubConnection(ZKChangeCacheMixin, ZKBranchCacheMixin, BaseConnection):
valid_flags = set()
branch_infos = {}
if BranchFlag.PROTECTED in required_flags:
if {BranchFlag.PROTECTED, BranchFlag.LOCKED} & required_flags:
valid_flags.add(BranchFlag.PROTECTED)
valid_flags.add(BranchFlag.LOCKED)
for branch_name, locked in \
self.graphql_client.fetch_branch_protection(
github, project).items():
bi = branch_infos.setdefault(
branch_name, BranchInfo(branch_name))
bi.protected = True
bi.locked = locked
if BranchFlag.PRESENT in required_flags:
valid_flags.add(BranchFlag.PRESENT)
for branch_name in self._fetchProjectBranchesREST(

View File

@ -7168,6 +7168,7 @@ class TenantProjectConfig(object):
# The tenant's default setting of exclude_unprotected_branches will
# be overridden by this one if not None.
self.exclude_unprotected_branches = None
self.exclude_locked_branches = None
self.include_branches = None
self.exclude_branches = None
self.always_dynamic_branches = None
@ -8391,6 +8392,7 @@ class Tenant(object):
self.max_job_timeout = 10800
self.max_dependencies = None
self.exclude_unprotected_branches = False
self.exclude_locked_branches = False
self.default_base_job = None
self.layout = None
# The unparsed configuration from the main zuul config for
@ -8554,10 +8556,16 @@ class Tenant(object):
# match wins. The order is project -> tenant (default is false).
project_config = self.project_configs.get(project.canonical_name)
if project_config.exclude_unprotected_branches is not None:
exclude_unprotected = project_config.exclude_unprotected_branches
else:
exclude_unprotected = self.exclude_unprotected_branches
return exclude_unprotected
return project_config.exclude_unprotected_branches
return self.exclude_unprotected_branches
def getExcludeLockedBranches(self, project):
# Evaluate if locked branches should be excluded or not. The first
# match wins. The order is project -> tenant (default is false).
project_config = self.project_configs.get(project.canonical_name)
if project_config.exclude_locked_branches is not None:
return project_config.exclude_locked_branches
return self.exclude_locked_branches
def addConfigProject(self, tpc):
self.config_projects.append(tpc.project)