Index: gatekeeper_mailer.py |
diff --git a/gatekeeper_mailer.py b/gatekeeper_mailer.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a1e179125cb278bec2d6405dd095aa303024f6e4 |
--- /dev/null |
+++ b/gatekeeper_mailer.py |
@@ -0,0 +1,282 @@ |
+#!/usr/bin/env python |
Vadim Sh.
2013/08/26 21:30:41
I'm assuming this is being converted to jinja2 now
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
|
+# Copyright (c) 2013 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Provides mailer support for gatekeeper_ng. |
+ |
+This module containes mail templates to notify tree watchers |
+when the tree is closed. |
+""" |
+ |
+import cStringIO |
+import urllib |
+ |
+# From buildbot's results.py. |
+SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY = range(6) |
+Results = ["success", "warnings", "failure", "skipped", "exception", "retry"] |
Vadim Sh.
2013/08/26 21:30:41
Why camelcase here instead all CAPS?
|
+ |
+ |
+def _GenBuildBox(buildstatus, waterfall_url, styles): |
Vadim Sh.
2013/08/26 21:30:41
Also why camel case in functions instead of PEP8?.
|
+ """Generates a box for one build.""" |
+ class_ = Results[buildstatus['result']] |
+ style = '' |
+ if class_ and class_ in styles: |
+ style = styles[class_] |
+ reason = buildstatus['reason'] |
+ url = '%sbuilders/%s/builds/%d' % ( |
+ waterfall_url, |
+ urllib.quote(buildstatus['builderName'], safe=''), |
+ buildstatus['number']) |
+ fmt = ('<tr><td style="%s"><a title="Reason: %s" href="%s">' |
+ 'Build %d' |
+ '</a></td></tr>') |
+ return fmt % (style, reason, url, buildstatus['number']) |
+ |
+ |
+# escape taken from twisted/web/microdom.py |
+# order is important |
+HTML_ESCAPE_CHARS = (('&', '&'), # don't add any entities before this one |
+ ('<', '<'), |
+ ('>', '>'), |
+ ('"', '"')) |
+ |
+ |
+def escape(text, chars=HTML_ESCAPE_CHARS): |
+ "Escape a few XML special chars with XML entities." |
+ for s, h in chars: |
+ text = text.replace(s, h) |
+ return text |
+ |
+ |
+def _GenStepBox(stepstatus, styles): |
+ """Generates a box for one step.""" |
+ class_ = Results[stepstatus['results']] |
+ # class == running or results |
+ style = '' |
+ if class_ and class_ in styles: |
+ style = styles[class_] |
+ text = stepstatus['text'] or [] |
+ text = text[:] |
+ for steplog in stepstatus['logs']: |
+ logname = steplog[0] |
+ url = steplog[1] |
+ text.append('<a href="%s">%s</a>' % (url, escape(logname))) |
+ for urlname, target in stepstatus['urls'].iteritems(): |
+ text.append('<a href="%s">%s</a>' % (target, escape(urlname))) |
+ fmt = '<tr><td style="%s">%s</td></tr>' |
+ return fmt % (style, '<br/>'.join(text)) |
+ |
+ |
+class MailTemplate(object): |
+ """Encapsulates a buildbot status email.""" |
+ |
+ # Extracted from |
+ # http://src.chromium.org/svn/trunk/tools/buildbot/master.chromium/ |
+ # public_html/buildbot.css |
+ DEFAULT_STYLES = { |
+ 'BuildStep': '', |
+ 'start': ('color: #666666; background-color: #fffc6c;' |
+ 'border-color: #C5C56D;'), |
+ 'success': ('color: #FFFFFF; background-color: #8fdf5f; ' |
+ 'border-color: #4F8530;'), |
+ 'failure': ('color: #FFFFFF; background-color: #e98080; ' |
+ 'border-color: #A77272;'), |
+ 'warnings': ('color: #FFFFFF; background-color: #ffc343; ' |
+ 'border-color: #C29D46;'), |
+ 'exception': ('color: #FFFFFF; background-color: #e0b0ff; ' |
+ 'border-color: #ACA0B3;'), |
+ 'offline': ('color: #FFFFFF; background-color: #e0b0ff; ' |
+ 'border-color: #ACA0B3;'), |
+ } |
+ |
+ # Generate a HTML table looking like the waterfall. |
+ # WARNING: Gmail ignores embedded CSS style. I don't know how to fix that so |
+ # meanwhile, I just won't embedded the CSS style. |
+ html_template = ( |
+"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
+<html xmlns="http://www.w3.org/1999/xhtml"> |
+<head> |
+ <title>%s</title> |
+</head> |
+<body> |
+ <a href="%s">%s</a><p> |
+ %s<p> |
+ <a href="%s">%s</a><p> |
+ Revision: %s<br> |
+""" |
+) |
+ |
+ # Simpler text content for non-html aware clients. |
+ text_template = ( |
+"""%s |
+ |
+%s |
+ |
+%swaterfall?builder=%s |
+ |
+--=> %s <=-- |
+ |
+Revision: %s |
+Blame list: %s |
+ |
+Buildbot waterfall: http://build.chromium.org/ |
+""" |
+) |
+ |
+ change_tmpl = ( |
+""" |
+<p>Changed by: <b>%(who)s</b><br /> |
+Changed at: <b>%(at)s</b><br /> |
+%(repository)s |
+%(branch)s |
+%(revision)s |
+<br /> |
+ |
+Changed files: |
+%(files)s |
+ |
+Comments: |
+%(comments)s |
+ |
+Properties: |
+%(properties)s |
+</p> |
+""" |
+) |
+ |
+ status_header = 'Automatically closing tree for "%(steps)s" on "%(builder)s"' |
+ |
+ def __init__(self, waterfall_url, build_url, |
+ project_name, |
+ fromaddr, |
+ reply_to=None, |
+ subject='buildbot %(result)s in %(projectName)s on %(builder)s, ' |
+ 'revision %(revision)s'): |
+ |
+ self.reply_to = reply_to |
+ self.fromaddr = fromaddr |
+ self.subject = subject |
+ self.waterfall_url = waterfall_url |
+ self.build_url = build_url |
+ self.project_name = project_name |
+ |
+ @staticmethod |
+ def UL(lst): |
+ # from third_party/twisted_10_2/twisted/web/html.py |
+ io = cStringIO.StringIO() |
+ io.write("<ul>\n") |
+ for el in lst: |
+ io.write("<li> %s</li>\n" % el) |
+ io.write("</ul>") |
+ return io.getvalue() |
+ |
+ def changeToHTML(self, change): |
+ # Ported from third_party/buildbot_8_4p1/buildbot/changes.py. |
+ |
+ links = [] |
+ for f in change['files']: |
+ if f['url'] is not None: |
+ # could get confused |
+ links.append('<a href="%s"><b>%s</b></a>' % ( |
+ f['url'], f['name'])) |
+ else: |
+ links.append('<b>%s</b>' % f['name']) |
+ |
+ revision = '' |
+ if change['revision']: |
+ if change.get('revlink'): |
+ revision = 'Revision: <a href="%s"><b>%s</b></a>\n' % ( |
+ change['revlink'], change['revision']) |
+ else: |
+ revision = "Revision: <b>%s</b><br />\n" % change['revision'] |
+ |
+ repository = '' |
+ if change['repository']: |
+ repository = "Repository: <b>%s</b><br />\n" % change['repository'] |
+ |
+ branch = '' |
+ if change['branch']: |
+ branch = "Branch: <b>%s</b><br />\n" % change['branch'] |
+ |
+ properties = [] |
+ for prop in change['properties']: |
+ properties.append("%s: %s<br />" % (prop[0], prop[1])) |
+ |
+ kwargs = { 'who' : escape(change['who']), |
+ 'at' : change['at'], |
+ 'files' : self.UL(links) + '\n', |
+ 'repository': repository, |
+ 'revision' : revision, |
+ 'branch' : branch, |
+ 'comments' : '<pre>' + escape(change['comments']) + '</pre>', |
+ 'properties': self.UL(properties) + '\n' } |
+ return self.change_tmpl % kwargs |
+ |
+ def genMessageContent(self, build_status): |
+ builder_name = build_status['builderName'] |
+ us_steps = ','.join(build_status['unsatisfied']) |
+ revisions_list = build_status['revisions'] |
+ status_text = self.status_header % { |
+ 'builder': builder_name, |
+ 'steps': us_steps, |
+ } |
+ # Use the first line as a title. |
+ status_title = status_text.split('\n', 1)[0] |
+ blame_list = ','.join(build_status['blamelist']) |
+ revisions_string = '' |
+ latest_revision = 0 |
+ if revisions_list: |
+ revisions_string = ', '.join([str(rev) for rev in revisions_list]) |
+ latest_revision = max([rev for rev in revisions_list]) |
+ if build_status['result'] == FAILURE: |
+ result = 'failure' |
+ else: |
+ result = 'warning' |
+ |
+ html_content = self.html_template % (status_title, self.waterfall_url, |
+ self.waterfall_url, |
+ status_text.replace('\n', "<br>\n"), self.build_url, |
+ self.build_url, revisions_string) |
+ |
+ # Only include the blame list if relevant. |
+ html_content += " Blame list: %s<p>\n" % blame_list |
+ |
+ build_boxes = [_GenBuildBox(build_status, self.waterfall_url, |
+ self.DEFAULT_STYLES)] |
+ build_boxes.extend([_GenStepBox(step, |
+ self.DEFAULT_STYLES) |
+ for step in build_status['steps'] |
+ if step['started'] and step['text']]) |
+ table_content = ''.join(build_boxes) |
+ html_content += ( |
+ ('<table style="border-spacing: 1px 1px; font-weight: bold;' |
+ ' padding: 3px 0px 3px 0px; text-align: center;">\n') + |
+ table_content + |
+ '</table>\n') |
+ |
+ html_content += "<p>" |
+ # Add the change list descriptions. getChanges() returns a tuple of |
+ # buildbot.changes.changes.Change |
+ for change in build_status['changes']: |
+ html_content += self.changeToHTML(change) |
+ html_content += "</body>\n</html>" |
+ |
+ text_content = self.text_template % (status_title, |
+ self.build_url, |
+ urllib.quote(self.waterfall_url, '/:'), |
+ urllib.quote(builder_name), |
+ status_text, |
+ revisions_string, |
+ blame_list) |
+ |
+ subject = self.subject % { |
+ 'result': result, |
+ 'projectName': self.project_name, |
+ 'builder': builder_name, |
+ 'reason': build_status['reason'], |
+ 'revision': str(latest_revision), |
+ 'buildnumber': str(build_status['number']), |
+ } |
+ |
+ return text_content, html_content, subject |