Reformat build failure message
[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_zephyr
20     class: myclass [required]
21     instance: myinstance [optional]
22     zsig: myzsig [optional]
23   failed_build:
24    zephyr: *post_build_zephyr
25   ...
26
27 == mail ==
28
29 To configure email notifications, add something like the following to your invirt config:
30
31 build:
32  hooks:
33   post_build:
34    mail: &post_build_mail
35     to: myemail@example.com [required]
36     from: myemail@example.com [required]
37     subject: My Subject [optional]
38   failed_build:
39    mail: *post_build_mail
40   ...
41
42 The script chooses which configuration option to use based off the
43 name it is called with.  This name also determines which command-line
44 arguments the script takes, as well as how they are formatted.  When
45 called as:
46
47 post-build: uses post_build option
48 failed-build: uses failed_build option
49 post-submit: uses post_submit option
50 failed-submit: uses failed_submit option
51 post-add-repo: uses post_add_repo option
52 """
53
54 import optparse
55 import os
56 import re
57 import sys
58 import textwrap
59
60 from email.mime import text
61
62 from invirt import common, database, builder
63 from invirt.config import structs as config
64
65 def build_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
66     """Format a message reporting the results of a build"""
67     values = dict(values)
68     if not verbose and values['traceback'] is not None:
69         split = values['traceback'].split('\n')
70         # Here, have a hackish heuristic
71         truncated = '(empty)'
72         for i in xrange(2, len(split)):
73             truncated = textwrap.fill('\n'.join(split[-i:]))
74             if len(truncated) >= 10:
75                 break
76         values['traceback'] = truncated
77
78     if succeeded:
79         values['result'] = success(values['result'])
80         msg = """Build of %(package)s %(version)s in %(pocket)s %(result)s.
81
82 Branch %(pocket)s has been advanced to %(short_commit)s.
83
84 (Build %(build_id)s was submitted by %(principal)s at %(inserted_at)s.)""" % values
85     else:
86         values['result'] = failure(values['result'])
87         msg = """Build of %(package)s version %(version)s in %(pocket)s %(result)s while %(failed_stage)s.
88 Job submitted by %(principal)s at %(inserted_at)s.
89 Error: %(traceback)s""" % values
90     return msg
91
92 def submit_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
93     values = dict(values)
94     if succeeded:
95         values['result'] = success(values['result'])
96     else:
97         values['result'] = failure(values['result'])
98     msg = """Submission of %(commit)s to be built in %(pocket)s %(result)s.
99 Build submitted by %(principal)s.""" % values
100     return msg
101
102 def repo_creation_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
103     values = dict(values)
104     assert succeeded
105     msg = '%(principal)s just created a new repository, %(category)s/%(name)s.git' % values
106     return msg
107
108 # Names of hooks
109 POST_BUILD = 'post-build'
110 FAILED_BUILD = 'failed-build'
111 POST_SUBMIT = 'post-submit'
112 FAILED_SUBMIT = 'failed-submit'
113 POST_ADD_REPO = 'post-add-repo'
114
115 # Types of communication
116
117 ZEPHYR = 'zephyr'
118 MAIL = 'mail'
119
120 message_generators = {
121     ZEPHYR : { POST_BUILD : build_completion_msg,
122                FAILED_BUILD : build_completion_msg,
123                POST_SUBMIT : submit_completion_msg,
124                FAILED_SUBMIT : submit_completion_msg,
125                POST_ADD_REPO : repo_creation_msg },
126     MAIL   : { POST_BUILD : build_completion_msg,
127                FAILED_BUILD : build_completion_msg,
128                POST_SUBMIT : submit_completion_msg,
129                FAILED_SUBMIT : submit_completion_msg,
130                POST_ADD_REPO : repo_creation_msg }
131     }
132
133 def zephyr_escape(m):
134     m = re.sub('@', '@@', m)
135     m = re.sub('}', '@(})', m)
136     return m
137
138 def zephyr_success(m):
139     return '}@{@color(green)%s}@{' % zephyr_escape(m)
140
141 def zephyr_failure(m):
142     return '}@{@color(red)%s}@{' % zephyr_escape(m)
143
144 def main():
145     parser = optparse.OptionParser('Usage: %prog [options] [arguments]')
146     opts, args = parser.parse_args()
147     prog = os.path.basename(sys.argv[0])
148
149     try:
150         if prog == POST_BUILD:
151             hook_config = config.build.hooks.post_build
152         elif prog == FAILED_BUILD:
153             hook_config = config.build.hooks.failed_build
154         elif prog == POST_SUBMIT:
155             hook_config = config.build.hooks.post_submit
156         elif prog == FAILED_SUBMIT:
157             hook_config = config.build.hooks.failed_submit
158         elif prog == POST_ADD_REPO:
159             hook_config = config.build.hooks.post_add_repo
160         else:
161             parser.error('hook script invoked with unrecognized name %s' % prog)
162             return 2
163     except common.InvirtConfigError:
164         print >>sys.stderr, 'No hook configuration found for %s.' % prog
165         return 1
166
167     if prog in [POST_BUILD, FAILED_BUILD]:
168         if len(args) != 1:
169             parser.set_usage('Usage: %prog [options] build_id')
170             parser.print_help()
171             return 1
172         database.connect()
173         build = database.Build.query().get(args[0])
174         short_commit = builder.canonicalize_commit(build.package, build.commit, shorten=True)
175         values = { 'build_id' : build.build_id,
176                    'commit' : build.commit,
177                    'failed_stage' : build.failed_stage,
178                    'inserted_at' : build.inserted_at,
179                    'package' : build.package,
180                    'pocket' : build.pocket,
181                    'principal' : build.principal,
182                    'short_commit' : short_commit,
183                    'traceback' : build.traceback,
184                    'version' : build.version,
185                    'default_instance' : 'build_%(build_id)s',
186                    'default_subject' : 'XVM build %(result)s: %(package)s %(version)s in %(pocket)s'}
187         if build.succeeded:
188             assert prog == POST_BUILD
189             values['result'] = 'succeeded'
190             succeeded = True
191         else:
192             assert prog == FAILED_BUILD
193             values['result'] = 'failed'
194             succeeded = False
195     elif prog in [POST_SUBMIT, FAILED_SUBMIT]:
196         if len(args) != 4:
197             parser.set_usage('Usage: %prog [options] pocket package commit principal')
198             parser.print_help()
199             return 2
200         values = { 'pocket' : args[0],
201                    'package' : args[1],
202                    'commit' : args[2],
203                    'principal' : args[3],
204                    'default_instance' : 'submission',
205                    'default_subject' : 'Submission %(result)s: %(package)s %(version)s in %(pocket)s'}
206         if prog == POST_SUBMIT:
207             values['result'] = 'succeeded'
208             succeeded = True
209         else:
210             values['result'] = 'failed'
211             succeeded = False
212     elif prog in [POST_ADD_REPO]:
213         if len(args) != 3:
214             parser.set_usage('Usage: %prog [options] category name principal')
215             parser.print_help()
216             return 3
217         values = { 'category' : args[0],
218                    'name' : args[1],
219                    'principal' : args[2],
220                    'default_instance' : 'new-repo',
221                    'default_subject' : 'New repository %(category)s/%(name)s'}
222         succeeded = True
223     else:
224         raise AssertionError('Impossible state')
225
226     try:
227         zephyr_config = hook_config.zephyr
228         klass = zephyr_config['class'] % values
229     except common.InvirtConfigError:
230         print >>sys.stderr, 'No zephyr configuration specified for %s.' % prog
231     else:
232         make_msg = message_generators[ZEPHYR][prog]
233         msg = '@{%s}' % make_msg(succeeded, values, verbose=False,
234                                  success=zephyr_success, failure=zephyr_failure)
235         instance = zephyr_config.get('instance', values['default_instance']) % values
236         zsig = zephyr_config.get('zsig', 'XVM Buildbot') % values
237         common.captureOutput(['zwrite', '-c', klass, '-i', instance, '-s',
238                               zsig, '-d', '-m', msg],
239                              stdout=None, stderr=None)
240
241     try:
242         mail_config = hook_config.mail
243         to = mail_config.to % values
244         sender = mail_config['from'] % values
245     except common.InvirtConfigError:
246         print >>sys.stderr, 'No email configuration specified for %s.' % prog
247     else:
248         make_msg = message_generators[MAIL][prog]
249         msg = make_msg(succeeded, values)
250         email = text.MIMEText(msg)
251         email['To'] = to % values
252         email['From'] = sender % values
253         email['Subject'] = mail_config.get('subject', values['default_subject']) % values
254         common.captureOutput(['sendmail', '-t'], email.as_string(),
255                              stdout=None, stderr=None)
256         
257 if __name__ == '__main__':
258     sys.exit(main())