Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build: Bug in target_url, failure to add "success" status if no external version exists #10369

Merged
merged 5 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion readthedocs/builds/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _

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

# General Build Statuses
# General build statuses, i.e. the status that is reported back to the
# user on a Git Provider. This not the same as BUILD_STATE which the internal
# representation.
BUILD_STATUS_FAILURE = 'failed'
BUILD_STATUS_PENDING = 'pending'
BUILD_STATUS_SUCCESS = 'success'
Expand Down
29 changes: 22 additions & 7 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,14 +377,29 @@ def commit_name(self):
return self.identifier

def get_absolute_url(self):
"""Get absolute url to the docs of the version."""
"""
Get the absolute URL to the docs of the version.

If the version doesn't have a successfully uploaded build, then we return the project's
dashboard page.

Because documentation projects can be hosted on separate domains, this function ALWAYS
returns with a full "http(s)://<domain>/" prefix.
"""
if not self.built and not self.uploaded:
return reverse(
'project_version_detail',
kwargs={
'project_slug': self.project.slug,
'version_slug': self.slug,
},
# TODO: Stop assuming protocol based on settings.DEBUG
# (this pattern is also used in builds.tasks for sending emails)
protocol = "http" if settings.DEBUG else "https"
return "{}://{}{}".format(
protocol,
settings.PRODUCTION_DOMAIN,
reverse(
humitos marked this conversation as resolved.
Show resolved Hide resolved
"project_version_detail",
kwargs={
"project_slug": self.project.slug,
"version_slug": self.slug,
},
),
)
external = self.type == EXTERNAL
return self.project.get_docs_url(
Expand Down
16 changes: 9 additions & 7 deletions readthedocs/oauth/services/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,8 @@ def send_build_status(self, build, commit, status):
project = build.project
owner, repo = build_utils.get_github_username_repo(url=project.repo)

# select the correct state and description.
github_build_state = SELECT_BUILD_STATUS[status]["github"]
# select the correct status and description.
github_build_status = SELECT_BUILD_STATUS[status]["github"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that the rename of the state to status helps here. We call it status in some places and GitHub calls it state. Probably makes sense to keep consistency in our code, tho 👍🏼

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed this because this is definitely about status. GitHub also calls it status AFAICT? https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are sending state in the data we send to them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we're sending the status :) I know that it's meaningless to talk about a real difference between these two words, but I think we only use "state" to refer to "build state" and everything else is "status".

I think that clearly when it's called SELECT_BUILD_STATUS[status]["github"], it is correct to call the variable github_build_status and it did make me less confused after I renamed it :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, my brain thinks about "GitHub commit status" - but I can see they called a field in the API "state", perhaps it was to avoid mixing it up with HTTP status. I think we should try to simplify our terminology rule to saying that we have "build state" for the Build objects internal naming of states - and then we have status for everything else. And then imagining a better future, we do a refactor to stop calling it "Build state" and call that "Build status" as well, so everything is "status" and we don't have "state" suddenly pop up and cause confusion :)

description = SELECT_BUILD_STATUS[status]["description"]
statuses_url = f"https://api.github.com/repos/{owner}/{repo}/statuses/{commit}"

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

data = {
'state': github_build_state,
'target_url': target_url,
'description': description,
'context': context,
"state": github_build_status,
"target_url": target_url,
"description": description,
"context": context,
}

log.bind(
project_slug=project.slug,
commit_status=github_build_state,
commit_status=github_build_status,
user_username=self.user.username,
statuses_url=statuses_url,
target_url=target_url,
status=status,
)
resp = None
try:
Expand Down
6 changes: 4 additions & 2 deletions readthedocs/projects/tasks/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,15 @@ def before_start(self, task_id, args, kwargs):
def _reset_build(self):
# Reset build only if it has some commands already.
if self.data.build.get("commands"):
log.info("Reseting build.")
log.info("Resetting build.")
api_v2.build(self.data.build["id"]).reset.post()

def on_failure(self, exc, task_id, args, kwargs, einfo):
"""
Celery handler to be executed when a task fails.

Updates build data, adds tasks to send build notifications.

.. note::

Since the task has failed, some attributes from the `self.data`
Expand Down Expand Up @@ -520,7 +522,7 @@ def on_failure(self, exc, task_id, args, kwargs, einfo):
)

# Update build object
self.data.build['success'] = False
self.data.build["success"] = False

def get_valid_artifact_types(self):
"""
Expand Down
23 changes: 13 additions & 10 deletions readthedocs/rtd_tests/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,14 @@ def test_response_building(self):
'version_slug': version.slug,
},
)

build = resp.data
self.assertEqual(build['state'], 'cloning')
self.assertEqual(build['error'], '')
self.assertEqual(build['exit_code'], 0)
self.assertEqual(build['success'], True)
self.assertEqual(build['docs_url'], dashboard_url)
self.assertEqual(build["state"], "cloning")
self.assertEqual(build["error"], "")
self.assertEqual(build["exit_code"], 0)
self.assertEqual(build["success"], True)
self.assertTrue(build["docs_url"].endswith(dashboard_url))
self.assertTrue(build["docs_url"].startswith("https://"))

@override_settings(DOCROOT="/home/docs/checkouts/readthedocs.org/user_builds")
def test_response_finished_and_success(self):
Expand Down Expand Up @@ -390,11 +392,12 @@ def test_response_finished_and_fail(self):
},
)
build = resp.data
self.assertEqual(build['state'], 'finished')
self.assertEqual(build['error'], '')
self.assertEqual(build['exit_code'], 1)
self.assertEqual(build['success'], False)
self.assertEqual(build['docs_url'], dashboard_url)
self.assertEqual(build["state"], "finished")
self.assertEqual(build["error"], "")
self.assertEqual(build["exit_code"], 1)
self.assertEqual(build["success"], False)
self.assertTrue(build["docs_url"].endswith(dashboard_url))
self.assertTrue(build["docs_url"].startswith("https://"))

def test_make_build_without_permission(self):
"""Ensure anonymous/non-staff users cannot write the build endpoint."""
Expand Down
2 changes: 2 additions & 0 deletions readthedocs/rtd_tests/tests/test_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ def test_send_build_status_value_error(self, session, mock_logger):
commit_status='success',
user_username=self.user.username,
statuses_url='https://api.github.com/repos/pypa/pip/statuses/1234',
target_url=mock.ANY,
status="success",
)
mock_logger.exception.assert_called_with(
'GitHub commit status creation failed for project.',
Expand Down