Roll repo creation hook into general hook script
[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_zepyhr
20     class: myclass [required]
21     instance: myinstance [optional]
22     zsig: myzsig [optional]
23   failed_build:
24    zephyr: *post_build_zephyr
25
26 == mail ==
27
28 To configure email notifications, add something like the following to your invirt config:
29
30 build:
31  hooks:
32   post_build:
33    mail: &post_build_mail
34     to: myemail@example.com [required]
35     from: myemail@example.com [required]
36     subject: My Subject [optional]
37   failed_build:
38    mail: *post_build_mail
39
40 post_build values will be used when this script is invoked as
41 post-build, while failed_build values will be used when it is invoked
42 as failed-build.
43 """
44
45 import optparse
46 import os
47 import re
48 import sys
49 import textwrap
50
51 from email.mime import text
52
53 from invirt import common, database, builder
54 from invirt.config import structs as config
55
56 def build_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
57     """Format a message reporting the results of a build"""
58     values = dict(values)
59     if not verbose and values['traceback'] is not None:
60         # TODO: better heuristic
61         values['traceback'] = textwrap.fill('\n'.join(values['traceback'].split('\n')[-2:]))
62
63     if succeeded:
64         values['result'] = success(values['result'])
65         msg = """Build of %(package)s %(version)s in %(pocket)s %(result)s.
66
67 Branch %(pocket)s has been advanced to %(short_commit)s.
68
69 (Build %(build_id)s was submitted by %(principal)s at %(inserted_at)s.)""" % values
70     else:
71         values['result'] = failure(values['result'])
72         msg = """Build of %(package)s %(version)s in %(pocket)s %(result)s while %(failed_stage)s.
73
74 %(traceback)s
75
76 (Build %(build_id)s was submitted by %(principal)s at %(inserted_at)s.)""" % values
77     return msg
78
79 def submit_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
80     values = dict(values)
81     if succeeded:
82         values['result'] = success(values['result'])
83     else:
84         values['result'] = failure(values['result'])
85     msg = """Submission of %(commit)s to be built in %(pocket)s %(result)s.
86 Build submitted by %(principal)s.""" % values
87     return msg
88
89 def repo_creation_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x):
90     values = dict(values)
91     assert succeeded
92     msg = '%(principal)s just created a new repository, %(category)s/%(name)s.git' % values
93     return msg
94
95 # Names of hooks
96 POST_BUILD = 'post-build'
97 FAILED_BUILD = 'failed-build'
98 POST_SUBMIT = 'post-submit'
99 FAILED_SUBMIT = 'failed-submit'
100 POST_ADD_REPO = 'post-add-repo'
101
102 # Types of communication
103
104 ZEPHYR = 'zephyr'
105 MAIL = 'mail'
106
107 message_generators = {
108     ZEPHYR : { POST_BUILD : build_completion_msg,
109                FAILED_BUILD : build_completion_msg,
110                POST_SUBMIT : submit_completion_msg,
111                FAILED_SUBMIT : submit_completion_msg,
112                POST_ADD_REPO : repo_creation_msg },
113     MAIL   : { POST_BUILD : build_completion_msg,
114                FAILED_BUILD : build_completion_msg,
115                POST_SUBMIT : submit_completion_msg,
116                FAILED_SUBMIT : submit_completion_msg,
117                POST_ADD_REPO : repo_creation_msg }
118     }
119
120 def zephyr_escape(m):
121     m = re.sub('@', '@@', m)
122     m = re.sub('}', '@(})', m)
123     return m
124
125 def zephyr_success(m):
126     return '}@{@color(green)%s}@{' % zephyr_escape(m)
127
128 def zephyr_failure(m):
129     return '}@{@color(red)%s}@{' % zephyr_escape(m)
130
131 def main():
132     parser = optparse.OptionParser('Usage: %prog [options] [arguments]')
133     opts, args = parser.parse_args()
134     prog = os.path.basename(sys.argv[0])
135
136     try:
137         if prog == POST_BUILD:
138             hook_config = config.build.hooks.post_build
139         elif prog == FAILED_BUILD:
140             hook_config = config.build.hooks.failed_build
141         elif prog == POST_SUBMIT:
142             hook_config = config.build.hooks.post_submit
143         elif prog == FAILED_SUBMIT:
144             hook_config = config.build.hooks.failed_submit
145         elif prog == POST_ADD_REPO:
146             hook_config = config.build.hooks.post_add_repo
147         else:
148             parser.error('hook script invoked with unrecognized name %s' % prog)
149             return 2
150     except common.InvirtConfigError:
151         print >>sys.stderr, 'No hook configuration found for %s.' % prog
152         return 1
153
154     if prog in [POST_BUILD, FAILED_BUILD]:
155         if len(args) != 1:
156             parser.set_usage('Usage: %prog [options] build_id')
157             parser.print_help()
158             return 1
159         database.connect()
160         build = database.Build.query().get(args[0])
161         short_commit = builder.canonicalize_commit(build.package, build.commit, shorten=True)
162         values = { 'build_id' : build.build_id,
163                    'commit' : build.commit,
164                    'failed_stage' : build.failed_stage,
165                    'inserted_at' : build.inserted_at,
166                    'package' : build.package,
167                    'pocket' : build.pocket,
168                    'principal' : build.principal,
169                    'short_commit' : short_commit,
170                    'traceback' : build.traceback,
171                    'version' : build.version,
172                    'default_instance' : 'build_%(build_id)s',
173                    'default_subject' : 'XVM build %(result)s: %(package)s %(version)s in %(pocket)s'}
174         if build.succeeded:
175             assert prog == POST_BUILD
176             values['result'] = 'succeeded'
177             succeeded = True
178         else:
179             assert prog == FAILED_BUILD
180             values['result'] = 'failed'
181             succeeded = False
182     elif prog in [POST_SUBMIT, FAILED_SUBMIT]:
183         if len(args) != 4:
184             parser.set_usage('Usage: %prog [options] pocket package commit principal')
185             parser.print_help()
186             return 2
187         values = { 'pocket' : args[0],
188                    'package' : args[1],
189                    'commit' : args[2],
190                    'principal' : args[3],
191                    'default_instance' : 'submission',
192                    'default_subject' : 'Submission %(result)s: %(package)s %(version)s in %(pocket)s'}
193         if prog == POST_SUBMIT:
194             values['result'] = 'succeeded'
195             succeeded = True
196         else:
197             values['result'] = 'failed'
198             succeeded = False
199     elif prog in [POST_ADD_REPO]:
200         if len(args) != 3:
201             parser.set_usage('Usage: %prog [options] category name principal')
202             parser.print_help()
203             return 3
204         values = { 'category' : args[0],
205                    'name' : args[1],
206                    'principal' : args[2],
207                    'default_instance' : 'new-repo',
208                    'default_subject' : 'New repository %(category)s/%(name)s'}
209         succeeded = True
210     else:
211         raise AssertionError('Impossible state')
212
213     try:
214         zephyr_config = hook_config.zephyr
215         klass = zephyr_config['class'] % values
216     except common.InvirtConfigError:
217         print >>sys.stderr, 'No zephyr configuration specified for %s.' % prog
218     else:
219         make_msg = message_generators[ZEPHYR][prog]
220         msg = '@{%s}' % make_msg(succeeded, values, verbose=False,
221                                  success=zephyr_success, failure=zephyr_failure)
222         instance = zephyr_config.get('instance', values['default_instance']) % values
223         zsig = zephyr_config.get('zsig', 'XVM Buildbot') % values
224         common.captureOutput(['zwrite', '-c', klass, '-i', instance, '-s',
225                               zsig, '-d', '-m', msg],
226                              stdout=None, stderr=None)
227
228     try:
229         mail_config = hook_config.mail
230         to = mail_config.to % values
231         sender = mail_config['from'] % values
232     except common.InvirtConfigError:
233         print >>sys.stderr, 'No email configuration specified for %s.' % prog
234     else:
235         make_msg = message_generators[MAIL][prog]
236         msg = make_msg(succeeded, values)
237         email = text.MIMEText(msg)
238         email['To'] = to % values
239         email['From'] = sender % values
240         email['Subject'] = mail_config.get('subject', values['default_subject']) % values
241         common.captureOutput(['sendmail', '-t'], email.as_string(),
242                              stdout=None, stderr=None)
243         
244 if __name__ == '__main__':
245     sys.exit(main())