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