Have zuul handle merge failures.
If Zuul is unable to merge a change, don't run any jobs, and report
the merge failure to gerrit directly (but still observing the
dependent change queue, in case a change ahead caused the merge
failure).
Adds a test for this situation.
Change-Id: I1ee2a8846b159db385019352cc04af2140db81af
Reviewed-on: https://review.openstack.org/11421
Reviewed-by: Clark Boylan <clark.boylan@gmail.com>
Approved: James E. Blair <corvus@inaugust.com>
Tested-by: Jenkins
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index fc6d4a4..862c382 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -74,7 +74,7 @@
repo.create_tag('init')
-def add_fake_change_to_repo(project, branch, change_num, patchset, msg):
+def add_fake_change_to_repo(project, branch, change_num, patchset, msg, fn):
path = os.path.join("/tmp/zuul-test/upstream", project)
repo = git.Repo(path)
ref = ChangeReference.create(repo, '1/%s/%s' % (change_num,
@@ -85,9 +85,9 @@
repo.git.clean('-x', '-f', '-d')
path = os.path.join("/tmp/zuul-test/upstream", project)
- fn = os.path.join(path, '%s-%s' % (branch, change_num))
+ fn = os.path.join(path, fn)
f = open(fn, 'w')
- f.write("test\n")
+ f.write("test %s %s %s\n" % (branch, change_num, patchset))
f.close()
repo.index.add([fn])
repo.index.commit(msg)
@@ -177,9 +177,14 @@
self.data['currentPatchSet'] = d
self.patchsets.append(d)
self.data['submitRecords'] = self.getSubmitRecords()
+ if files:
+ fn = files[0]
+ else:
+ fn = '%s-%s' % (self.branch, self.number)
add_fake_change_to_repo(self.project, self.branch,
self.number, self.latest_patchset,
- self.subject + '-' + str(self.latest_patchset))
+ self.subject + '-' + str(self.latest_patchset),
+ fn)
def addApproval(self, category, value):
approval = {'description': self.categories[category][0],
@@ -1009,6 +1014,7 @@
ref = jobs[-1].parameters['ZUUL_REF']
self.fake_jenkins.hold_jobs_in_queue = False
self.fake_jenkins.fakeRelease()
+ self.waitUntilSettled()
path = os.path.join("/tmp/zuul-test/git/org/project")
repo = git.Repo(path)
@@ -1017,3 +1023,39 @@
print ' repo messages :', repo_messages
correct_messages = ['initial commit', 'A-1', 'B-1', 'C-1']
assert repo_messages == correct_messages
+
+ def test_build_configuration_conflict(self):
+ "Test that merge conflicts are handled"
+ self.fake_jenkins.hold_jobs_in_queue = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addPatchset(['conflict'])
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addPatchset(['conflict'])
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ A.addApproval('CRVW', 2)
+ B.addApproval('CRVW', 2)
+ C.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+ self.waitUntilSettled()
+
+ jobs = self.fake_jenkins.all_jobs
+
+ self.fake_jenkins.fakeRelease('.*-merge')
+ self.waitUntilSettled()
+ self.fake_jenkins.fakeRelease('.*-merge')
+ self.waitUntilSettled()
+ self.fake_jenkins.fakeRelease('.*-merge')
+ self.waitUntilSettled()
+ ref = jobs[-1].parameters['ZUUL_REF']
+ self.fake_jenkins.hold_jobs_in_queue = False
+ self.fake_jenkins.fakeRelease()
+ self.waitUntilSettled()
+
+ assert A.data['status'] == 'MERGED'
+ assert B.data['status'] == 'NEW'
+ assert C.data['status'] == 'MERGED'
+ assert A.reported == 2
+ assert B.reported == 2
+ assert C.reported == 2
diff --git a/zuul/merger.py b/zuul/merger.py
index a1345b8..3efc98e 100644
--- a/zuul/merger.py
+++ b/zuul/merger.py
@@ -120,7 +120,7 @@
repo.cherryPick(change.refspec)
repo.setZuulRef(change.branch + '/' + target_ref, 'HEAD')
except:
- self.log.exception("Unable to merge %s" % change)
+ self.log.info("Unable to merge %s" % change)
return False
return True
diff --git a/zuul/model.py b/zuul/model.py
index 1674082..195aa88 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -164,17 +164,22 @@
else:
ret += 'Build failed\n\n'
- for job in self.getJobs(changeish):
- build = changeish.current_build_set.getBuild(job.name)
- result = build.result
- if result == 'SUCCESS' and job.success_message:
- result = job.success_message
- elif result == 'FAILURE' and job.failure_message:
- result = job.failure_message
- url = build.url
- if not url:
- url = job.name
- ret += '- %s : %s\n' % (url, result)
+ if changeish.current_build_set.unable_to_merge:
+ ret += "This change was unable to be automatically merged "\
+ "with the current state of the repository. Please "\
+ "rebase your change and upload a new patchset."
+ else:
+ for job in self.getJobs(changeish):
+ build = changeish.current_build_set.getBuild(job.name)
+ result = build.result
+ if result == 'SUCCESS' and job.success_message:
+ result = job.success_message
+ elif result == 'FAILURE' and job.failure_message:
+ result = job.failure_message
+ url = build.url
+ if not url:
+ url = job.name
+ ret += '- %s : %s\n' % (url, result)
return ret
def formatDescription(self, build):
@@ -287,6 +292,14 @@
fakebuild.result = 'SKIPPED'
changeish.addBuild(fakebuild)
+ def setUnableToMerge(self, changeish):
+ changeish.current_build_set.unable_to_merge = True
+ root = self.getJobTree(changeish.project)
+ for job in root.getJobs():
+ fakebuild = Build(job, None)
+ fakebuild.result = 'SKIPPED'
+ changeish.addBuild(fakebuild)
+
class ChangeQueue(object):
"""DependentPipelines have multiple parallel queues shared by
@@ -433,6 +446,7 @@
self.next_build_set = None
self.previous_build_set = None
self.ref = None
+ self.unable_to_merge = False
def setConfiguration(self):
# The change isn't enqueued until after it's created
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 5fd2126..551af22 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -482,8 +482,13 @@
if not ref:
change.current_build_set.setConfiguration()
ref = change.current_build_set.getRef()
- self.sched.merger.mergeChanges([change], ref,
- mode=model.MERGE_IF_NECESSARY)
+ merged = self.sched.merger.mergeChanges([change], ref,
+ mode=model.MERGE_IF_NECESSARY)
+ if not merged:
+ self.log.info("Unable to merge change %s" % change)
+ self.pipeline.setUnableToMerge(change)
+ self.possiblyReportChange(change)
+ return
for job in self.pipeline.findJobsToRun(change):
self.log.debug("Found job %s for change %s" % (job, change))
@@ -747,7 +752,13 @@
ref = change.current_build_set.getRef()
dependent_changes = self._getDependentChanges(change)
dependent_changes.reverse()
- self.sched.merger.mergeChanges(dependent_changes + [change], ref)
+ all_changes = dependent_changes + [change]
+ merged = self.sched.merger.mergeChanges(all_changes, ref)
+ if not merged:
+ self.log.info("Unable to merge changes %s" % all_changes)
+ self.pipeline.setUnableToMerge(change)
+ self.possiblyReportChange(change)
+ return
#TODO: remove this line after GERRIT_CHANGES is gone
dependent_changes = self._getDependentChanges(change)