Skip to content

Commit 7cf66f6

Browse files
authored
Integrations: allow to pass more data about external versions (#7692)
This was extracted from #7664
1 parent 2151cf0 commit 7cf66f6

File tree

3 files changed

+88
-48
lines changed

3 files changed

+88
-48
lines changed

readthedocs/api/v2/views/integrations.py

+41-29
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import hashlib
44
import hmac
55
import json
6-
import structlog
76
import re
7+
from functools import namedtuple
88

9+
import structlog
910
from django.shortcuts import get_object_or_404
1011
from rest_framework import permissions, status
1112
from rest_framework.exceptions import NotFound, ParseError
@@ -14,11 +15,7 @@
1415
from rest_framework.status import HTTP_400_BAD_REQUEST
1516
from rest_framework.views import APIView
1617

17-
from readthedocs.core.signals import (
18-
webhook_bitbucket,
19-
webhook_github,
20-
webhook_gitlab,
21-
)
18+
from readthedocs.core.signals import webhook_bitbucket, webhook_github, webhook_gitlab
2219
from readthedocs.core.views.hooks import (
2320
build_branches,
2421
build_external_version,
@@ -27,7 +24,7 @@
2724
trigger_sync_versions,
2825
)
2926
from readthedocs.integrations.models import HttpExchange, Integration
30-
from readthedocs.projects.models import Feature, Project
27+
from readthedocs.projects.models import Project
3128

3229
log = structlog.get_logger(__name__)
3330

@@ -55,6 +52,12 @@
5552
BITBUCKET_PUSH = 'repo:push'
5653

5754

55+
ExternalVersionData = namedtuple(
56+
"ExternalVersionData",
57+
["id", "source_branch", "base_branch", "commit"],
58+
)
59+
60+
5861
class WebhookMixin:
5962

6063
"""Base class for Webhook mixins."""
@@ -228,20 +231,22 @@ def get_external_version_response(self, project):
228231
:param project: Project instance
229232
:type project: readthedocs.projects.models.Project
230233
"""
231-
identifier, verbose_name = self.get_external_version_data()
234+
version_data = self.get_external_version_data()
232235
# create or get external version object using `verbose_name`.
233236
external_version = get_or_create_external_version(
234-
project, identifier, verbose_name
237+
project=project,
238+
version_data=version_data,
235239
)
236240
# returns external version verbose_name (pull/merge request number)
237241
to_build = build_external_version(
238-
project=project, version=external_version, commit=identifier
242+
project=project,
243+
version=external_version,
239244
)
240245

241246
return {
242-
'build_triggered': True,
243-
'project': project.slug,
244-
'versions': [to_build],
247+
"build_triggered": bool(to_build),
248+
"project": project.slug,
249+
"versions": [to_build] if to_build else [],
245250
}
246251

247252
def get_deactivated_external_version_response(self, project):
@@ -259,9 +264,10 @@ def get_deactivated_external_version_response(self, project):
259264
:param project: Project instance
260265
:type project: Project
261266
"""
262-
identifier, verbose_name = self.get_external_version_data()
267+
version_data = self.get_external_version_data()
263268
deactivated_version = deactivate_external_version(
264-
project, identifier, verbose_name
269+
project=project,
270+
version_data=version_data,
265271
)
266272
return {
267273
'version_deactivated': bool(deactivated_version),
@@ -320,13 +326,16 @@ def get_data(self):
320326
def get_external_version_data(self):
321327
"""Get Commit Sha and pull request number from payload."""
322328
try:
323-
identifier = self.data['pull_request']['head']['sha']
324-
verbose_name = str(self.data['number'])
325-
326-
return identifier, verbose_name
327-
328-
except KeyError:
329-
raise ParseError('Parameters "sha" and "number" are required')
329+
data = ExternalVersionData(
330+
id=str(self.data["number"]),
331+
commit=self.data["pull_request"]["head"]["sha"],
332+
source_branch=self.data["pull_request"]["head"]["ref"],
333+
base_branch=self.data["pull_request"]["base"]["ref"],
334+
)
335+
return data
336+
except KeyError as e:
337+
key = e.args[0]
338+
raise ParseError(f"Invalid payload. {key} is required.")
330339

331340
def is_payload_valid(self):
332341
"""
@@ -523,13 +532,16 @@ def is_payload_valid(self):
523532
def get_external_version_data(self):
524533
"""Get commit SHA and merge request number from payload."""
525534
try:
526-
identifier = self.data['object_attributes']['last_commit']['id']
527-
verbose_name = str(self.data['object_attributes']['iid'])
528-
529-
return identifier, verbose_name
530-
531-
except KeyError:
532-
raise ParseError('Parameters "id" and "iid" are required')
535+
data = ExternalVersionData(
536+
id=str(self.data["object_attributes"]["iid"]),
537+
commit=self.data["object_attributes"]["last_commit"]["id"],
538+
source_branch=self.data["object_attributes"]["source_branch"],
539+
base_branch=self.data["object_attributes"]["target_branch"],
540+
)
541+
return data
542+
except KeyError as e:
543+
key = e.args[0]
544+
raise ParseError(f"Invalid payload. {key} is required.")
533545

534546
def handle_webhook(self):
535547
"""

readthedocs/core/views/hooks.py

+21-15
Original file line numberDiff line numberDiff line change
@@ -118,22 +118,22 @@ def trigger_sync_versions(project):
118118
return None
119119

120120

121-
def get_or_create_external_version(project, identifier, verbose_name):
121+
def get_or_create_external_version(project, version_data):
122122
"""
123-
Get or create external versions using `identifier` and `verbose_name`.
123+
Get or create version using the ``commit`` as identifier, and PR id as ``verbose_name``.
124124
125125
if external version does not exist create an external version
126126
127127
:param project: Project instance
128-
:param identifier: Commit Hash
129-
:param verbose_name: pull/merge request number
130-
:returns: External version.
128+
:param version_data: A :py:class:`readthedocs.api.v2.views.integrations.ExternalVersionData`
129+
instance.
130+
:returns: External version.
131131
:rtype: Version
132132
"""
133133
external_version, created = project.versions.get_or_create(
134-
verbose_name=verbose_name,
134+
verbose_name=version_data.id,
135135
type=EXTERNAL,
136-
defaults={'identifier': identifier, 'active': True},
136+
defaults={"identifier": version_data.commit, "active": True},
137137
)
138138

139139
if created:
@@ -144,11 +144,10 @@ def get_or_create_external_version(project, identifier, verbose_name):
144144
)
145145
else:
146146
# Identifier will change if there is a new commit to the Pull/Merge Request.
147-
external_version.identifier = identifier
147+
external_version.identifier = version_data.commit
148148
# If the PR was previously closed it was marked as inactive.
149149
external_version.active = True
150150
external_version.save()
151-
152151
log.info(
153152
'External version updated.',
154153
project_slug=project.slug,
@@ -157,7 +156,7 @@ def get_or_create_external_version(project, identifier, verbose_name):
157156
return external_version
158157

159158

160-
def deactivate_external_version(project, identifier, verbose_name):
159+
def deactivate_external_version(project, version_data):
161160
"""
162161
Deactivate external versions using `identifier` and `verbose_name`.
163162
@@ -167,14 +166,21 @@ def deactivate_external_version(project, identifier, verbose_name):
167166
so another celery task will remove it after some days.
168167
169168
:param project: Project instance
169+
:param version_data: A :py:class:`readthedocs.api.v2.views.integrations.ExternalVersionData`
170+
instance.
170171
:param identifier: Commit Hash
171172
:param verbose_name: pull/merge request number
172173
:returns: verbose_name (pull/merge request number).
173174
:rtype: str
174175
"""
175-
external_version = project.versions(manager=EXTERNAL).filter(
176-
verbose_name=verbose_name, identifier=identifier
177-
).first()
176+
external_version = (
177+
project.versions(manager=EXTERNAL)
178+
.filter(
179+
verbose_name=version_data.id,
180+
identifier=version_data.commit,
181+
)
182+
.first()
183+
)
178184

179185
if external_version:
180186
external_version.active = False
@@ -188,7 +194,7 @@ def deactivate_external_version(project, identifier, verbose_name):
188194
return None
189195

190196

191-
def build_external_version(project, version, commit):
197+
def build_external_version(project, version):
192198
"""
193199
Where we actually trigger builds for external versions.
194200
@@ -204,6 +210,6 @@ def build_external_version(project, version, commit):
204210
project_slug=project.slug,
205211
version_slug=version.slug,
206212
)
207-
trigger_build(project=project, version=version, commit=commit)
213+
trigger_build(project=project, version=version, commit=version.identifier)
208214

209215
return version.verbose_name

readthedocs/rtd_tests/tests/test_api.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -920,9 +920,13 @@ def setUp(self):
920920
"number": 2,
921921
"pull_request": {
922922
"head": {
923-
"sha": self.commit
924-
}
925-
}
923+
"sha": self.commit,
924+
"ref": "source_branch",
925+
},
926+
"base": {
927+
"ref": "master",
928+
},
929+
},
926930
}
927931
self.gitlab_merge_request_payload = {
928932
"object_kind": GITLAB_MERGE_REQUEST,
@@ -931,7 +935,9 @@ def setUp(self):
931935
"last_commit": {
932936
"id": self.commit
933937
},
934-
"action": "open"
938+
"action": "open",
939+
"source_branch": "source_branch",
940+
"target_branch": "master",
935941
},
936942
}
937943
self.gitlab_payload = {
@@ -1526,6 +1532,14 @@ def test_github_dont_trigger_double_sync(self, trigger_build):
15261532
)
15271533
self.assertTrue(resp.json()['versions_synced'])
15281534

1535+
def test_github_get_external_version_data(self, trigger_build):
1536+
view = GitHubWebhookView(data=self.github_pull_request_payload)
1537+
version_data = view.get_external_version_data()
1538+
self.assertEqual(version_data.id, "2")
1539+
self.assertEqual(version_data.commit, self.commit)
1540+
self.assertEqual(version_data.source_branch, "source_branch")
1541+
self.assertEqual(version_data.base_branch, "master")
1542+
15291543
def test_gitlab_webhook_for_branches(self, trigger_build):
15301544
"""GitLab webhook API."""
15311545
client = APIClient()
@@ -2041,6 +2055,14 @@ def test_gitlab_merge_request_close_event_invalid_payload(self, trigger_build):
20412055

20422056
self.assertEqual(resp.status_code, 400)
20432057

2058+
def test_gitlab_get_external_version_data(self, trigger_build):
2059+
view = GitLabWebhookView(data=self.gitlab_merge_request_payload)
2060+
version_data = view.get_external_version_data()
2061+
self.assertEqual(version_data.id, "2")
2062+
self.assertEqual(version_data.commit, self.commit)
2063+
self.assertEqual(version_data.source_branch, "source_branch")
2064+
self.assertEqual(version_data.base_branch, "master")
2065+
20442066
def test_bitbucket_webhook(self, trigger_build):
20452067
"""Bitbucket webhook API."""
20462068
client = APIClient()

0 commit comments

Comments
 (0)