The buildbot can also send email when builds finish. The most common use of this is to tell developers when their change has caused the build to fail. It is also quite common to send a message to a mailing list (usually named “builds” or similar) about every build.
The MailNotifier
status target is used to accomplish this. You
configure it by specifying who mail should be sent to, under what
circumstances mail should be sent, and how to deliver the mail. It can
be configured to only send out mail for certain builders, and only
send messages when the build fails, or when the builder transitions
from success to failure. It can also be configured to include various
build logs in each message.
By default, the message will be sent to the Interested Users list (see Doing Things With Users), which includes all developers who made changes in the
build. You can add additional recipients with the extraRecipients argument.
You can also add interested users by setting the owners
build property
to a list of users in the scheduler constructor (see Configuring Schedulers).
Each MailNotifier sends mail to a single set of recipients. To send different kinds of mail to different recipients, use multiple MailNotifiers.
The following simple example will send an email upon the completion of each build, to just those developers whose Changes were included in the build. The email contains a description of the Build, its results, and URLs where more information can be obtained.
from buildbot.status.mail import MailNotifier mn = MailNotifier(fromaddr="buildbot@example.org", lookup="example.org") c['status'].append(mn)
To get a simple one-message-per-build (say, for a mailing list), use
the following form instead. This form does not send mail to individual
developers (and thus does not need the lookup=
argument,
explained below), instead it only ever sends mail to the “extra
recipients” named in the arguments:
mn = MailNotifier(fromaddr="buildbot@example.org", sendToInterestedUsers=False, extraRecipients=['listaddr@example.org'])
If your SMTP host requires authentication before it allows you to send emails, this can also be done by specifying “smtpUser” and “smptPassword”:
mn = MailNotifier(fromaddr="myuser@gmail.com", sendToInterestedUsers=False, extraRecipients=["listaddr@example.org"], relayhost="smtp.gmail.com", smtpPort=587, smtpUser="myuser@gmail.com", smtpPassword="mypassword")
If you want to require Transport Layer Security (TLS), then you can also set “useTls”:
mn = MailNotifier(fromaddr="myuser@gmail.com", sendToInterestedUsers=False, extraRecipients=["listaddr@example.org"], useTls=True, relayhost="smtp.gmail.com", smtpPort=587, smtpUser="myuser@gmail.com", smtpPassword="mypassword")
Note that if you see twisted.mail.smtp.TLSRequiredError
exceptions in
the log while using TLS, this can be due either to the server not
supporting TLS or to a missing pyopenssl package on the buildmaster system.
In some cases it is desirable to have different information then what is
provided in a standard MailNotifier message. For this purpose MailNotifier
provides the argument messageFormatter
(a function) which allows for the
creation of messages with unique content.
For example, if only short emails are desired (e.g., for delivery to phones)
from buildbot.status.builder import Results def messageFormatter(mode, name, build, results, master_status): result = Results[results] text = list() text.append("STATUS: %s" % result.title()) return { 'body' : "\n".join(text), 'type' : 'plain' } mn = MailNotifier(fromaddr="buildbot@example.org", sendToInterestedUsers=False, mode='problem', extraRecipients=['listaddr@example.org'], messageFormatter=messageFormatter)
Another example of a function delivering a customized html email containing the last 80 log lines of the last build step is given below:
from buildbot.status.builder import Results def html_message_formatter(mode, name, build, results, master_status): """Provide a customized message to BuildBot's MailNotifier. The last 80 lines of the log are provided as well as the changes relevant to the build. Message content is formatted as html. """ result = Results[results] limit_lines = 80 text = list() text.append(u'<h4>Build status: %s</h4>' % result.upper()) text.append(u'<table cellspacing="10"><tr>') text.append(u"<td>Buildslave for this Build:</td><td><b>%s</b></td></tr>" % build.getSlavename()) if master_status.getURLForThing(build): text.append(u'<tr><td>Complete logs for all build steps:</td><td><a href="%s">%s</a></td></tr>' % (master_status.getURLForThing(build), master_status.getURLForThing(build)) ) text.append(u'<tr><td>Build Reason:</td><td>%s</td></tr>' % build.getReason()) source = u"" ss = build.getSourceStamp() if ss.branch: source += u"[branch %s] " % ss.branch if ss.revision: source += ss.revision else: source += u"HEAD" if ss.patch: source += u" (plus patch)" text.append(u"<tr><td>Build Source Stamp:</td><td><b>%s</b></td></tr>" % source) text.append(u"<tr><td>Blamelist:</td><td>%s</td></tr>" % ",".join(build.getResponsibleUsers())) text.append(u'</table>') if ss.changes: text.append(u'<h4>Recent Changes:</h4>') for c in ss.changes: cd = c.asDict() when = datetime.datetime.fromtimestamp(cd['when'] ).ctime() text.append(u'<table cellspacing="10">') text.append(u'<tr><td>Repository:</td><td>%s</td></tr>' % cd['repository'] ) text.append(u'<tr><td>Project:</td><td>%s</td></tr>' % cd['project'] ) text.append(u'<tr><td>Time:</td><td>%s</td></tr>' % when) text.append(u'<tr><td>Changed by:</td><td>%s</td></tr>' % cd['who'] ) text.append(u'<tr><td>Comments:</td><td>%s</td></tr>' % cd['comments'] ) text.append(u'</table>') files = cd['files'] if files: text.append(u'<table cellspacing="10"><tr><th align="left">Files</th><th>URL</th></tr>') for file in files: text.append(u'<tr><td>%s:</td><td>%s</td></tr>' % (file['name'], file['url'])) text.append(u'</table>') text.append(u'<br>') # get log for last step logs = build.getLogs() # logs within a step are in reverse order. Search back until we find stdio for log in reversed(logs): if log.getName() == 'stdio': break name = "%s.%s" % (log.getStep().getName(), log.getName()) status, dummy = log.getStep().getResults() content = log.getText().splitlines() # Note: can be VERY LARGE url = u'%s/steps/%s/logs/%s' % (master_status.getURLForThing(build), log.getStep().getName(), log.getName()) text.append(u'<i>Detailed log of last build step:</i> <a href="%s">%s</a>' % (url, url)) text.append(u'<br>') text.append(u'<h4>Last %d lines of "%s"</h4>' % (limit_lines, name)) unilist = list() for line in content[-limit_lines:]: unilist.append(cgi.escape(unicode(line,'utf-8'))) text.append(u'<pre>'.join([uniline for uniline in unilist])) text.append(u'</pre>') text.append(u'<br><br>') text.append(u'<b>-The BuildBot</b>') return { 'body': u"\n".join(text), 'type': 'html' } mn = MailNotifier(fromaddr="buildbot@example.org", sendToInterestedUsers=False, mode='failing', extraRecipients=['listaddr@example.org'], messageFormatter=message_formatter)
fromaddr
sendToInterestedUsers
extraRecipients
subject
%(builder)s
will be replaced with the name of the builder which
provoked the message.
mode
all
change
failing
warning
passing
problem
builders
categories
addLogs
addPatch
buildSetSummary
relayhost
smtpPort
useTls
True
(default is False
)
MailNotifier
sends emails using TLS and authenticates with the
relayhost
. When using TLS the arguments smtpUser
and
smtpPassword
must also be specified.
smtpUser
relayhost
.
smtpPassword
relayhost
.
lookup
IEmailLookup
). Object which provides
IEmailLookup, which is responsible for mapping User names (which come
from the VC system) into valid email addresses. If not provided, the
notifier will only be able to send mail to the addresses in the
extraRecipients list. Most of the time you can use a simple Domain
instance. As a shortcut, you can pass as string: this will be treated
as if you had provided Domain(str). For example,
lookup='twistedmatrix.com' will allow mail to be sent to all
developers whose SVN usernames match their twistedmatrix.com account
names. See buildbot/status/mail.py for more details.
messageFormatter
messageFormatter
function takes the mail mode (mode
), builder
name (name
), the build status (build
), the result code
(results
), and the BuildMaster status (master_status
). It
returns a dictionary. The body
key gives a string that is the complete
text of the message. The type
key is the message type ('plain' or
'html'). The 'html' type should be used when generating an HTML message. The
subject
key is optional, but gives the subject for the email.
extraHeaders
As a help to those writing messageFormatter
functions, the following
table describes how to get some useful pieces of information from the various
status objects:
name
master_status.getTitle()
mode
(one of all, failing, problem, change, passing
)
from buildbot.status.builder import Results result_str = Results[results] # one of 'success', 'warnings', 'failure', 'skipped', or 'exception'
master_status.getURLForThing(build)
master_status.getBuildbotURL()
build.getText()
build.getProperties()
(a Properties
instance)
build.getSlavename()
build.getReason()
build.getResponsibleUsers()
ss = build.getSourceStamp() if ss: branch = ss.branch revision = ss.revision patch = ss.patch changes = ss.changes # list
A change object has the following useful information:
who
revision
branch
when
files
comments
Change
methods asText and asDict can be used to format the
information above. asText returns a list of strings and asDict returns
a dictonary suitable for html/mail rendering.
logs = list() for log in build.getLogs(): log_name = "%s.%s" % (log.getStep().getName(), log.getName()) log_status, dummy = log.getStep().getResults() log_body = log.getText().splitlines() # Note: can be VERY LARGE log_url = '%s/steps/%s/logs/%s' % (master_status.getURLForThing(build), log.getStep().getName(), log.getName()) logs.append((log_name, log_url, log_body, log_status))