Improve the heuristic for truncating the traceback
[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)s in %(pocket)s %(result)s while %(failed_stage)s.
88
89 %(traceback)s
90
91 (Build %(build_id)s was submitted by %(principal)s at %(inserted_at)s.)""" % values
92     return msg
93
94 def submit_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
95     values = dict(values)
96     if succeeded:
97         values['result'] = success(values['result'])
98     else:
99         values['result'] = failure(values['result'])
100     msg = """Submission of %(commit)s to be built in %(pocket)s %(result)s.
101 Build submitted by %(principal)s.""" % values
102     return msg
103
104 def repo_creation_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
105     values = dict(values)
106     assert succeeded
107     msg = '%(principal)s just created a new repository, %(category)s/%(name)s.git' % values
108     return msg
109
110 # Names of hooks
111 POST_BUILD = 'post-build'
112 FAILED_BUILD = 'failed-build'
113 POST_SUBMIT = 'post-submit'
114 FAILED_SUBMIT = 'failed-submit'
115 POST_ADD_REPO = 'post-add-repo'
116
117 # Types of communication
118
119 ZEPHYR = 'zephyr'
120 MAIL = 'mail'
121
122 message_generators = {
123     ZEPHYR : { POST_BUILD : build_completion_msg,
124                FAILED_BUILD : build_completion_msg,
125                POST_SUBMIT : submit_completion_msg,
126                FAILED_SUBMIT : submit_completion_msg,
127                POST_ADD_REPO : repo_creation_msg },
128     MAIL   : { POST_BUILD : build_completion_msg,
129                FAILED_BUILD : build_completion_msg,
130                POST_SUBMIT : submit_completion_msg,
131                FAILED_SUBMIT : submit_completion_msg,
132                POST_ADD_REPO : repo_creation_msg }
133     }
134
135 def zephyr_escape(m):
136     m = re.sub('@', '@@', m)
137     m = re.sub('}', '@(})', m)
138     return m
139
140 def zephyr_success(m):
141     return '}@{@color(green)%s}@{' % zephyr_escape(m)
142
143 def zephyr_failure(m):
144     return '}@{@color(red)%s}@{' % zephyr_escape(m)
145
146 def main():
147     parser = optparse.OptionParser('Usage: %prog [options] [arguments]')
148     opts, args = parser.parse_args()
149     prog = os.path.basename(sys.argv[0])
150
151     try:
152         if prog == POST_BUILD:
153             hook_config = config.build.hooks.post_build
154         elif prog == FAILED_BUILD:
155             hook_config = config.build.hooks.failed_build
156         elif prog == POST_SUBMIT:
157             hook_config = config.build.hooks.post_submit
158         elif prog == FAILED_SUBMIT:
159             hook_config = config.build.hooks.failed_submit
160         elif prog == POST_ADD_REPO:
161             hook_config = config.build.hooks.post_add_repo
162         else:
163             parser.error('hook script invoked with unrecognized name %s' % prog)
164             return 2
165     except common.InvirtConfigError:
166         print >>sys.stderr, 'No hook configuration found for %s.' % prog
167         return 1
168
169     if prog in [POST_BUILD, FAILED_BUILD]:
170         if len(args) != 1:
171             parser.set_usage('Usage: %prog [options] build_id')
172             parser.print_help()
173             return 1
174         database.connect()
175         build = database.Build.query().get(args[0])
176         short_commit = builder.canonicalize_commit(build.package, build.commit, shorten=True)
177         values = { 'build_id' : build.build_id,
178                    'commit' : build.commit,
179                    'failed_stage' : build.failed_stage,
180                    'inserted_at' : build.inserted_at,
181                    'package' : build.package,
182                    'pocket' : build.pocket,
183                    'principal' : build.principal,
184                    'short_commit' : short_commit,
185                    'traceback' : build.traceback,
186                    'version' : build.version,
187                    'default_instance' : 'build_%(build_id)s',
188                    'default_subject' : 'XVM build %(result)s: %(package)s %(version)s in %(pocket)s'}
189         if build.succeeded:
190             assert prog == POST_BUILD
191             values['result'] = 'succeeded'
192             succeeded = True
193         else:
194             assert prog == FAILED_BUILD
195             values['result'] = 'failed'
196             succeeded = False
197     elif prog in [POST_SUBMIT, FAILED_SUBMIT]:
198         if len(args) != 4:
199             parser.set_usage('Usage: %prog [options] pocket package commit principal')
200             parser.print_help()
201             return 2
202         values = { 'pocket' : args[0],
203                    'package' : args[1],
204                    'commit' : args[2],
205                    'principal' : args[3],
206                    'default_instance' : 'submission',
207                    'default_subject' : 'Submission %(result)s: %(package)s %(version)s in %(pocket)s'}
208         if prog == POST_SUBMIT:
209             values['result'] = 'succeeded'
210             succeeded = True
211         else:
212             values['result'] = 'failed'
213             succeeded = False
214     elif prog in [POST_ADD_REPO]:
215         if len(args) != 3:
216             parser.set_usage('Usage: %prog [options] category name principal')
217             parser.print_help()
218             return 3
219         values = { 'category' : args[0],
220                    'name' : args[1],
221                    'principal' : args[2],
222                    'default_instance' : 'new-repo',
223                    'default_subject' : 'New repository %(category)s/%(name)s'}
224         succeeded = True
225     else:
226         raise AssertionError('Impossible state')
227
228     try:
229         zephyr_config = hook_config.zephyr
230         klass = zephyr_config['class'] % values
231     except common.InvirtConfigError:
232         print >>sys.stderr, 'No zephyr configuration specified for %s.' % prog
233     else:
234         make_msg = message_generators[ZEPHYR][prog]
235         msg = '@{%s}' % make_msg(succeeded, values, verbose=False,
236                                  success=zephyr_success, failure=zephyr_failure)
237         instance = zephyr_config.get('instance', values['default_instance']) % values
238         zsig = zephyr_config.get('zsig', 'XVM Buildbot') % values
239         common.captureOutput(['zwrite', '-c', klass, '-i', instance, '-s',
240                               zsig, '-d', '-m', msg],
241                              stdout=None, stderr=None)
242
243     try:
244         mail_config = hook_config.mail
245         to = mail_config.to % values
246         sender = mail_config['from'] % values
247     except common.InvirtConfigError:
248         print >>sys.stderr, 'No email configuration specified for %s.' % prog
249     else:
250         make_msg = message_generators[MAIL][prog]
251         msg = make_msg(succeeded, values)
252         email = text.MIMEText(msg)
253         email['To'] = to % values
254         email['From'] = sender % values
255         email['Subject'] = mail_config.get('subject', values['default_subject']) % values
256         common.captureOutput(['sendmail', '-t'], email.as_string(),
257                              stdout=None, stderr=None)
258         
259 if __name__ == '__main__':
260     sys.exit(main())