Skip to content

Commit 356d811

Browse files
authored
Merge pull request #5823 from readthedocs/gsoc-19-pr-builder
Merge initial work from Pull Request Builder GSOC
2 parents fcab59c + d89d519 commit 356d811

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2063
-107
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ logs/*
2828
media/dash
2929
media/epub
3030
media/export
31+
media/external
3132
media/html
3233
media/htmlzip
3334
media/json

docs/guides/feature-flags.rst

+2
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ e.g. python-reno release notes manager is known to do that
2929
(error message line would probably include one of old Git commit id's).
3030

3131
``USE_TESTING_BUILD_IMAGE``: :featureflags:`USE_TESTING_BUILD_IMAGE`
32+
33+
``EXTERNAL_VERSION_BUILD``: :featureflags:`EXTERNAL_VERSION_BUILD`

readthedocs/api/v2/serializers.py

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class BuildSerializer(serializers.ModelSerializer):
118118
version_slug = serializers.ReadOnlyField(source='version.slug')
119119
docs_url = serializers.ReadOnlyField(source='version.get_absolute_url')
120120
state_display = serializers.ReadOnlyField(source='get_state_display')
121+
commit_url = serializers.ReadOnlyField(source='get_commit_url')
121122
# Jsonfield needs an explicit serializer
122123
# https://github.com/dmkoch/django-jsonfield/issues/188#issuecomment-300439829
123124
config = serializers.JSONField(required=False)

readthedocs/api/v2/views/footer_views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def get_version_compare_data(project, base_version=None):
2525
:param base_version: We assert whether or not the base_version is also the
2626
highest version in the resulting "is_highest" value.
2727
"""
28-
versions_qs = Version.objects.public(project=project)
28+
versions_qs = Version.internal.public(project=project)
2929

3030
# Take preferences over tags only if the project has at least one tag
3131
if versions_qs.filter(type=TAG).exists():

readthedocs/api/v2/views/integrations.py

+124-9
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,27 @@
1919
webhook_github,
2020
webhook_gitlab,
2121
)
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+
)
2329
from readthedocs.integrations.models import HttpExchange, Integration
24-
from readthedocs.projects.models import Project
30+
from readthedocs.projects.models import Project, Feature
2531

2632

2733
log = logging.getLogger(__name__)
2834

2935
GITHUB_EVENT_HEADER = 'HTTP_X_GITHUB_EVENT'
3036
GITHUB_SIGNATURE_HEADER = 'HTTP_X_HUB_SIGNATURE'
3137
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'
3243
GITHUB_CREATE = 'create'
3344
GITHUB_DELETE = 'delete'
3445
GITLAB_TOKEN_HEADER = 'HTTP_X_GITLAB_TOKEN'
@@ -110,6 +121,10 @@ def handle_webhook(self):
110121
"""Handle webhook payload."""
111122
raise NotImplementedError
112123

124+
def get_external_version_data(self):
125+
"""Get External Version data from payload."""
126+
raise NotImplementedError
127+
113128
def is_payload_valid(self):
114129
"""Validates the webhook's payload using the integration's secret."""
115130
return False
@@ -183,28 +198,96 @@ def sync_versions(self, project):
183198
'versions': [version],
184199
}
185200

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

187257
class GitHubWebhookView(WebhookMixin, APIView):
188258

189259
"""
190260
Webhook consumer for GitHub.
191261
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.
195265
196266
Expects the following JSON::
197267
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+
}
202284
203285
See full payload here:
204286
205287
- https://developer.github.com/v3/activity/events/types/#pushevent
206288
- https://developer.github.com/v3/activity/events/types/#createevent
207289
- https://developer.github.com/v3/activity/events/types/#deleteevent
290+
- https://developer.github.com/v3/activity/events/types/#pullrequestevent
208291
"""
209292

210293
integration_type = Integration.GITHUB_WEBHOOK
@@ -218,6 +301,17 @@ def get_data(self):
218301
pass
219302
return super().get_data()
220303

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+
221315
def is_payload_valid(self):
222316
"""
223317
GitHub use a HMAC hexdigest hash to sign the payload.
@@ -256,6 +350,7 @@ def get_digest(secret, msg):
256350

257351
def handle_webhook(self):
258352
# Get event and trigger other webhook events
353+
action = self.data.get('action', None)
259354
event = self.request.META.get(GITHUB_EVENT_HEADER, GITHUB_PUSH)
260355
webhook_github.send(
261356
Project,
@@ -272,6 +367,26 @@ def handle_webhook(self):
272367
raise ParseError('Parameter "ref" is required')
273368
if event in (GITHUB_CREATE, GITHUB_DELETE):
274369
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+
275390
return None
276391

277392
def _normalize_ref(self, ref):

readthedocs/api/v2/views/model_views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from rest_framework.renderers import BaseRenderer, JSONRenderer
1111
from rest_framework.response import Response
1212

13-
from readthedocs.builds.constants import BRANCH, TAG
13+
from readthedocs.builds.constants import BRANCH, TAG, INTERNAL
1414
from readthedocs.builds.models import Build, BuildCommandResult, Version
1515
from readthedocs.core.utils import trigger_build
1616
from readthedocs.core.utils.extend import SettingsOverrideObject
@@ -130,7 +130,7 @@ def active_versions(self, request, **kwargs):
130130
Project.objects.api(request.user),
131131
pk=kwargs['pk'],
132132
)
133-
versions = project.versions.filter(active=True)
133+
versions = project.versions(manager=INTERNAL).filter(active=True)
134134
return Response({
135135
'versions': VersionSerializer(versions, many=True).data,
136136
})

readthedocs/api/v3/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ class VersionsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin,
281281
lookup_value_regex = r'[^/]+'
282282

283283
filterset_class = VersionFilter
284-
queryset = Version.objects.all()
284+
queryset = Version.internal.all()
285285
permit_list_expands = [
286286
'last_build',
287287
'last_build.config',
@@ -320,7 +320,7 @@ class BuildsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin,
320320
lookup_url_kwarg = 'build_pk'
321321
serializer_class = BuildSerializer
322322
filterset_class = BuildFilter
323-
queryset = Build.objects.all()
323+
queryset = Build.internal.all()
324324
permit_list_expands = [
325325
'config',
326326
]

readthedocs/builds/constants.py

+39
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@
3131
('dash', _('Dash')),
3232
)
3333

34+
# Manager name for Internal Versions or Builds.
35+
# ie: Versions and Builds Excluding pull request/merge request Versions and Builds.
36+
INTERNAL = 'internal'
37+
# Manager name for External Versions or Builds.
38+
# ie: Only pull request/merge request Versions and Builds.
39+
EXTERNAL = 'external'
40+
3441
BRANCH = 'branch'
3542
TAG = 'tag'
3643
UNKNOWN = 'unknown'
3744

3845
VERSION_TYPES = (
3946
(BRANCH, _('Branch')),
4047
(TAG, _('Tag')),
48+
(EXTERNAL, _('External')),
4149
(UNKNOWN, _('Unknown')),
4250
)
4351

@@ -53,3 +61,34 @@
5361
LATEST,
5462
STABLE,
5563
)
64+
65+
# General Build Statuses
66+
BUILD_STATUS_FAILURE = 'failed'
67+
BUILD_STATUS_PENDING = 'pending'
68+
BUILD_STATUS_SUCCESS = 'success'
69+
70+
# GitHub Build Statuses
71+
GITHUB_BUILD_STATUS_FAILURE = 'failure'
72+
GITHUB_BUILD_STATUS_PENDING = 'pending'
73+
GITHUB_BUILD_STATUS_SUCCESS = 'success'
74+
75+
# Used to select correct Build status and description to be sent to each service API
76+
SELECT_BUILD_STATUS = {
77+
BUILD_STATUS_FAILURE: {
78+
'github': GITHUB_BUILD_STATUS_FAILURE,
79+
'description': 'Read the Docs build failed!',
80+
},
81+
BUILD_STATUS_PENDING: {
82+
'github': GITHUB_BUILD_STATUS_PENDING,
83+
'description': 'Read the Docs build is in progress!',
84+
},
85+
BUILD_STATUS_SUCCESS: {
86+
'github': GITHUB_BUILD_STATUS_SUCCESS,
87+
'description': 'Read the Docs build succeeded!',
88+
},
89+
}
90+
91+
RTD_BUILD_STATUS_API_NAME = 'continuous-documentation/read-the-docs'
92+
93+
GITHUB_EXTERNAL_VERSION_NAME = 'Pull Request'
94+
GENERIC_EXTERNAL_VERSION_NAME = 'External Version'

0 commit comments

Comments
 (0)