Skip to content

Commit 37f2ef4

Browse files
authored
Merge pull request #4876 from stsewd/sync-version-when-creating-branch-tag
Sync versions when creating/deleting versions
2 parents 7f2b831 + 804d717 commit 37f2ef4

File tree

4 files changed

+416
-58
lines changed

4 files changed

+416
-58
lines changed

readthedocs/core/views/hooks.py

+39-5
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
"""Views pertaining to builds."""
22

3-
from __future__ import absolute_import
3+
from __future__ import (
4+
absolute_import,
5+
division,
6+
print_function,
7+
unicode_literals,
8+
)
9+
410
import json
11+
import logging
512
import re
613

714
from django.http import HttpResponse, HttpResponseNotFound
815
from django.shortcuts import redirect
916
from django.views.decorators.csrf import csrf_exempt
1017

11-
from readthedocs.core.utils import trigger_build
1218
from readthedocs.builds.constants import LATEST
19+
from readthedocs.core.utils import trigger_build
1320
from readthedocs.projects import constants
14-
from readthedocs.projects.models import Project, Feature
21+
from readthedocs.projects.models import Feature, Project
1522
from readthedocs.projects.tasks import sync_repository_task
1623

17-
import logging
18-
1924
log = logging.getLogger(__name__)
2025

2126

@@ -75,6 +80,35 @@ def build_branches(project, branch_list):
7580
return (to_build, not_building)
7681

7782

83+
def sync_versions(project):
84+
"""
85+
Sync the versions of a repo using its latest version.
86+
87+
This doesn't register a new build,
88+
but clones the repo and syncs the versions.
89+
Due that `sync_repository_task` is bound to a version,
90+
we always pass the default version.
91+
92+
:returns: The version slug that was used to trigger the clone.
93+
:rtype: str
94+
"""
95+
try:
96+
version_identifier = project.get_default_branch()
97+
version = (
98+
project.versions
99+
.filter(identifier=version_identifier)
100+
.first()
101+
)
102+
if not version:
103+
log.info('Unable to sync from %s version', version_identifier)
104+
return None
105+
sync_repository_task.delay(version.pk)
106+
return version.slug
107+
except Exception:
108+
log.exception('Unknown sync versions exception')
109+
return None
110+
111+
78112
def get_project_from_url(url):
79113
if not url:
80114
return Project.objects.none()

readthedocs/oauth/services/github.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def get_webhook_data(self, project, integration):
172172
),
173173
'content_type': 'json',
174174
},
175-
'events': ['push', 'pull_request'],
175+
'events': ['push', 'pull_request', 'create', 'delete'],
176176
})
177177

178178
def setup_webhook(self, project):

readthedocs/restapi/views/integrations.py

+120-29
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,44 @@
11
"""Endpoints integrating with Github, Bitbucket, and other webhooks."""
22

3-
from __future__ import absolute_import
3+
from __future__ import (
4+
absolute_import,
5+
division,
6+
print_function,
7+
unicode_literals,
8+
)
9+
410
import json
511
import logging
612
import re
713

8-
from builtins import object
14+
import six
15+
from django.shortcuts import get_object_or_404
916
from rest_framework import permissions
10-
from rest_framework.views import APIView
17+
from rest_framework.exceptions import NotFound, ParseError
1118
from rest_framework.renderers import JSONRenderer
1219
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
1621

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
2028
from readthedocs.integrations.models import HttpExchange, Integration
2129
from readthedocs.integrations.utils import normalize_request_payload
2230
from readthedocs.projects.models import Project
23-
import six
24-
2531

2632
log = logging.getLogger(__name__)
2733

34+
GITHUB_EVENT_HEADER = 'HTTP_X_GITHUB_EVENT'
2835
GITHUB_PUSH = 'push'
36+
GITHUB_CREATE = 'create'
37+
GITHUB_DELETE = 'delete'
2938
GITLAB_PUSH = 'push'
39+
GITLAB_NULL_HASH = '0' * 40
40+
GITLAB_TAG_PUSH = 'tag_push'
41+
BITBUCKET_EVENT_HEADER = 'HTTP_X_EVENT_KEY'
3042
BITBUCKET_PUSH = 'repo:push'
3143

3244

@@ -124,6 +136,14 @@ def get_response_push(self, project, branches):
124136
'project': project.slug,
125137
'versions': list(to_build)}
126138

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+
127147

128148
class GitHubWebhookView(WebhookMixin, APIView):
129149

@@ -140,6 +160,12 @@ class GitHubWebhookView(WebhookMixin, APIView):
140160
"ref": "branch-name",
141161
...
142162
}
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
143169
"""
144170

145171
integration_type = Integration.GITHUB_WEBHOOK
@@ -154,16 +180,23 @@ def get_data(self):
154180

155181
def handle_webhook(self):
156182
# 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+
)
160190
# Handle push events and trigger builds
161191
if event == GITHUB_PUSH:
162192
try:
163193
branches = [self._normalize_ref(self.data['ref'])]
164194
return self.get_response_push(self.project, branches)
165195
except KeyError:
166196
raise ParseError('Parameter "ref" is required')
197+
if event in (GITHUB_CREATE, GITHUB_DELETE):
198+
return self.sync_versions(self.project)
199+
return None
167200

168201
def _normalize_ref(self, ref):
169202
pattern = re.compile(r'^refs/(heads|tags)/')
@@ -180,26 +213,55 @@ class GitLabWebhookView(WebhookMixin, APIView):
180213
Expects the following JSON::
181214
182215
{
216+
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
217+
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
183218
"object_kind": "push",
184219
"ref": "branch-name",
185220
...
186221
}
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
187227
"""
188228

189229
integration_type = Integration.GITLAB_WEBHOOK
190230

191231
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+
"""
193239
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+
)
196246
# 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
198255
try:
199-
branches = [self.request.data['ref'].replace('refs/heads/', '')]
256+
branches = [self._normalize_ref(data['ref'])]
200257
return self.get_response_push(self.project, branches)
201258
except KeyError:
202259
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)
203265

204266

205267
class BitbucketWebhookView(WebhookMixin, APIView):
@@ -218,31 +280,60 @@ class BitbucketWebhookView(WebhookMixin, APIView):
218280
"name": "branch-name",
219281
...
220282
},
283+
"old" {
284+
"name": "branch-name",
285+
...
286+
},
221287
...
222288
}],
223289
...
224290
},
225291
...
226292
}
293+
294+
See full payload here:
295+
296+
- https://confluence.atlassian.com/bitbucket/event-payloads-740262817.html#EventPayloads-Push
227297
"""
228298

229299
integration_type = Integration.BITBUCKET_WEBHOOK
230300

231301
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+
)
237316
if event == BITBUCKET_PUSH:
238317
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)
244334
except KeyError:
245335
raise ParseError('Invalid request')
336+
return None
246337

247338

248339
class IsAuthenticatedOrHasToken(permissions.IsAuthenticated):

0 commit comments

Comments
 (0)