Move Item formatting into Reporters
This will allow a reporter to decide how to handle the results of
each item. It can use the common plain text formatter (as has been
the case, '_formatItemReport') or it may generate a report itself.
This will be useful for the MySQL reporter where it will want to
create an entry in a table for each build.
Action reporters are now configured with the action type, so they can
react differently for the success, failure, etc.
Co-Authored-By: Jan Hruban <jan.hruban@gooddata.com>
Change-Id: Ib270334ff694fdff69a3028db8d6d7fed1f05176
diff --git a/zuul/reporter/__init__.py b/zuul/reporter/__init__.py
index d9857da..e29f9a7 100644
--- a/zuul/reporter/__init__.py
+++ b/zuul/reporter/__init__.py
@@ -28,12 +28,16 @@
self.reporter_config = reporter_config
self.sched = sched
self.connection = connection
+ self._action = None
+
+ def setAction(self, action):
+ self._action = action
def stop(self):
"""Stop the reporter."""
@abc.abstractmethod
- def report(self, source, change, message):
+ def report(self, source, pipeline, item):
"""Send the compiled report message."""
def getSubmitAllowNeeds(self):
@@ -46,3 +50,115 @@
def postConfig(self):
"""Run tasks after configuration is reloaded"""
+
+ def _getFormatter(self):
+ format_methods = {
+ 'start': self._formatItemReportStart,
+ 'success': self._formatItemReportSuccess,
+ 'failure': self._formatItemReportFailure,
+ 'merge-failure': self._formatItemReportMergeFailure,
+ 'disabled': self._formatItemReportDisabled
+ }
+ return format_methods[self._action]
+
+ def _formatItemReport(self, pipeline, item):
+ """Format a report from the given items. Usually to provide results to
+ a reporter taking free-form text."""
+ ret = self._getFormatter()(pipeline, item)
+
+ if pipeline.footer_message:
+ ret += '\n' + pipeline.footer_message
+
+ return ret
+
+ def _formatItemReportStart(self, pipeline, item):
+ msg = "Starting %s jobs." % pipeline.name
+ if self.sched.config.has_option('zuul', 'status_url'):
+ msg += "\n" + self.sched.config.get('zuul', 'status_url')
+ return msg
+
+ def _formatItemReportSuccess(self, pipeline, item):
+ return (pipeline.success_message + '\n\n' +
+ self._formatItemReportJobs(pipeline, item))
+
+ def _formatItemReportFailure(self, pipeline, item):
+ if item.dequeued_needing_change:
+ msg = 'This change depends on a change that failed to merge.\n'
+ else:
+ msg = (pipeline.failure_message + '\n\n' +
+ self._formatItemReportJobs(pipeline, item))
+ return msg
+
+ def _formatItemReportMergeFailure(self, pipeline, item):
+ return pipeline.merge_failure_message
+
+ def _formatItemReportDisabled(self, pipeline, item):
+ if item.current_build_set.result == 'SUCCESS':
+ return self._formatItemReportSuccess(pipeline, item)
+ elif item.current_build_set.result == 'FAILURE':
+ return self._formatItemReportFailure(pipeline, item)
+ else:
+ return self._formatItemReport(pipeline, item)
+
+ def _formatItemReportJobs(self, pipeline, item):
+ # Return the list of jobs portion of the report
+ ret = ''
+
+ if self.sched.config.has_option('zuul', 'url_pattern'):
+ url_pattern = self.sched.config.get('zuul', 'url_pattern')
+ else:
+ url_pattern = None
+
+ for job in pipeline.getJobs(item):
+ 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=pipeline,
+ job=job,
+ build=build)
+ else:
+ url = build.url or job.name
+ if not job.voting:
+ voting = ' (non-voting)'
+ else:
+ voting = ''
+
+ if self.sched.config and self.sched.config.has_option(
+ 'zuul', 'report_times'):
+ report_times = self.sched.config.getboolean(
+ 'zuul', 'report_times')
+ else:
+ report_times = True
+
+ if 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)
+ return ret
diff --git a/zuul/reporter/gerrit.py b/zuul/reporter/gerrit.py
index e1b0571..1427449 100644
--- a/zuul/reporter/gerrit.py
+++ b/zuul/reporter/gerrit.py
@@ -25,16 +25,18 @@
name = 'gerrit'
log = logging.getLogger("zuul.reporter.gerrit.Reporter")
- def report(self, source, change, message):
+ def report(self, source, pipeline, item):
"""Send a message to gerrit."""
- self.log.debug("Report change %s, params %s, message: %s" %
- (change, self.reporter_config, message))
- changeid = '%s,%s' % (change.number, change.patchset)
- change._ref_sha = source.getRefSha(change.project.name,
- 'refs/heads/' + change.branch)
+ message = self._formatItemReport(pipeline, item)
- return self.connection.review(change.project.name, changeid, message,
- self.reporter_config)
+ self.log.debug("Report change %s, params %s, message: %s" %
+ (item.change, self.reporter_config, message))
+ changeid = '%s,%s' % (item.change.number, item.change.patchset)
+ item.change._ref_sha = source.getRefSha(
+ item.change.project.name, 'refs/heads/' + item.change.branch)
+
+ return self.connection.review(item.change.project.name, changeid,
+ message, self.reporter_config)
def getSubmitAllowNeeds(self):
"""Get a list of code review labels that are allowed to be
diff --git a/zuul/reporter/smtp.py b/zuul/reporter/smtp.py
index 4daa6df..586b941 100644
--- a/zuul/reporter/smtp.py
+++ b/zuul/reporter/smtp.py
@@ -24,10 +24,12 @@
name = 'smtp'
log = logging.getLogger("zuul.reporter.smtp.Reporter")
- def report(self, source, change, message):
+ def report(self, source, pipeline, item):
"""Send the compiled report message via smtp."""
+ message = self._formatItemReport(pipeline, item)
+
self.log.debug("Report change %s, params %s, message: %s" %
- (change, self.reporter_config, message))
+ (item.change, self.reporter_config, message))
from_email = self.reporter_config['from'] \
if 'from' in self.reporter_config else None
@@ -35,9 +37,10 @@
if 'to' in self.reporter_config else None
if 'subject' in self.reporter_config:
- subject = self.reporter_config['subject'].format(change=change)
+ subject = self.reporter_config['subject'].format(
+ change=item.change)
else:
- subject = "Report for change %s" % change
+ subject = "Report for change %s" % item.change
self.connection.sendMail(subject, message, from_email, to_email)
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 91bcf13..f8321d1 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -374,6 +374,7 @@
in conf_pipeline.get(conf_key).items():
reporter = self._getReporterDriver(reporter_name,
params)
+ reporter.setAction(conf_key)
reporter_set.append(reporter)
setattr(pipeline, action, reporter_set)
@@ -1054,12 +1055,6 @@
self.pipeline = pipeline
self.event_filters = []
self.changeish_filters = []
- if self.sched.config and self.sched.config.has_option(
- 'zuul', 'report_times'):
- self.report_times = self.sched.config.getboolean(
- 'zuul', 'report_times')
- else:
- self.report_times = True
def __str__(self):
return "<%s %s>" % (self.__class__.__name__, self.pipeline.name)
@@ -1153,32 +1148,30 @@
return True
return False
- def reportStart(self, change):
+ def reportStart(self, item):
if not self.pipeline._disabled:
try:
- self.log.info("Reporting start, action %s change %s" %
- (self.pipeline.start_actions, change))
- msg = "Starting %s jobs." % self.pipeline.name
- if self.sched.config.has_option('zuul', 'status_url'):
- msg += "\n" + self.sched.config.get('zuul', 'status_url')
+ self.log.info("Reporting start, action %s item %s" %
+ (self.pipeline.start_actions, item))
ret = self.sendReport(self.pipeline.start_actions,
- self.pipeline.source, change, msg)
+ self.pipeline.source, item)
if ret:
- self.log.error("Reporting change start %s received: %s" %
- (change, ret))
+ self.log.error("Reporting item start %s received: %s" %
+ (item, ret))
except:
self.log.exception("Exception while reporting start:")
- def sendReport(self, action_reporters, source, change, message):
+ def sendReport(self, action_reporters, source, item,
+ message=None):
"""Sends the built message off to configured reporters.
- Takes the action_reporters, change, message and extra options and
+ Takes the action_reporters, item, message and extra options and
sends them to the pluggable reporters.
"""
report_errors = []
if len(action_reporters) > 0:
for reporter in action_reporters:
- ret = reporter.report(source, change, message)
+ ret = reporter.report(source, self.pipeline, item)
if ret:
report_errors.append(ret)
if len(report_errors) == 0:
@@ -1314,14 +1307,14 @@
self.log.debug("Adding change %s to queue %s" %
(change, change_queue))
- if not quiet:
- if len(self.pipeline.start_actions) > 0:
- self.reportStart(change)
item = change_queue.enqueueChange(change)
if enqueue_time:
item.enqueue_time = enqueue_time
item.live = live
self.reportStats(item)
+ if not quiet:
+ if len(self.pipeline.start_actions) > 0:
+ self.reportStart(item)
self.enqueueChangesBehind(change, quiet, ignore_requirements,
change_queue)
for trigger in self.sched.triggers.values():
@@ -1636,95 +1629,19 @@
self.pipeline._consecutive_failures >= self.pipeline.disable_at):
self.pipeline._disabled = True
if actions:
- report = self.formatReport(item)
try:
- self.log.info("Reporting change %s, actions: %s" %
- (item.change, actions))
- ret = self.sendReport(actions, self.pipeline.source,
- item.change, report)
+ self.log.info("Reporting item %s, actions: %s" %
+ (item, actions))
+ ret = self.sendReport(actions, self.pipeline.source, item)
if ret:
- self.log.error("Reporting change %s received: %s" %
- (item.change, ret))
+ self.log.error("Reporting item %s received: %s" %
+ (item, ret))
except:
self.log.exception("Exception while reporting:")
item.setReportedResult('ERROR')
self.updateBuildDescriptions(item.current_build_set)
return ret
- def formatReport(self, item):
- ret = ''
-
- if item.dequeued_needing_change:
- ret += 'This change depends on a change that failed to merge.\n'
- elif not self.pipeline.didMergerSucceed(item):
- ret += self.pipeline.merge_failure_message
- else:
- if self.pipeline.didAllJobsSucceed(item):
- ret += self.pipeline.success_message + '\n\n'
- else:
- ret += self.pipeline.failure_message + '\n\n'
- ret += self._formatReportJobs(item)
-
- if self.pipeline.footer_message:
- ret += '\n' + self.pipeline.footer_message
-
- return ret
-
- def _formatReportJobs(self, item):
- # Return the list of jobs portion of the report
- ret = ''
-
- if self.sched.config.has_option('zuul', 'url_pattern'):
- url_pattern = self.sched.config.get('zuul', 'url_pattern')
- else:
- url_pattern = None
-
- for job in self.pipeline.getJobs(item):
- 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 = 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)
- return ret
-
def formatDescription(self, build):
concurrent_changes = ''
concurrent_builds = ''