Further generalize the implementation of post-build
[invirt/packages/invirt-dev.git] / build-hooks / post-build
1 #!/usr/bin/env python
2 """
3 A script for reporting the results of an invirtibuild.  Supports any
4 combination of the following reporting mechanisms.
5
6 Note that all strings are interpolated with a dictionary containing
7 the following keys:
8
9 build_id, commit, failed_stage, inserted_at, package,
10 pocket, principal, result, short_commit, traceback, version
11
12 == zephyr ==
13
14 To configure zephyr, add something like the following to your invirt config:
15
16 build:
17  hooks:
18   post_build:
19    zephyr: &post_build_zepyhr
20     class: myclass [required]
21     instance: myinstance [optional]
22     zsig: myzsig [optional]
23   failed_build:
24    zephyr: *post_build_zephyr
25
26 == mail ==
27
28 To configure email notifications, add something like the following to your invirt config:
29
30 build:
31  hooks:
32   post_build:
33    mail: &post_build_mail
34     to: myemail@example.com [required]
35     from: myemail@example.com [required]
36     subject: My Subject [optional]
37   failed_build:
38    mail: *post_build_mail
39
40 post_build values will be used when this script is invoked as
41 post-build, while failed_build values will be used when it is invoked
42 as failed-build.
43 """
44
45 import optparse
46 import os
47 import re
48 import sys
49 import textwrap
50
51 from email.mime import text
52
53 from invirt import common, database, builder
54 from invirt.config import structs as config
55
56 def build_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
57     """Format a message reporting the results of a build"""
58     values = dict(values)
59     if not verbose and values['traceback'] is not None:
60         # TODO: better heuristic
61         values['traceback'] = textwrap.fill('\n'.join(values['traceback'].split('\n')[-2:]))
62
63     if succeeded:
64         values['result'] = success(values['result'])
65         msg = """Build of %(package)s %(version)s in %(pocket)s %(result)s.
66
67 Branch %(pocket)s has been advanced to %(short_commit)s.
68
69 (Build %(build_id)s was submitted by %(principal)s at %(inserted_at)s.)""" % values
70     else:
71         values['result'] = failure(values['result'])
72         msg = """Build of %(package)s %(version)s in %(pocket)s %(result)s while %(failed_stage)s.
73
74 %(traceback)s
75
76 (Build %(build_id)s was submitted by %(principal)s at %(inserted_at)s.)""" % values
77     return msg
78
79 # Names of hooks
80 POST_BUILD = 'post-build'
81 FAILED_BUILD = 'failed-build'
82
83 # Types of communication
84
85 ZEPHYR = 'zephyr'
86 MAIL = 'mail'
87
88 message_generators = {
89     ZEPHYR : { POST_BUILD : build_completion_msg,
90                FAILED_BUILD : build_completion_msg },
91     MAIL   : { POST_BUILD : build_completion_msg,
92                FAILED_BUILD : build_completion_msg }
93     }
94
95 def zephyr_escape(m):
96     m = re.sub('@', '@@', m)
97     m = re.sub('}', '@(})', m)
98     return m
99
100 def zephyr_success(m):
101     return '}@{@color(green)%s}@{' % zephyr_escape(m)
102
103 def zephyr_failure(m):
104     return '}@{@color(red)%s}@{' % zephyr_escape(m)
105
106 def main():
107     parser = optparse.OptionParser('Usage: %prog [options] [arguments]')
108     opts, args = parser.parse_args()
109     prog = os.path.basename(sys.argv[0])
110
111     try:
112         if prog == POST_BUILD:
113             hook_config = config.build.hooks.post_build
114         elif prog == FAILED_BUILD:
115             hook_config = config.build.hooks.failed_build
116         else:
117             parser.error('hook script invoked with unrecognized name %s' % prog)
118             return 2
119     except common.InvirtConfigError:
120         print >>sys.stderr, 'No hook configuration found for %s.' % prog
121         return 1
122
123     if prog in [POST_BUILD, FAILED_BUILD]:
124         if len(args) != 1:
125             parser.set_usage('Usage: %prog [options] build_id')
126             parser.print_help()
127             return 1
128         database.connect()
129         build = database.Build.query().get(args[0])
130         short_commit = builder.canonicalize_commit(build.package, build.commit, shorten=True)
131         values = { 'build_id' : build.build_id,
132                    'commit' : build.commit,
133                    'failed_stage' : build.failed_stage,
134                    'inserted_at' : build.inserted_at,
135                    'package' : build.package,
136                    'pocket' : build.pocket,
137                    'principal' : build.principal,
138                    'short_commit' : short_commit,
139                    'traceback' : build.traceback,
140                    'version' : build.version,
141                    'default_instance' : 'build_%(build_id)s',
142                    'default_subject' : 'XVM build %(result)s: %(package)s %(version)s in %(pocket)s'}
143         if build.succeeded:
144             assert prog == POST_BUILD
145             values['result'] = 'succeeded'
146             succeeded = True
147         else:
148             assert prog == FAILED_BUILD
149             values['result'] = 'failed'
150             succeeded = False
151     else:
152         raise AssertionError('Impossible state')
153
154     try:
155         zephyr_config = hook_config.zephyr
156         klass = zephyr_config['class'] % values
157     except common.InvirtConfigError:
158         print >>sys.stderr, 'No zephyr configuration specified for %s.' % prog
159     else:
160         make_msg = message_generators[ZEPHYR][prog]
161         msg = '@{%s}' % make_msg(succeeded, values, verbose=False,
162                                  success=zephyr_success, failure=zephyr_failure)
163         instance = zephyr_config.get('instance', values['default_instance']) % values
164         zsig = zephyr_config.get('zsig', 'XVM Buildbot') % values
165         common.captureOutput(['zwrite', '-c', klass, '-i', instance, '-s',
166                               zsig, '-d', '-m', msg],
167                              stdout=None, stderr=None)
168
169     try:
170         mail_config = hook_config.mail
171         to = mail_config.to % values
172         sender = mail_config['from'] % values
173     except common.InvirtConfigError:
174         print >>sys.stderr, 'No email configuration specified for %s.' % prog
175     else:
176         make_msg = message_generators[MAIL][prog]
177         msg = make_msg(succeeded, values)
178         email = text.MIMEText(msg)
179         email['To'] = to % values
180         email['From'] = sender % values
181         email['Subject'] = mail_config.get('subject', values['default_subject']) % values
182         common.captureOutput(['sendmail', '-t'], email.as_string(),
183                              stdout=None, stderr=None)
184         
185 if __name__ == '__main__':
186     sys.exit(main())