Merge "Allow merge failures to have unique reporters."

This commit is contained in:
Jenkins 2014-03-24 17:30:15 +00:00 committed by Gerrit Code Review
commit 6e81141f58
8 changed files with 324 additions and 62 deletions

View File

@ -238,6 +238,12 @@ explanation of each of the parameters::
reported back to Gerrit when at least one voting build fails. reported back to Gerrit when at least one voting build fails.
Defaults to "Build failed." 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** **footer-message**
An optional field to supply additional information after test results. An optional field to supply additional information after test results.
Useful for adding information about the CI system such as debugging Useful for adding information about the CI system such as debugging
@ -413,6 +419,12 @@ explanation of each of the parameters::
Uses the same syntax as **success**, but describes what Zuul should Uses the same syntax as **success**, but describes what Zuul should
do if at least one job fails. 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** **start**
Uses the same syntax as **success**, but describes what Zuul should 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, do when a change is added to the pipeline manager. This can be used,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -3731,3 +3731,82 @@ For CI problems and help debugging, contact ci@example.org"""
self.assertEqual(failure_body, self.smtp_messages[0]['body']) self.assertEqual(failure_body, self.smtp_messages[0]['body'])
self.assertEqual(success_body, self.smtp_messages[1]['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'])

View File

@ -79,11 +79,13 @@ class LayoutSchema(object):
'description': str, 'description': str,
'success-message': str, 'success-message': str,
'failure-message': str, 'failure-message': str,
'merge-failure-message': str,
'footer-message': str, 'footer-message': str,
'dequeue-on-new-patchset': bool, 'dequeue-on-new-patchset': bool,
'trigger': trigger, 'trigger': trigger,
'success': report_actions, 'success': report_actions,
'failure': report_actions, 'failure': report_actions,
'merge-failure': report_actions,
'start': report_actions, 'start': report_actions,
'window': window, 'window': window,
'window-floor': window_floor, 'window-floor': window_floor,

View File

@ -63,6 +63,7 @@ class Pipeline(object):
self.name = name self.name = name
self.description = None self.description = None
self.failure_message = None self.failure_message = None
self.merge_failure_message = None
self.success_message = None self.success_message = None
self.footer_message = None self.footer_message = None
self.dequeue_on_new_patchset = True self.dequeue_on_new_patchset = True
@ -171,6 +172,11 @@ class Pipeline(object):
return False return False
return True return True
def didMergerSucceed(self, item):
if item.current_build_set.unable_to_merge:
return False
return True
def didAnyJobFail(self, item): def didAnyJobFail(self, item):
for job in self.getJobs(item.change): for job in self.getJobs(item.change):
if not job.voting: if not job.voting:
@ -206,9 +212,8 @@ class Pipeline(object):
fakebuild.result = 'SKIPPED' fakebuild.result = 'SKIPPED'
item.addBuild(fakebuild) 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 = True
item.current_build_set.unable_to_merge_message = msg
root = self.getJobTree(item.change.project) root = self.getJobTree(item.change.project)
for job in root.getJobs(): for job in root.getJobs():
fakebuild = Build(job, None) fakebuild = Build(job, None)
@ -677,7 +682,6 @@ class BuildSet(object):
self.commit = None self.commit = None
self.zuul_url = None self.zuul_url = None
self.unable_to_merge = False self.unable_to_merge = False
self.unable_to_merge_message = None
self.failing_reasons = [] self.failing_reasons = []
self.merge_state = self.NEW self.merge_state = self.NEW

View File

@ -226,6 +226,11 @@ class Scheduler(threading.Thread):
pipeline.precedence = precedence pipeline.precedence = precedence
pipeline.failure_message = conf_pipeline.get('failure-message', pipeline.failure_message = conf_pipeline.get('failure-message',
"Build failed.") "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', pipeline.success_message = conf_pipeline.get('success-message',
"Build succeeded.") "Build succeeded.")
pipeline.footer_message = conf_pipeline.get('footer-message', "") pipeline.footer_message = conf_pipeline.get('footer-message', "")
@ -233,7 +238,7 @@ class Scheduler(threading.Thread):
'dequeue-on-new-patchset', True) 'dequeue-on-new-patchset', True)
action_reporters = {} action_reporters = {}
for action in ['start', 'success', 'failure']: for action in ['start', 'success', 'failure', 'merge-failure']:
action_reporters[action] = [] action_reporters[action] = []
if conf_pipeline.get(action): if conf_pipeline.get(action):
for reporter_name, params \ for reporter_name, params \
@ -247,6 +252,11 @@ class Scheduler(threading.Thread):
pipeline.start_actions = action_reporters['start'] pipeline.start_actions = action_reporters['start']
pipeline.success_actions = action_reporters['success'] pipeline.success_actions = action_reporters['success']
pipeline.failure_actions = action_reporters['failure'] 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 = conf_pipeline.get('window', 20)
pipeline.window_floor = conf_pipeline.get('window-floor', 3) pipeline.window_floor = conf_pipeline.get('window-floor', 3)
@ -936,6 +946,8 @@ class BasePipelineManager(object):
self.log.info(" %s" % self.pipeline.success_actions) self.log.info(" %s" % self.pipeline.success_actions)
self.log.info(" On failure:") self.log.info(" On failure:")
self.log.info(" %s" % self.pipeline.failure_actions) 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): def getSubmitAllowNeeds(self):
# Get a list of code review labels that are allowed to be # Get a list of code review labels that are allowed to be
@ -1334,10 +1346,7 @@ class BasePipelineManager(object):
build_set.commit = item.change.newrev build_set.commit = item.change.newrev
if not build_set.commit: if not build_set.commit:
self.log.info("Unable to merge change %s" % item.change) self.log.info("Unable to merge change %s" % item.change)
msg = ("This change was unable to be automatically merged " self.pipeline.setUnableToMerge(item)
"with the current state of the repository. Please "
"rebase your change and upload a new patchset.")
self.pipeline.setUnableToMerge(item, msg)
def reportItem(self, item): def reportItem(self, item):
if item.reported: if item.reported:
@ -1370,10 +1379,12 @@ class BasePipelineManager(object):
self.log.debug("Reporting change %s" % item.change) self.log.debug("Reporting change %s" % item.change)
ret = True # Means error as returned by trigger.report ret = True # Means error as returned by trigger.report
if self.pipeline.didAllJobsSucceed(item): if self.pipeline.didAllJobsSucceed(item):
self.log.debug("success %s %s" % (self.pipeline.success_actions, self.log.debug("success %s" % (self.pipeline.success_actions))
self.pipeline.failure_actions))
actions = self.pipeline.success_actions actions = self.pipeline.success_actions
item.setReportedResult('SUCCESS') item.setReportedResult('SUCCESS')
elif not self.pipeline.didMergerSucceed(item):
actions = self.pipeline.merge_failure_actions
item.setReportedResult('MERGER_FAILURE')
else: else:
actions = self.pipeline.failure_actions actions = self.pipeline.failure_actions
item.setReportedResult('FAILURE') item.setReportedResult('FAILURE')
@ -1395,66 +1406,72 @@ class BasePipelineManager(object):
def formatReport(self, item): def formatReport(self, item):
ret = '' 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): if self.pipeline.didAllJobsSucceed(item):
ret += self.pipeline.success_message + '\n\n' ret += self.pipeline.success_message + '\n\n'
else: else:
ret += self.pipeline.failure_message + '\n\n' ret += self.pipeline.failure_message + '\n\n'
if item.dequeued_needing_change: if self.sched.config.has_option('zuul', 'url_pattern'):
ret += "This change depends on a change that failed to merge." url_pattern = self.sched.config.get('zuul', 'url_pattern')
elif item.current_build_set.unable_to_merge_message:
ret += item.current_build_set.unable_to_merge_message
else: else:
if self.sched.config.has_option('zuul', 'url_pattern'): url_pattern = None
url_pattern = self.sched.config.get('zuul', 'url_pattern')
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: else:
url_pattern = None url = build.url or job.name
for job in self.pipeline.getJobs(item.change): if not job.voting:
build = item.current_build_set.getBuild(job.name) voting = ' (non-voting)'
result = build.result else:
pattern = url_pattern voting = ''
if result == 'SUCCESS': if self.report_times and build.end_time and build.start_time:
if job.success_message: dt = int(build.end_time - build.start_time)
result = job.success_message m, s = divmod(dt, 60)
if job.success_pattern: h, m = divmod(m, 60)
pattern = job.success_pattern if h:
elif result == 'FAILURE': elapsed = ' in %dh %02dm %02ds' % (h, m, s)
if job.failure_message: elif m:
result = job.failure_message elapsed = ' in %dm %02ds' % (m, s)
if job.failure_pattern:
pattern = job.failure_pattern
if pattern:
url = pattern.format(change=item.change,
pipeline=self.pipeline,
job=job,
build=build)
else: else:
url = build.url or job.name elapsed = ' in %ds' % (s)
if not job.voting: else:
voting = ' (non-voting)' elapsed = ''
else: name = ''
voting = '' if self.sched.config.has_option('zuul', 'job_name_in_report'):
if self.report_times and build.end_time and build.start_time: if self.sched.config.getboolean('zuul',
dt = int(build.end_time - build.start_time) 'job_name_in_report'):
m, s = divmod(dt, 60) name = job.name + ' '
h, m = divmod(m, 60) ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
if h: voting)
elapsed = ' in %dh %02dm %02ds' % (h, m, s) if self.pipeline.footer_message:
elif m: ret += '\n' + self.pipeline.footer_message
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
return ret return ret
def formatDescription(self, build): def formatDescription(self, build):