Fix implied branch matchers and override-checkout

When specifying job.override-checkout, we apply job variants from
that match the specified branch.  The mechanism we use to do that
is to create a synthetic Ref object to pass to the branch matcher
instead of the real branch of the Change (since the real branch
is likely different -- that's why override-checkout was specified).
However, branch matching behavior has gottes slightly more
sophisticated and Ref objects don't match in the same way that
Change objects do.

In particular, implied branch matchers match Ref subclasses that
have a branch attribute iff the match is exact.

This means that if a user constructed two branches:

* testbranch
* testbranch2

and used override-checkout to favor a job definition from testbranch2,
the implied branch matcher for the variant in testbranch would match
since the matcher behaved as if it were matching a Ref not a Change
or Branch.

To correct this, we update the simulated change object used in the
override-checkout variant matching routine to be a Branch (which
unsurprisingly has a branch attribute) instead of a Ref.

The test test_implied_branch_matcher_similar_override_checkout is added
to cover this test case.  Additionally, the test
test_implied_branch_matcher_similar is added for good measure (it tests
implied branch matchers in the same way but without specifying
override-checkout), though its behavior is already correct.

A release note is included since this may have an effect on job behavior.

Change-Id: I1104eaf02f752e8a73e9b04939f03a4888763b27
This commit is contained in:
James E. Blair 2022-10-20 13:34:48 -07:00
parent 6a5a2d0f6f
commit 95097565e6
3 changed files with 169 additions and 4 deletions

View File

@ -0,0 +1,13 @@
---
fixes:
- |
The use of implied branch matchers in jobs where
`override-checkout` was specified could cause some jobs to include
unintended variants. Specifically: job variants with implied
branch matchers on branches which are substring matches of a
branch specified in the `override-checkout` job attribute may have
been used when not intended.
This has been corrected so that the same job variant matching
process happens whether the change's branch or the branch
specified by override-checkout is used.

View File

@ -923,6 +923,159 @@ class TestBranchMismatch(ZuulTestCase):
self.assertIn('nodeset "bar" was not found', A.messages[0],
"A should have a syntax error reported")
def _updateConfig(self, config, branch):
file_dict = {'zuul.yaml': config}
C = self.fake_gerrit.addFakeChange('org/project1', branch, 'C',
files=file_dict)
C.setMerged()
self.fake_gerrit.addEvent(C.getChangeMergedEvent())
self.waitUntilSettled()
def test_implied_branch_matcher_similar(self):
# Test that we perform a full-text match with implied branch
# matchers.
self.create_branch('org/project1', 'testbranch')
self.create_branch('org/project1', 'testbranch2')
self.fake_gerrit.addEvent(
self.fake_gerrit.getFakeBranchCreatedEvent(
'org/project1', 'testbranch'))
self.fake_gerrit.addEvent(
self.fake_gerrit.getFakeBranchCreatedEvent(
'org/project1', 'testbranch2'))
config = textwrap.dedent(
"""
- job:
name: testjob
vars:
this_branch: testbranch
testbranch: true
- project:
check: {jobs: [testjob]}
""")
self._updateConfig(config, 'testbranch')
config = textwrap.dedent(
"""
- job:
name: testjob
vars:
this_branch: testbranch2
testbranch2: true
- project:
check: {jobs: [testjob]}
""")
self._updateConfig(config, 'testbranch2')
self.executor_server.hold_jobs_in_build = True
A = self.fake_gerrit.addFakeChange(
'org/project1', 'testbranch', 'A')
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
self.assertEqual(
{'testbranch': True, 'this_branch': 'testbranch'},
self.builds[0].job.combined_variables)
self.executor_server.release()
self.waitUntilSettled()
B = self.fake_gerrit.addFakeChange(
'org/project1', 'testbranch2', 'B')
self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
# The two jobs should have distinct variables. Notably,
# testbranch2 should not pick up vars from testbranch.
self.assertEqual(
{'testbranch2': True, 'this_branch': 'testbranch2'},
self.builds[0].job.combined_variables)
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
self.assertHistory([
dict(name='testjob', result='SUCCESS', changes='3,1'),
dict(name='testjob', result='SUCCESS', changes='4,1'),
], ordered=False)
def test_implied_branch_matcher_similar_override_checkout(self):
# Overriding a checkout has some branch matching implications.
# Make sure that we are performing a full-text match on
# branches when we override a checkout.
self.create_branch('org/project1', 'testbranch')
self.create_branch('org/project1', 'testbranch2')
self.fake_gerrit.addEvent(
self.fake_gerrit.getFakeBranchCreatedEvent(
'org/project1', 'testbranch'))
self.fake_gerrit.addEvent(
self.fake_gerrit.getFakeBranchCreatedEvent(
'org/project1', 'testbranch2'))
config = textwrap.dedent(
"""
- job:
name: testjob
vars:
this_branch: testbranch
testbranch: true
- project:
check: {jobs: [testjob]}
""")
self._updateConfig(config, 'testbranch')
config = textwrap.dedent(
"""
- job:
name: testjob
vars:
this_branch: testbranch2
testbranch2: true
- project:
check: {jobs: [testjob]}
""")
self._updateConfig(config, 'testbranch2')
self.executor_server.hold_jobs_in_build = True
config = textwrap.dedent(
"""
- job:
name: project-test1
- job:
name: testjob-testbranch
parent: testjob
override-checkout: testbranch
- job:
name: testjob-testbranch2
parent: testjob
override-checkout: testbranch2
- project:
check: {jobs: [testjob-testbranch, testjob-testbranch2]}
""")
file_dict = {'zuul.yaml': config}
A = self.fake_gerrit.addFakeChange(
'org/project1', 'master', 'A', files=file_dict)
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
self.assertEqual(
{'testbranch': True, 'this_branch': 'testbranch'},
self.builds[0].job.combined_variables)
# The two jobs should have distinct variables (notably, the
# variant on testbranch2 should not pick up vars from
# testbranch.
self.assertEqual(
{'testbranch2': True, 'this_branch': 'testbranch2'},
self.builds[1].job.combined_variables)
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
self.assertHistory([
dict(name='testjob-testbranch', result='SUCCESS', changes='3,1'),
dict(name='testjob-testbranch2', result='SUCCESS', changes='3,1'),
], ordered=False)
class TestBranchRef(ZuulTestCase):
tenant_config_file = 'config/branch-ref/main.yaml'

View File

@ -3135,10 +3135,9 @@ class Job(ConfigObject):
branch_change = change
else:
# If an override branch is supplied, create a very basic
# change (a Ref) and set its branch to the override
# branch.
branch_change = Ref(change.project)
branch_change.ref = override_branch
# change and set its branch to the override branch.
branch_change = Branch(change.project)
branch_change.branch = override_branch
if self.branch_matcher and not self.branch_matcher.matches(
branch_change):