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