19
19
webhook_github ,
20
20
webhook_gitlab ,
21
21
)
22
- from readthedocs .core .views .hooks import build_branches , sync_versions
22
+ from readthedocs .core .views .hooks import (
23
+ build_branches ,
24
+ sync_versions ,
25
+ get_or_create_external_version ,
26
+ delete_external_version ,
27
+ build_external_version ,
28
+ )
23
29
from readthedocs .integrations .models import HttpExchange , Integration
24
- from readthedocs .projects .models import Project
30
+ from readthedocs .projects .models import Project , Feature
25
31
26
32
27
33
log = logging .getLogger (__name__ )
28
34
29
35
GITHUB_EVENT_HEADER = 'HTTP_X_GITHUB_EVENT'
30
36
GITHUB_SIGNATURE_HEADER = 'HTTP_X_HUB_SIGNATURE'
31
37
GITHUB_PUSH = 'push'
38
+ GITHUB_PULL_REQUEST = 'pull_request'
39
+ GITHUB_PULL_REQUEST_OPENED = 'opened'
40
+ GITHUB_PULL_REQUEST_CLOSED = 'closed'
41
+ GITHUB_PULL_REQUEST_REOPENED = 'reopened'
42
+ GITHUB_PULL_REQUEST_SYNC = 'synchronize'
32
43
GITHUB_CREATE = 'create'
33
44
GITHUB_DELETE = 'delete'
34
45
GITLAB_TOKEN_HEADER = 'HTTP_X_GITLAB_TOKEN'
@@ -110,6 +121,10 @@ def handle_webhook(self):
110
121
"""Handle webhook payload."""
111
122
raise NotImplementedError
112
123
124
+ def get_external_version_data (self ):
125
+ """Get External Version data from payload."""
126
+ raise NotImplementedError
127
+
113
128
def is_payload_valid (self ):
114
129
"""Validates the webhook's payload using the integration's secret."""
115
130
return False
@@ -183,28 +198,96 @@ def sync_versions(self, project):
183
198
'versions' : [version ],
184
199
}
185
200
201
+ def get_external_version_response (self , project ):
202
+ """
203
+ Trigger builds for External versions on pull/merge request events and return API response.
204
+
205
+ Return a JSON response with the following::
206
+
207
+ {
208
+ "build_triggered": true,
209
+ "project": "project_name",
210
+ "versions": [verbose_name]
211
+ }
212
+
213
+ :param project: Project instance
214
+ :type project: readthedocs.projects.models.Project
215
+ """
216
+ identifier , verbose_name = self .get_external_version_data ()
217
+ # create or get external version object using `verbose_name`.
218
+ external_version = get_or_create_external_version (
219
+ project , identifier , verbose_name
220
+ )
221
+ # returns external version verbose_name (pull/merge request number)
222
+ to_build = build_external_version (project , external_version )
223
+
224
+ return {
225
+ 'build_triggered' : True ,
226
+ 'project' : project .slug ,
227
+ 'versions' : [to_build ],
228
+ }
229
+
230
+ def get_delete_external_version_response (self , project ):
231
+ """
232
+ Delete External version on pull/merge request `closed` events and return API response.
233
+
234
+ Return a JSON response with the following::
235
+
236
+ {
237
+ "version_deleted": true,
238
+ "project": "project_name",
239
+ "versions": [verbose_name]
240
+ }
241
+
242
+ :param project: Project instance
243
+ :type project: Project
244
+ """
245
+ identifier , verbose_name = self .get_external_version_data ()
246
+ # Delete external version
247
+ deleted_version = delete_external_version (
248
+ project , identifier , verbose_name
249
+ )
250
+ return {
251
+ 'version_deleted' : deleted_version is not None ,
252
+ 'project' : project .slug ,
253
+ 'versions' : [deleted_version ],
254
+ }
255
+
186
256
187
257
class GitHubWebhookView (WebhookMixin , APIView ):
188
258
189
259
"""
190
260
Webhook consumer for GitHub.
191
261
192
- Accepts webhook events from GitHub, 'push' events trigger builds. Expects the
193
- webhook event type will be included in HTTP header ``X-GitHub-Event``, and
194
- we will have a JSON payload.
262
+ Accepts webhook events from GitHub, 'push' and 'pull_request' events trigger builds.
263
+ Expects the webhook event type will be included in HTTP header ``X-GitHub-Event``,
264
+ and we will have a JSON payload.
195
265
196
266
Expects the following JSON::
197
267
198
- {
199
- "ref": "branch-name",
200
- ...
201
- }
268
+ For push, create, delete Events:
269
+ {
270
+ "ref": "branch-name",
271
+ ...
272
+ }
273
+
274
+ For pull_request Events:
275
+ {
276
+ "action": "opened",
277
+ "number": 2,
278
+ "pull_request": {
279
+ "head": {
280
+ "sha": "ec26de721c3235aad62de7213c562f8c821"
281
+ }
282
+ }
283
+ }
202
284
203
285
See full payload here:
204
286
205
287
- https://developer.github.com/v3/activity/events/types/#pushevent
206
288
- https://developer.github.com/v3/activity/events/types/#createevent
207
289
- https://developer.github.com/v3/activity/events/types/#deleteevent
290
+ - https://developer.github.com/v3/activity/events/types/#pullrequestevent
208
291
"""
209
292
210
293
integration_type = Integration .GITHUB_WEBHOOK
@@ -218,6 +301,17 @@ def get_data(self):
218
301
pass
219
302
return super ().get_data ()
220
303
304
+ def get_external_version_data (self ):
305
+ """Get Commit Sha and pull request number from payload."""
306
+ try :
307
+ identifier = self .data ['pull_request' ]['head' ]['sha' ]
308
+ verbose_name = str (self .data ['number' ])
309
+
310
+ return identifier , verbose_name
311
+
312
+ except KeyError :
313
+ raise ParseError ('Parameters "sha" and "number" are required' )
314
+
221
315
def is_payload_valid (self ):
222
316
"""
223
317
GitHub use a HMAC hexdigest hash to sign the payload.
@@ -256,6 +350,7 @@ def get_digest(secret, msg):
256
350
257
351
def handle_webhook (self ):
258
352
# Get event and trigger other webhook events
353
+ action = self .data .get ('action' , None )
259
354
event = self .request .META .get (GITHUB_EVENT_HEADER , GITHUB_PUSH )
260
355
webhook_github .send (
261
356
Project ,
@@ -272,6 +367,26 @@ def handle_webhook(self):
272
367
raise ParseError ('Parameter "ref" is required' )
273
368
if event in (GITHUB_CREATE , GITHUB_DELETE ):
274
369
return self .sync_versions (self .project )
370
+
371
+ if (
372
+ self .project .has_feature (Feature .EXTERNAL_VERSION_BUILD ) and
373
+ event == GITHUB_PULL_REQUEST and action
374
+ ):
375
+ if (
376
+ action in
377
+ [
378
+ GITHUB_PULL_REQUEST_OPENED ,
379
+ GITHUB_PULL_REQUEST_REOPENED ,
380
+ GITHUB_PULL_REQUEST_SYNC
381
+ ]
382
+ ):
383
+ # Handle opened, synchronize, reopened pull_request event.
384
+ return self .get_external_version_response (self .project )
385
+
386
+ if action == GITHUB_PULL_REQUEST_CLOSED :
387
+ # Handle closed pull_request event.
388
+ return self .get_delete_external_version_response (self .project )
389
+
275
390
return None
276
391
277
392
def _normalize_ref (self , ref ):
0 commit comments