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