3 from twisted
.python
import log
4 from twisted
.internet
import reactor
5 from twisted
.web
import client
, error
7 from buildbot
import status
12 class WebHookTransmitter(status
.base
.StatusReceiverMultiService
):
14 A webhook status listener for buildbot.
16 WebHookTransmitter listens for build events and sends the events
17 as http POSTs to one or more webhook URLs.
19 The easiest way to deploy this is to place it next to your
20 master.cfg and do something like this (assuming you've got a
21 postbin URL for purposes of demonstration):
23 from webhook_status import WebHookTransmitter
24 c['status'].append(WebHookTransmitter('http://www.postbin.org/xxxxxxx'))
26 Alternatively, you may provide a list of URLs and each one will
27 receive information on every event.
29 The following optional parameters influence when and what data is
32 categories: If provided, only events belonging to one of the
33 categories listed will be transmitted.
35 extra_params: Additional parameters to be supplied with every request.
37 max_attempts: The maximum number of times to retry transmission
38 on failure. Default: 10
40 retry_multiplier: A value multiplied by the retry number to wait before
41 attempting a retry. Default 5
44 agent
= 'buildbot webhook'
46 def __init__(self
, url
, categories
=None, extra_params
={},
47 max_attempts
=MAX_ATTEMPTS
, retry_multiplier
=RETRY_MULTIPLIER
):
48 status
.base
.StatusReceiverMultiService
.__init
__(self
)
49 if isinstance(url
, basestring
):
53 self
.categories
= categories
54 self
.extra_params
= extra_params
55 self
.max_attempts
= max_attempts
56 self
.retry_multiplier
= retry_multiplier
58 def _transmit(self
, event
, params
={}):
60 cat
= dict(params
).get('category', None)
61 if (cat
and self
.categories
) and cat
not in self
.categories
:
62 log
.msg("Ignoring request for unhandled category: %s" % cat
)
65 new_params
= [('event', event
)]
66 new_params
.extend(list(self
.extra_params
.items()))
67 if hasattr(params
, "items"):
68 new_params
.extend(params
.items())
70 new_params
.extend(params
)
71 encoded_params
= urllib
.urlencode(new_params
)
73 log
.msg("WebHookTransmitter announcing a %s event" % event
)
75 self
._retrying
_fetch
(u
, encoded_params
, event
, 0)
77 def _retrying_fetch(self
, u
, data
, event
, attempt
):
78 d
= client
.getPage(u
, method
='POST', agent
=self
.agent
,
79 postdata
=data
, followRedirect
=0)
83 if attempt
< self
.max_attempts
:
84 reactor
.callLater(attempt
* self
.retry_multiplier
,
85 self
._retrying
_fetch
, u
, data
, event
, attempt
+ 1)
89 def _trap_status(x
, *acceptable
):
91 if int(x
.value
.status
) in acceptable
:
92 log
.msg("Terminating retries of event %s with a %s response"
93 % (event
, x
.value
.status
))
98 # Any sort of redirect is considered success
99 d
.addErrback(lambda x
: x
.trap(error
.PageRedirect
))
101 # Any of these status values are considered delivered, or at
102 # least not something that should be retried.
103 d
.addErrback(_trap_status
,
104 # These are all actually successes
106 # These tell me I'm sending stuff it doesn't want.
107 400, 401, 403, 405, 406, 407, 410, 413, 414, 415,
108 # This tells me the server can't deal with what I sent
111 d
.addCallback(lambda x
: log
.msg("Completed %s event hook on attempt %d" %
113 d
.addErrback(_maybe_retry
)
114 d
.addErrback(lambda e
: log
.err("Giving up delivering %s to %s" % (event
, u
)))
116 def builderAdded(self
, builderName
, builder
):
117 builder
.subscribe(self
)
118 self
._transmit
('builderAdded',
119 {'builder': builderName
,
120 'category': builder
.getCategory()})
122 def builderRemoved(self
, builderName
, builder
):
123 self
._transmit
('builderRemoved',
124 {'builder': builderName
,
125 'category': builder
.getCategory()})
127 def buildStarted(self
, builderName
, build
):
128 build
.subscribe(self
)
130 args
= {'builder': builderName
,
131 'category': build
.getBuilder().getCategory(),
132 'reason': build
.getReason(),
133 'revision': build
.getSourceStamp().revision
,
134 'buildNumber': build
.getNumber()}
136 if build
.getSourceStamp().patch
:
137 args
['patch'] = build
.getSourceStamp().patch
[1]
139 self
._transmit
('buildStarted', args
)
141 def buildFinished(self
, builderName
, build
, results
):
142 self
._transmit
('buildFinished',
143 {'builder': builderName
,
144 'category': build
.getBuilder().getCategory(),
145 'result': status
.builder
.Results
[results
],
146 'revision': build
.getSourceStamp().revision
,
147 'had_patch': bool(build
.getSourceStamp().patch
),
148 'buildNumber': build
.getNumber()})
150 def stepStarted(self
, build
, step
):
152 self
._transmit
('stepStarted',
153 [('builder', build
.getBuilder().getName()),
154 ('category', build
.getBuilder().getCategory()),
155 ('buildNumber', build
.getNumber()),
156 ('step', step
.getName())])
158 def stepFinished(self
, build
, step
, results
):
159 gu
= self
.status
.getURLForThing
160 self
._transmit
('stepFinished',
161 [('builder', build
.getBuilder().getName()),
162 ('category', build
.getBuilder().getCategory()),
163 ('buildNumber', build
.getNumber()),
164 ('resultStatus', status
.builder
.Results
[results
[0]]),
165 ('resultString', ' '.join(results
[1])),
166 ('step', step
.getName())]
167 + [('logFile', gu(l
)) for l
in step
.getLogs()])
169 def _subscribe(self
):
170 self
.status
.subscribe(self
)
172 def setServiceParent(self
, parent
):
173 status
.base
.StatusReceiverMultiService
.setServiceParent(self
, parent
)
174 self
.status
= parent
.getStatus()
176 self
._transmit
('startup')