mgood@533: #-*- coding: utf-8 -*- mgood@533: # mgood@533: # Copyright (C) 2007 Ole Trenner, mgood@533: # All rights reserved. mgood@533: # mgood@533: # This software is licensed as described in the file COPYING, which mgood@533: # you should have received as part of this distribution. mgood@533: osimons@824: from genshi.template.text import NewTextTemplate mgood@625: from trac.core import Component, implements osimons@824: from trac.web.chrome import ITemplateProvider, Chrome mgood@533: from trac.config import BoolOption mgood@533: from trac.notification import NotifyEmail mgood@533: from bitten.api import IBuildListener osimons@875: from bitten.model import Build, BuildStep, BuildLog, TargetPlatform mgood@533: mgood@533: mgood@533: class BittenNotify(Component): mgood@534: """Sends notifications on build status by mail.""" mgood@534: implements(IBuildListener, ITemplateProvider) mgood@534: mgood@625: notify_on_failure = BoolOption('notification', mgood@625: 'notify_on_failed_build', 'true', mgood@533: """Notify if bitten build fails.""") mgood@533: mgood@625: notify_on_success = BoolOption('notification', mgood@625: 'notify_on_successful_build', 'false', mgood@533: """Notify if bitten build succeeds.""") mgood@533: mgood@533: def __init__(self): mgood@533: self.log.debug('Initializing BittenNotify plugin') mgood@533: mgood@541: def notify(self, build=None): mgood@626: self.log.info('BittenNotify invoked for build %r', build) mgood@626: self.log.debug('build status: %s', build.status) mgood@541: if not self._should_notify(build): mgood@541: return mgood@626: self.log.info('Sending notification for build %r', build) mgood@541: try: mgood@626: email = BuildNotifyEmail(self.env) mgood@626: email.notify(build) mgood@541: except Exception, e: mgood@541: self.log.exception("Failure sending notification for build " mgood@541: "%s: %s", build.id, e) mgood@533: mgood@533: def _should_notify(self, build): mgood@541: if build.status == Build.FAILURE: mgood@541: return self.notify_on_failure mgood@541: elif build.status == Build.SUCCESS: mgood@541: return self.notify_on_success mgood@541: else: mgood@541: return False mgood@533: mgood@533: # IBuildListener methods mgood@533: mgood@533: def build_started(self, build): mgood@533: """build started""" mgood@533: self.notify(build) mgood@533: mgood@533: def build_aborted(self, build): mgood@533: """build aborted""" mgood@533: self.notify(build) mgood@533: mgood@533: def build_completed(self, build): mgood@533: """build completed""" mgood@533: self.notify(build) mgood@533: mgood@533: # ITemplateProvider methods mgood@533: mgood@533: def get_templates_dirs(self): mgood@533: """Return a list of directories containing the provided template mgood@533: files.""" mgood@533: from pkg_resources import resource_filename mgood@533: return [resource_filename(__name__, 'templates')] mgood@533: mgood@533: def get_htdocs_dirs(self): mgood@533: """Return the absolute path of a directory containing additional mgood@533: static resources (such as images, style sheets, etc).""" mgood@533: return [] mgood@533: mgood@533: mgood@626: class BuildNotifyEmail(NotifyEmail): mgood@533: """Notification of failed builds.""" mgood@533: mgood@626: readable_states = { mgood@626: Build.SUCCESS: 'Successful', mgood@626: Build.FAILURE: 'Failed', mgood@626: } mgood@533: template_name = 'bitten_notify_email.txt' mgood@533: from_email = 'bitten@localhost' mgood@533: mgood@533: def __init__(self, env): mgood@533: NotifyEmail.__init__(self, env) osimons@824: # Override the template type to always use NewTextTemplate osimons@824: if not isinstance(self.template, NewTextTemplate): osimons@824: self.template = Chrome(env).templates.load( osimons@824: self.template.filepath, cls=NewTextTemplate) mgood@533: mgood@626: def notify(self, build): mgood@626: self.build = build mgood@626: self.data.update(self.template_data()) mgood@626: subject = '[%s Build] %s [%s] %s' % (self.readable_states[build.status], mgood@626: self.env.project_name, mgood@626: self.build.rev, mgood@626: self.build.config) mgood@626: NotifyEmail.notify(self, self.build.id, subject) mgood@533: mgood@533: def get_recipients(self, resid): mgood@626: to = [self.get_author()] mgood@624: cc = [] mgood@624: return (to, cc) mgood@533: mgood@626: def send(self, torcpts, ccrcpts): mgood@537: mime_headers = { mgood@626: 'X-Trac-Build-ID': str(self.build.id), mgood@626: 'X-Trac-Build-URL': self.build_link(), mgood@537: } mgood@533: NotifyEmail.send(self, torcpts, ccrcpts, mime_headers) mgood@626: mgood@626: def build_link(self): mgood@626: return self.env.abs_href.build(self.build.config, self.build.id) mgood@626: mgood@626: def template_data(self): mgood@626: failed_steps = BuildStep.select(self.env, build=self.build.id, mgood@626: status=BuildStep.FAILURE) osimons@875: platform = TargetPlatform.fetch(self.env, id=self.build.platform) mgood@626: change = self.get_changeset() mgood@626: return { mgood@626: 'build': { mgood@626: 'id': self.build.id, mgood@626: 'status': self.readable_states[self.build.status], mgood@626: 'link': self.build_link(), mgood@626: 'config': self.build.config, osimons@875: 'platform': getattr(platform, 'name', 'unknown'), mgood@626: 'slave': self.build.slave, mgood@626: 'failed_steps': [{ mgood@626: 'name': step.name, mgood@626: 'description': step.description, mgood@626: 'errors': step.errors, mgood@626: 'log_messages': self.get_all_log_messages_for_step(step), mgood@626: } for step in failed_steps], mgood@626: }, mgood@626: 'change': { mgood@626: 'rev': change.rev, mgood@626: 'link': self.env.abs_href.changeset(change.rev), mgood@626: 'author': change.author, mgood@626: }, mgood@626: } mgood@626: mgood@626: def get_all_log_messages_for_step(self, step): mgood@626: messages = [] mgood@626: for log in BuildLog.select(self.env, build=self.build.id, mgood@626: step=step.name): mgood@626: messages.extend(log.messages) mgood@626: return messages mgood@626: mgood@626: def get_changeset(self): osimons@804: repos = self.env.get_repository() osimons@804: assert repos, 'No "(default)" Repository: Add a repository or alias ' \ osimons@804: 'named "(default)" to Trac.' osimons@804: return repos.get_changeset(self.build.rev) mgood@626: mgood@626: def get_author(self): mgood@626: return self.get_changeset().author