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