Skip to content

Commit 8bd1567

Browse files
authored
Build: Bug in target_url, failure to add "success" status if no external version exists (#10369)
* Clarifications to state vs status * Use an absolute URL, including the "https://<domain>" for versions * Update tests with new Version.get_absolute_url implementation * Revert a change: Always set `self.data.build["success"] = False` in on_failure * Don't use URLValidator in tests
1 parent b255f78 commit 8bd1567

File tree

6 files changed

+56
-27
lines changed

6 files changed

+56
-27
lines changed

readthedocs/builds/constants.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from django.conf import settings
44
from django.utils.translation import gettext_lazy as _
55

6+
# BUILD_STATE is our *INTERNAL* representation of build states.
7+
# This is not to be confused with external representations of 'status'
8+
# that are sent back to Git providers.
69
BUILD_STATE_TRIGGERED = "triggered"
710
BUILD_STATE_CLONING = "cloning"
811
BUILD_STATE_INSTALLING = "installing"
@@ -78,7 +81,9 @@
7881
STABLE,
7982
)
8083

81-
# General Build Statuses
84+
# General build statuses, i.e. the status that is reported back to the
85+
# user on a Git Provider. This not the same as BUILD_STATE which the internal
86+
# representation.
8287
BUILD_STATUS_FAILURE = 'failed'
8388
BUILD_STATUS_PENDING = 'pending'
8489
BUILD_STATUS_SUCCESS = 'success'

readthedocs/builds/models.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,29 @@ def commit_name(self):
377377
return self.identifier
378378

379379
def get_absolute_url(self):
380-
"""Get absolute url to the docs of the version."""
380+
"""
381+
Get the absolute URL to the docs of the version.
382+
383+
If the version doesn't have a successfully uploaded build, then we return the project's
384+
dashboard page.
385+
386+
Because documentation projects can be hosted on separate domains, this function ALWAYS
387+
returns with a full "http(s)://<domain>/" prefix.
388+
"""
381389
if not self.built and not self.uploaded:
382-
return reverse(
383-
'project_version_detail',
384-
kwargs={
385-
'project_slug': self.project.slug,
386-
'version_slug': self.slug,
387-
},
390+
# TODO: Stop assuming protocol based on settings.DEBUG
391+
# (this pattern is also used in builds.tasks for sending emails)
392+
protocol = "http" if settings.DEBUG else "https"
393+
return "{}://{}{}".format(
394+
protocol,
395+
settings.PRODUCTION_DOMAIN,
396+
reverse(
397+
"project_version_detail",
398+
kwargs={
399+
"project_slug": self.project.slug,
400+
"version_slug": self.slug,
401+
},
402+
),
388403
)
389404
external = self.type == EXTERNAL
390405
return self.project.get_docs_url(

readthedocs/oauth/services/github.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,8 @@ def send_build_status(self, build, commit, status):
446446
project = build.project
447447
owner, repo = build_utils.get_github_username_repo(url=project.repo)
448448

449-
# select the correct state and description.
450-
github_build_state = SELECT_BUILD_STATUS[status]["github"]
449+
# select the correct status and description.
450+
github_build_status = SELECT_BUILD_STATUS[status]["github"]
451451
description = SELECT_BUILD_STATUS[status]["description"]
452452
statuses_url = f"https://api.github.com/repos/{owner}/{repo}/statuses/{commit}"
453453

@@ -461,17 +461,19 @@ def send_build_status(self, build, commit, status):
461461
context = f'{settings.RTD_BUILD_STATUS_API_NAME}:{project.slug}'
462462

463463
data = {
464-
'state': github_build_state,
465-
'target_url': target_url,
466-
'description': description,
467-
'context': context,
464+
"state": github_build_status,
465+
"target_url": target_url,
466+
"description": description,
467+
"context": context,
468468
}
469469

470470
log.bind(
471471
project_slug=project.slug,
472-
commit_status=github_build_state,
472+
commit_status=github_build_status,
473473
user_username=self.user.username,
474474
statuses_url=statuses_url,
475+
target_url=target_url,
476+
status=status,
475477
)
476478
resp = None
477479
try:

readthedocs/projects/tasks/builds.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -422,13 +422,15 @@ def before_start(self, task_id, args, kwargs):
422422
def _reset_build(self):
423423
# Reset build only if it has some commands already.
424424
if self.data.build.get("commands"):
425-
log.info("Reseting build.")
425+
log.info("Resetting build.")
426426
api_v2.build(self.data.build["id"]).reset.post()
427427

428428
def on_failure(self, exc, task_id, args, kwargs, einfo):
429429
"""
430430
Celery handler to be executed when a task fails.
431431
432+
Updates build data, adds tasks to send build notifications.
433+
432434
.. note::
433435
434436
Since the task has failed, some attributes from the `self.data`
@@ -520,7 +522,7 @@ def on_failure(self, exc, task_id, args, kwargs, einfo):
520522
)
521523

522524
# Update build object
523-
self.data.build['success'] = False
525+
self.data.build["success"] = False
524526

525527
def get_valid_artifact_types(self):
526528
"""

readthedocs/rtd_tests/tests/test_api.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,14 @@ def test_response_building(self):
208208
'version_slug': version.slug,
209209
},
210210
)
211+
211212
build = resp.data
212-
self.assertEqual(build['state'], 'cloning')
213-
self.assertEqual(build['error'], '')
214-
self.assertEqual(build['exit_code'], 0)
215-
self.assertEqual(build['success'], True)
216-
self.assertEqual(build['docs_url'], dashboard_url)
213+
self.assertEqual(build["state"], "cloning")
214+
self.assertEqual(build["error"], "")
215+
self.assertEqual(build["exit_code"], 0)
216+
self.assertEqual(build["success"], True)
217+
self.assertTrue(build["docs_url"].endswith(dashboard_url))
218+
self.assertTrue(build["docs_url"].startswith("https://"))
217219

218220
@override_settings(DOCROOT="/home/docs/checkouts/readthedocs.org/user_builds")
219221
def test_response_finished_and_success(self):
@@ -299,11 +301,12 @@ def test_response_finished_and_fail(self):
299301
},
300302
)
301303
build = resp.data
302-
self.assertEqual(build['state'], 'finished')
303-
self.assertEqual(build['error'], '')
304-
self.assertEqual(build['exit_code'], 1)
305-
self.assertEqual(build['success'], False)
306-
self.assertEqual(build['docs_url'], dashboard_url)
304+
self.assertEqual(build["state"], "finished")
305+
self.assertEqual(build["error"], "")
306+
self.assertEqual(build["exit_code"], 1)
307+
self.assertEqual(build["success"], False)
308+
self.assertTrue(build["docs_url"].endswith(dashboard_url))
309+
self.assertTrue(build["docs_url"].startswith("https://"))
307310

308311
def test_make_build_without_permission(self):
309312
"""Ensure anonymous/non-staff users cannot write the build endpoint."""

readthedocs/rtd_tests/tests/test_oauth.py

+2
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,8 @@ def test_send_build_status_value_error(self, session, mock_logger):
350350
commit_status='success',
351351
user_username=self.user.username,
352352
statuses_url='https://api.github.com/repos/pypa/pip/statuses/1234',
353+
target_url=mock.ANY,
354+
status="success",
353355
)
354356
mock_logger.exception.assert_called_with(
355357
'GitHub commit status creation failed for project.',

0 commit comments

Comments
 (0)