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