1
1
"""Endpoints integrating with Github, Bitbucket, and other webhooks."""
2
2
3
- from __future__ import absolute_import
3
+ from __future__ import (
4
+ absolute_import ,
5
+ division ,
6
+ print_function ,
7
+ unicode_literals ,
8
+ )
9
+
4
10
import json
5
11
import logging
6
12
import re
7
13
8
- from builtins import object
14
+ import six
15
+ from django .shortcuts import get_object_or_404
9
16
from rest_framework import permissions
10
- from rest_framework .views import APIView
17
+ from rest_framework .exceptions import NotFound , ParseError
11
18
from rest_framework .renderers import JSONRenderer
12
19
from rest_framework .response import Response
13
- from rest_framework .exceptions import ParseError , NotFound
14
-
15
- from django .shortcuts import get_object_or_404
20
+ from rest_framework .views import APIView
16
21
17
- from readthedocs .core .views .hooks import build_branches
18
- from readthedocs .core .signals import (webhook_github , webhook_bitbucket ,
19
- webhook_gitlab )
22
+ from readthedocs .core .signals import (
23
+ webhook_bitbucket ,
24
+ webhook_github ,
25
+ webhook_gitlab ,
26
+ )
27
+ from readthedocs .core .views .hooks import build_branches , sync_versions
20
28
from readthedocs .integrations .models import HttpExchange , Integration
21
29
from readthedocs .integrations .utils import normalize_request_payload
22
30
from readthedocs .projects .models import Project
23
- import six
24
-
25
31
26
32
log = logging .getLogger (__name__ )
27
33
34
+ GITHUB_EVENT_HEADER = 'HTTP_X_GITHUB_EVENT'
28
35
GITHUB_PUSH = 'push'
36
+ GITHUB_CREATE = 'create'
37
+ GITHUB_DELETE = 'delete'
29
38
GITLAB_PUSH = 'push'
39
+ GITLAB_NULL_HASH = '0' * 40
40
+ GITLAB_TAG_PUSH = 'tag_push'
41
+ BITBUCKET_EVENT_HEADER = 'HTTP_X_EVENT_KEY'
30
42
BITBUCKET_PUSH = 'repo:push'
31
43
32
44
@@ -124,6 +136,14 @@ def get_response_push(self, project, branches):
124
136
'project' : project .slug ,
125
137
'versions' : list (to_build )}
126
138
139
+ def sync_versions (self , project ):
140
+ version = sync_versions (project )
141
+ return {
142
+ 'build_triggered' : False ,
143
+ 'project' : project .slug ,
144
+ 'versions' : [version ],
145
+ }
146
+
127
147
128
148
class GitHubWebhookView (WebhookMixin , APIView ):
129
149
@@ -140,6 +160,12 @@ class GitHubWebhookView(WebhookMixin, APIView):
140
160
"ref": "branch-name",
141
161
...
142
162
}
163
+
164
+ See full payload here:
165
+
166
+ - https://developer.github.com/v3/activity/events/types/#pushevent
167
+ - https://developer.github.com/v3/activity/events/types/#createevent
168
+ - https://developer.github.com/v3/activity/events/types/#deleteevent
143
169
"""
144
170
145
171
integration_type = Integration .GITHUB_WEBHOOK
@@ -154,16 +180,23 @@ def get_data(self):
154
180
155
181
def handle_webhook (self ):
156
182
# Get event and trigger other webhook events
157
- event = self .request .META .get ('HTTP_X_GITHUB_EVENT' , 'push' )
158
- webhook_github .send (Project , project = self .project ,
159
- data = self .data , event = event )
183
+ event = self .request .META .get (GITHUB_EVENT_HEADER , GITHUB_PUSH )
184
+ webhook_github .send (
185
+ Project ,
186
+ project = self .project ,
187
+ data = self .data ,
188
+ event = event
189
+ )
160
190
# Handle push events and trigger builds
161
191
if event == GITHUB_PUSH :
162
192
try :
163
193
branches = [self ._normalize_ref (self .data ['ref' ])]
164
194
return self .get_response_push (self .project , branches )
165
195
except KeyError :
166
196
raise ParseError ('Parameter "ref" is required' )
197
+ if event in (GITHUB_CREATE , GITHUB_DELETE ):
198
+ return self .sync_versions (self .project )
199
+ return None
167
200
168
201
def _normalize_ref (self , ref ):
169
202
pattern = re .compile (r'^refs/(heads|tags)/' )
@@ -180,26 +213,55 @@ class GitLabWebhookView(WebhookMixin, APIView):
180
213
Expects the following JSON::
181
214
182
215
{
216
+ "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
217
+ "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
183
218
"object_kind": "push",
184
219
"ref": "branch-name",
185
220
...
186
221
}
222
+
223
+ See full payload here:
224
+
225
+ - https://docs.gitlab.com/ce/user/project/integrations/webhooks.html#push-events
226
+ - https://docs.gitlab.com/ce/user/project/integrations/webhooks.html#tag-events
187
227
"""
188
228
189
229
integration_type = Integration .GITLAB_WEBHOOK
190
230
191
231
def handle_webhook (self ):
192
- # Get event and trigger other webhook events
232
+ """
233
+ Handle GitLab events for push and tag_push.
234
+
235
+ GitLab doesn't have a separate event for creation/deletion,
236
+ instead, it sets the before/after field to
237
+ 0000000000000000000000000000000000000000 ('0' * 40)
238
+ """
193
239
event = self .request .data .get ('object_kind' , GITLAB_PUSH )
194
- webhook_gitlab .send (Project , project = self .project ,
195
- data = self .request .data , event = event )
240
+ webhook_gitlab .send (
241
+ Project ,
242
+ project = self .project ,
243
+ data = self .request .data ,
244
+ event = event
245
+ )
196
246
# Handle push events and trigger builds
197
- if event == GITLAB_PUSH :
247
+ if event in (GITLAB_PUSH , GITLAB_TAG_PUSH ):
248
+ data = self .request .data
249
+ before = data ['before' ]
250
+ after = data ['after' ]
251
+ # Tag/branch created/deleted
252
+ if GITLAB_NULL_HASH in (before , after ):
253
+ return self .sync_versions (self .project )
254
+ # Normal push to master
198
255
try :
199
- branches = [self .request . data ['ref' ]. replace ( 'refs/heads/' , '' )]
256
+ branches = [self ._normalize_ref ( data ['ref' ])]
200
257
return self .get_response_push (self .project , branches )
201
258
except KeyError :
202
259
raise ParseError ('Parameter "ref" is required' )
260
+ return None
261
+
262
+ def _normalize_ref (self , ref ):
263
+ pattern = re .compile (r'^refs/(heads|tags)/' )
264
+ return pattern .sub ('' , ref )
203
265
204
266
205
267
class BitbucketWebhookView (WebhookMixin , APIView ):
@@ -218,31 +280,60 @@ class BitbucketWebhookView(WebhookMixin, APIView):
218
280
"name": "branch-name",
219
281
...
220
282
},
283
+ "old" {
284
+ "name": "branch-name",
285
+ ...
286
+ },
221
287
...
222
288
}],
223
289
...
224
290
},
225
291
...
226
292
}
293
+
294
+ See full payload here:
295
+
296
+ - https://confluence.atlassian.com/bitbucket/event-payloads-740262817.html#EventPayloads-Push
227
297
"""
228
298
229
299
integration_type = Integration .BITBUCKET_WEBHOOK
230
300
231
301
def handle_webhook (self ):
232
- # Get event and trigger other webhook events
233
- event = self .request .META .get ('HTTP_X_EVENT_KEY' , BITBUCKET_PUSH )
234
- webhook_bitbucket .send (Project , project = self .project ,
235
- data = self .request .data , event = event )
236
- # Handle push events and trigger builds
302
+ """
303
+ Handle BitBucket events for push.
304
+
305
+ BitBucket doesn't have a separate event for creation/deletion,
306
+ instead it sets the new attribute (null if it is a deletion)
307
+ and the old attribute (null if it is a creation).
308
+ """
309
+ event = self .request .META .get (BITBUCKET_EVENT_HEADER , BITBUCKET_PUSH )
310
+ webhook_bitbucket .send (
311
+ Project ,
312
+ project = self .project ,
313
+ data = self .request .data ,
314
+ event = event
315
+ )
237
316
if event == BITBUCKET_PUSH :
238
317
try :
239
- changes = self .request .data ['push' ]['changes' ]
240
- branches = [change ['new' ]['name' ]
241
- for change in changes
242
- if change .get ('new' )]
243
- return self .get_response_push (self .project , branches )
318
+ data = self .request .data
319
+ changes = data ['push' ]['changes' ]
320
+ branches = []
321
+ for change in changes :
322
+ old = change ['old' ]
323
+ new = change ['new' ]
324
+ # Normal push to master
325
+ if old is not None and new is not None :
326
+ branches .append (new ['name' ])
327
+ # BitBuck returns an array of changes rather than
328
+ # one webhook per change. If we have at least one normal push
329
+ # we don't trigger the sync versions, because that
330
+ # will be triggered with the normal push.
331
+ if branches :
332
+ return self .get_response_push (self .project , branches )
333
+ return self .sync_versions (self .project )
244
334
except KeyError :
245
335
raise ParseError ('Invalid request' )
336
+ return None
246
337
247
338
248
339
class IsAuthenticatedOrHasToken (permissions .IsAuthenticated ):
0 commit comments