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