Allow merge failures to have unique reporters.
For example, we would like to be able to count (with the MySQL
reporter) what jobs have failed because they can't be merged
vs. real failures.
Change-Id: I98eb8b8817bbda57efc2ef5bfcc2a5076fe8f4fd
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 5086d4d..24de765 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -238,6 +238,12 @@
reported back to Gerrit when at least one voting build fails.
Defaults to "Build failed."
+**merge-failure-message**
+ An optional field that supplies the introductory text in message
+ reported back to Gerrit when a change fails to merge with the
+ current state of the repository.
+ Defaults to "Merge failed."
+
**footer-message**
An optional field to supply additional information after test results.
Useful for adding information about the CI system such as debugging
@@ -413,6 +419,12 @@
Uses the same syntax as **success**, but describes what Zuul should
do if at least one job fails.
+**merge-failure**
+ Uses the same syntax as **success**, but describes what Zuul should
+ do if it is unable to merge in the patchset. If no merge-failure
+ reporters are listed then the ``failure`` reporters will be used to
+ notify of unsuccessful merges.
+
**start**
Uses the same syntax as **success**, but describes what Zuul should
do when a change is added to the pipeline manager. This can be used,
diff --git a/tests/fixtures/layout-merge-failure.yaml b/tests/fixtures/layout-merge-failure.yaml
new file mode 100644
index 0000000..72bc9c9
--- /dev/null
+++ b/tests/fixtures/layout-merge-failure.yaml
@@ -0,0 +1,56 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: post
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: ref-updated
+ ref: ^(?!refs/).*$
+
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ merge-failure-message: "The merge failed! For more information..."
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ merge-failure:
+ gerrit:
+ verified: -1
+ smtp:
+ to: you@example.com
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+projects:
+ - name: org/project
+ check:
+ - project-merge:
+ - project-test1
+ - project-test2
+ gate:
+ - project-merge:
+ - project-test1
+ - project-test2
diff --git a/tests/fixtures/layouts/bad_merge_failure.yaml b/tests/fixtures/layouts/bad_merge_failure.yaml
new file mode 100644
index 0000000..313d23b
--- /dev/null
+++ b/tests/fixtures/layouts/bad_merge_failure.yaml
@@ -0,0 +1,39 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+ merge-failure-message:
+
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ merge-failure:
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+projects:
+ - name: org/project
+ check:
+ - project-check
diff --git a/tests/fixtures/layouts/good_merge_failure.yaml b/tests/fixtures/layouts/good_merge_failure.yaml
new file mode 100644
index 0000000..f69b764
--- /dev/null
+++ b/tests/fixtures/layouts/good_merge_failure.yaml
@@ -0,0 +1,53 @@
+pipelines:
+ - name: check
+ manager: IndependentPipelineManager
+ merge-failure-message: "Could not merge the change. Please rebase..."
+ trigger:
+ gerrit:
+ - event: patchset-created
+ success:
+ gerrit:
+ verified: 1
+ failure:
+ gerrit:
+ verified: -1
+
+ - name: post
+ manager: IndependentPipelineManager
+ trigger:
+ gerrit:
+ - event: ref-updated
+ ref: ^(?!refs/).*$
+ merge-failure:
+ gerrit:
+ verified: -1
+
+ - name: gate
+ manager: DependentPipelineManager
+ failure-message: Build failed. For information on how to proceed, see http://wiki.example.org/Test_Failures
+ trigger:
+ gerrit:
+ - event: comment-added
+ approval:
+ - approved: 1
+ success:
+ gerrit:
+ verified: 2
+ submit: true
+ failure:
+ gerrit:
+ verified: -2
+ merge-failure:
+ gerrit:
+ verified: -1
+ smtp:
+ to: you@example.com
+ start:
+ gerrit:
+ verified: 0
+ precedence: high
+
+projects:
+ - name: org/project
+ check:
+ - project-check
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 1d7be4a..496d468 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -3712,3 +3712,82 @@
self.assertEqual(failure_body, self.smtp_messages[0]['body'])
self.assertEqual(success_body, self.smtp_messages[1]['body'])
+
+ def test_merge_failure_reporters(self):
+ """Check that the config is set up correctly"""
+
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-merge-failure.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ self.assertEqual(
+ "Merge Failed.\n\nThis change was unable to be automatically "
+ "merged with the current state of the repository. Please rebase "
+ "your change and upload a new patchset.",
+ self.sched.layout.pipelines['check'].merge_failure_message)
+ self.assertEqual(
+ "The merge failed! For more information...",
+ self.sched.layout.pipelines['gate'].merge_failure_message)
+
+ self.assertEqual(
+ len(self.sched.layout.pipelines['check'].merge_failure_actions), 1)
+ self.assertEqual(
+ len(self.sched.layout.pipelines['gate'].merge_failure_actions), 2)
+
+ self.assertTrue(isinstance(
+ self.sched.layout.pipelines['check'].merge_failure_actions[0].
+ reporter, zuul.reporter.gerrit.Reporter))
+
+ self.assertTrue(
+ (
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[0].reporter,
+ zuul.reporter.smtp.Reporter) and
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[1].reporter,
+ zuul.reporter.gerrit.Reporter)
+ ) or (
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[0].reporter,
+ zuul.reporter.gerrit.Reporter) and
+ isinstance(self.sched.layout.pipelines['gate'].
+ merge_failure_actions[1].reporter,
+ zuul.reporter.smtp.Reporter)
+ )
+ )
+
+ def test_merge_failure_reports(self):
+ """Check that when a change fails to merge the correct message is sent
+ to the correct reporter"""
+ self.config.set('zuul', 'layout_config',
+ 'tests/fixtures/layout-merge-failure.yaml')
+ self.sched.reconfigure(self.config)
+ self.registerJobs()
+
+ # Check a test failure isn't reported to SMTP
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('CRVW', 2)
+ self.worker.addFailTest('project-test1', A)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(3, len(self.history)) # 3 jobs
+ self.assertEqual(0, len(self.smtp_messages))
+
+ # Check a merge failure is reported to SMTP
+ # B should be merged, but C will conflict with B
+ B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+ B.addPatchset(['conflict'])
+ C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+ C.addPatchset(['conflict'])
+ B.addApproval('CRVW', 2)
+ C.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+ self.waitUntilSettled()
+
+ self.assertEqual(6, len(self.history)) # A and B jobs
+ self.assertEqual(1, len(self.smtp_messages))
+ self.assertEqual('The merge failed! For more information...',
+ self.smtp_messages[0]['body'])
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index de58c25..18b532d 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -79,11 +79,13 @@
'description': str,
'success-message': str,
'failure-message': str,
+ 'merge-failure-message': str,
'footer-message': str,
'dequeue-on-new-patchset': bool,
'trigger': trigger,
'success': report_actions,
'failure': report_actions,
+ 'merge-failure': report_actions,
'start': report_actions,
'window': window,
'window-floor': window_floor,
diff --git a/zuul/model.py b/zuul/model.py
index b20c08e..1a7c421 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -63,6 +63,7 @@
self.name = name
self.description = None
self.failure_message = None
+ self.merge_failure_message = None
self.success_message = None
self.footer_message = None
self.dequeue_on_new_patchset = True
@@ -171,6 +172,11 @@
return False
return True
+ def didMergerSucceed(self, item):
+ if item.current_build_set.unable_to_merge:
+ return False
+ return True
+
def didAnyJobFail(self, item):
for job in self.getJobs(item.change):
if not job.voting:
@@ -206,9 +212,8 @@
fakebuild.result = 'SKIPPED'
item.addBuild(fakebuild)
- def setUnableToMerge(self, item, msg):
+ def setUnableToMerge(self, item):
item.current_build_set.unable_to_merge = True
- item.current_build_set.unable_to_merge_message = msg
root = self.getJobTree(item.change.project)
for job in root.getJobs():
fakebuild = Build(job, None)
@@ -677,7 +682,6 @@
self.commit = None
self.zuul_url = None
self.unable_to_merge = False
- self.unable_to_merge_message = None
self.failing_reasons = []
self.merge_state = self.NEW
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index c941a98..1a4474c 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -226,6 +226,11 @@
pipeline.precedence = precedence
pipeline.failure_message = conf_pipeline.get('failure-message',
"Build failed.")
+ pipeline.merge_failure_message = conf_pipeline.get(
+ 'merge-failure-message', "Merge Failed.\n\nThis change was "
+ "unable to be automatically merged with the current state of "
+ "the repository. Please rebase your change and upload a new "
+ "patchset.")
pipeline.success_message = conf_pipeline.get('success-message',
"Build succeeded.")
pipeline.footer_message = conf_pipeline.get('footer-message', "")
@@ -233,7 +238,7 @@
'dequeue-on-new-patchset', True)
action_reporters = {}
- for action in ['start', 'success', 'failure']:
+ for action in ['start', 'success', 'failure', 'merge-failure']:
action_reporters[action] = []
if conf_pipeline.get(action):
for reporter_name, params \
@@ -247,6 +252,11 @@
pipeline.start_actions = action_reporters['start']
pipeline.success_actions = action_reporters['success']
pipeline.failure_actions = action_reporters['failure']
+ if len(action_reporters['merge-failure']) > 0:
+ pipeline.merge_failure_actions = \
+ action_reporters['merge-failure']
+ else:
+ pipeline.merge_failure_actions = action_reporters['failure']
pipeline.window = conf_pipeline.get('window', 20)
pipeline.window_floor = conf_pipeline.get('window-floor', 3)
@@ -936,6 +946,8 @@
self.log.info(" %s" % self.pipeline.success_actions)
self.log.info(" On failure:")
self.log.info(" %s" % self.pipeline.failure_actions)
+ self.log.info(" On merge-failure:")
+ self.log.info(" %s" % self.pipeline.merge_failure_actions)
def getSubmitAllowNeeds(self):
# Get a list of code review labels that are allowed to be
@@ -1334,10 +1346,7 @@
build_set.commit = item.change.newrev
if not build_set.commit:
self.log.info("Unable to merge change %s" % item.change)
- msg = ("This change was unable to be automatically merged "
- "with the current state of the repository. Please "
- "rebase your change and upload a new patchset.")
- self.pipeline.setUnableToMerge(item, msg)
+ self.pipeline.setUnableToMerge(item)
def reportItem(self, item):
if item.reported:
@@ -1370,10 +1379,12 @@
self.log.debug("Reporting change %s" % item.change)
ret = True # Means error as returned by trigger.report
if self.pipeline.didAllJobsSucceed(item):
- self.log.debug("success %s %s" % (self.pipeline.success_actions,
- self.pipeline.failure_actions))
+ self.log.debug("success %s" % (self.pipeline.success_actions))
actions = self.pipeline.success_actions
item.setReportedResult('SUCCESS')
+ elif not self.pipeline.didMergerSucceed(item):
+ actions = self.pipeline.merge_failure_actions
+ item.setReportedResult('MERGER_FAILURE')
else:
actions = self.pipeline.failure_actions
item.setReportedResult('FAILURE')
@@ -1395,66 +1406,72 @@
def formatReport(self, item):
ret = ''
+
+ if not self.pipeline.didMergerSucceed(item):
+ ret += self.pipeline.merge_failure_message
+ if item.dequeued_needing_change:
+ ret += ('\n\nThis change depends on a change that failed to '
+ 'merge.')
+ if self.pipeline.footer_message:
+ ret += '\n\n' + self.pipeline.footer_message
+ return ret
+
if self.pipeline.didAllJobsSucceed(item):
ret += self.pipeline.success_message + '\n\n'
else:
ret += self.pipeline.failure_message + '\n\n'
- if item.dequeued_needing_change:
- ret += "This change depends on a change that failed to merge."
- elif item.current_build_set.unable_to_merge_message:
- ret += item.current_build_set.unable_to_merge_message
+ if self.sched.config.has_option('zuul', 'url_pattern'):
+ url_pattern = self.sched.config.get('zuul', 'url_pattern')
else:
- if self.sched.config.has_option('zuul', 'url_pattern'):
- url_pattern = self.sched.config.get('zuul', 'url_pattern')
+ url_pattern = None
+
+ for job in self.pipeline.getJobs(item.change):
+ build = item.current_build_set.getBuild(job.name)
+ result = build.result
+ pattern = url_pattern
+ if result == 'SUCCESS':
+ if job.success_message:
+ result = job.success_message
+ if job.success_pattern:
+ pattern = job.success_pattern
+ elif result == 'FAILURE':
+ if job.failure_message:
+ result = job.failure_message
+ if job.failure_pattern:
+ pattern = job.failure_pattern
+ if pattern:
+ url = pattern.format(change=item.change,
+ pipeline=self.pipeline,
+ job=job,
+ build=build)
else:
- url_pattern = None
- for job in self.pipeline.getJobs(item.change):
- build = item.current_build_set.getBuild(job.name)
- result = build.result
- pattern = url_pattern
- if result == 'SUCCESS':
- if job.success_message:
- result = job.success_message
- if job.success_pattern:
- pattern = job.success_pattern
- elif result == 'FAILURE':
- if job.failure_message:
- result = job.failure_message
- if job.failure_pattern:
- pattern = job.failure_pattern
- if pattern:
- url = pattern.format(change=item.change,
- pipeline=self.pipeline,
- job=job,
- build=build)
+ url = build.url or job.name
+ if not job.voting:
+ voting = ' (non-voting)'
+ else:
+ voting = ''
+ if self.report_times and build.end_time and build.start_time:
+ dt = int(build.end_time - build.start_time)
+ m, s = divmod(dt, 60)
+ h, m = divmod(m, 60)
+ if h:
+ elapsed = ' in %dh %02dm %02ds' % (h, m, s)
+ elif m:
+ elapsed = ' in %dm %02ds' % (m, s)
else:
- url = build.url or job.name
- if not job.voting:
- voting = ' (non-voting)'
- else:
- voting = ''
- if self.report_times and build.end_time and build.start_time:
- dt = int(build.end_time - build.start_time)
- m, s = divmod(dt, 60)
- h, m = divmod(m, 60)
- if h:
- elapsed = ' in %dh %02dm %02ds' % (h, m, s)
- elif m:
- elapsed = ' in %dm %02ds' % (m, s)
- else:
- elapsed = ' in %ds' % (s)
- else:
- elapsed = ''
- name = ''
- if self.sched.config.has_option('zuul', 'job_name_in_report'):
- if self.sched.config.getboolean('zuul',
- 'job_name_in_report'):
- name = job.name + ' '
- ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
- voting)
- ret += '\n'
- ret += self.pipeline.footer_message
+ elapsed = ' in %ds' % (s)
+ else:
+ elapsed = ''
+ name = ''
+ if self.sched.config.has_option('zuul', 'job_name_in_report'):
+ if self.sched.config.getboolean('zuul',
+ 'job_name_in_report'):
+ name = job.name + ' '
+ ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
+ voting)
+ if self.pipeline.footer_message:
+ ret += '\n' + self.pipeline.footer_message
return ret
def formatDescription(self, build):