Skip to content
Draft
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
30 changes: 29 additions & 1 deletion docs/user/api/v3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,21 @@ Build details
"success": true,
"error": null,
"commit": "6f808d743fd6f6907ad3e2e969c88a549e76db30",
"commit_url": "https://github.com/pypa/pip/commit/6f808d743fd6f6907ad3e2e969c88a549e76db30",
"docs_url": "https://pip.readthedocs.io/en/latest/",
"builder": "build-default-6fccbf5cb-xtzkc",
"commands": [
{
"id": 1,
"command": "git clone --depth 1 https://github.com/pypa/pip .",
"description": "Clone repository",
"output": "Cloning into '.'...",
"exit_code": 0,
"start_time": "2018-06-19T15:16:01+00:00",
"end_time": "2018-06-19T15:16:05+00:00",
"run_time": 4
}
],
"config": {
"version": "1",
"formats": [
Expand Down Expand Up @@ -791,9 +806,22 @@ Build details
:>json integer duration: The length of the build in seconds.
:>json string state: The state of the build (one of ``triggered``, ``building``, ``installing``, ``cloning``, ``finished`` or ``cancelled``)
:>json string error: An error message if the build was unsuccessful
:>json string commit_url: URL to the commit in the version control system
:>json string docs_url: URL to the built documentation for this build
:>json string builder: Identifier of the builder instance that executed this build (useful for debugging)
:>json array commands: List of build commands executed during the build process. Each command includes:

* ``id`` - Command identifier
* ``command`` - The actual command that was executed
* ``description`` - Human-readable description of the command
* ``output`` - Output from the command execution
* ``exit_code`` - Exit code of the command (0 for success)
* ``start_time`` - ISO-8601 datetime when the command started
* ``end_time`` - ISO-8601 datetime when the command finished
* ``run_time`` - Duration of the command in seconds

:query string expand: Add additional fields in the response.
Allowed value is ``config``.
Allowed values are ``config`` and ``notifications``.


Builds listing
Expand Down
49 changes: 48 additions & 1 deletion readthedocs/api/v3/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from readthedocs.builds.constants import LATEST
from readthedocs.builds.constants import STABLE
from readthedocs.builds.models import Build
from readthedocs.builds.models import BuildCommandResult
from readthedocs.builds.models import Version
from readthedocs.core.permissions import AdminPermission
from readthedocs.core.resolver import Resolver
Expand Down Expand Up @@ -143,6 +144,25 @@ def get_version(self, obj):
return None


class BuildCommandSerializer(serializers.ModelSerializer):
"""Serializer for BuildCommandResult objects."""

run_time = serializers.ReadOnlyField()

class Meta:
model = BuildCommandResult
fields = [
"id",
"command",
"description",
"output",
"exit_code",
"start_time",
"end_time",
"run_time",
]


class BuildConfigSerializer(FlexFieldsSerializerMixin, serializers.Serializer):
"""
Render ``Build.config`` property without modifying it.
Expand All @@ -168,6 +188,13 @@ def get_name(self, obj):


class BuildSerializer(FlexFieldsModelSerializer):
"""
Serializer for Build objects.

Includes build commands, documentation URL, commit URL, and builder information.
Supports expanding ``config`` and ``notifications`` via the ``?expand=`` query parameter.
"""

project = serializers.SlugRelatedField(slug_field="slug", read_only=True)
version = serializers.SlugRelatedField(slug_field="slug", read_only=True)
created = serializers.DateTimeField(source="date")
Expand All @@ -177,6 +204,10 @@ class BuildSerializer(FlexFieldsModelSerializer):
state = BuildStateSerializer(source="*")
_links = BuildLinksSerializer(source="*")
urls = BuildURLsSerializer(source="*")
commands = BuildCommandSerializer(many=True, read_only=True)
docs_url = serializers.SerializerMethodField()
commit_url = serializers.ReadOnlyField(source="get_commit_url")
builder = serializers.CharField(read_only=True)

class Meta:
model = Build
Expand All @@ -191,11 +222,21 @@ class Meta:
"success",
"error",
"commit",
"commit_url",
"docs_url",
"builder",
"commands",
"_links",
"urls",
]

expandable_fields = {"config": (BuildConfigSerializer,)}
expandable_fields = {
"config": (BuildConfigSerializer,),
"notifications": (
"readthedocs.api.v3.serializers.NotificationSerializer",
{"many": True},
),
}

def get_finished(self, obj):
if obj.date and obj.length:
Expand All @@ -212,6 +253,12 @@ def get_success(self, obj):

return None

def get_docs_url(self, obj):
"""Return the URL to the documentation built by this build."""
if obj.version:
return obj.version.get_absolute_url()
return None


class NotificationMessageSerializer(serializers.Serializer):
id = serializers.SlugField()
Expand Down
14 changes: 13 additions & 1 deletion readthedocs/api/v3/tests/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from rest_framework.test import APIClient

from readthedocs.builds.constants import LATEST, TAG
from readthedocs.builds.models import Build, Version
from readthedocs.builds.models import Build, BuildCommandResult, Version
from readthedocs.core.notifications import MESSAGE_EMAIL_VALIDATION_PENDING
from readthedocs.doc_builder.exceptions import BuildCancelled
from readthedocs.notifications.models import Notification
Expand Down Expand Up @@ -112,6 +112,18 @@ def setUp(self):
length=60,
)

# Create some build commands for testing
self.build_command = fixture.get(
BuildCommandResult,
build=self.build,
command="python setup.py install",
description="Install",
output="Successfully installed",
exit_code=0,
start_time=self.created,
end_time=self.created + datetime.timedelta(seconds=5),
)

self.other = fixture.get(User, projects=[])
self.others_token = fixture.get(Token, key="other", user=self.other)
self.others_project = fixture.get(
Expand Down
15 changes: 15 additions & 0 deletions readthedocs/api/v3/tests/responses/projects-builds-detail.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
{
"commit": "a1b2c3",
"commit_url": "",
"created": "2019-04-29T10:00:00Z",
"docs_url": "http://project.readthedocs.io/en/v1.0/",
"duration": 60,
"error": "",
"finished": "2019-04-29T10:01:00Z",
"id": 1,
"builder": "builder01",
"commands": [
{
"id": 1,
"command": "python setup.py install",
"description": "Install",
"output": "Successfully installed",
"exit_code": 0,
"start_time": "2019-04-29T10:00:00Z",
"end_time": "2019-04-29T10:00:05Z",
"run_time": 5
}
],
"_links": {
"_self": "https://readthedocs.org/api/v3/projects/project/builds/1/",
"project": "https://readthedocs.org/api/v3/projects/project/",
Expand Down
15 changes: 15 additions & 0 deletions readthedocs/api/v3/tests/responses/projects-builds-list.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@
"results": [
{
"commit": "a1b2c3",
"commit_url": "",
"created": "2019-04-29T10:00:00Z",
"docs_url": "http://project.readthedocs.io/en/v1.0/",
"duration": 60,
"error": "",
"finished": "2019-04-29T10:01:00Z",
"id": 1,
"builder": "builder01",
"commands": [
{
"id": 1,
"command": "python setup.py install",
"description": "Install",
"output": "Successfully installed",
"exit_code": 0,
"start_time": "2019-04-29T10:00:00Z",
"end_time": "2019-04-29T10:00:05Z",
"run_time": 5
}
],
"_links": {
"_self": "https://readthedocs.org/api/v3/projects/project/builds/1/",
"project": "https://readthedocs.org/api/v3/projects/project/",
Expand Down
76 changes: 76 additions & 0 deletions readthedocs/api/v3/tests/test_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,3 +675,79 @@ def test_projects_builds_notifitications_detail_post(self):
response = self.client.patch(url, data)
self.assertEqual(response.status_code, 204)
self.assertEqual(self.build.notifications.first().state, "read")

def test_projects_builds_detail_has_new_fields(self):
"""Test that build detail includes commands, docs_url, commit_url, and builder fields."""
url = reverse(
"projects-builds-detail",
kwargs={
"parent_lookup_project__slug": self.project.slug,
"build_pk": self.build.pk,
},
)

self.client.logout()
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")

response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()

# Check new fields exist
self.assertIn("commands", data)
self.assertIn("docs_url", data)
self.assertIn("commit_url", data)
self.assertIn("builder", data)

# Validate field values
self.assertEqual(data["builder"], "builder01")
self.assertEqual(data["docs_url"], "http://project.readthedocs.io/en/v1.0/")
self.assertEqual(data["commit_url"], "")
self.assertEqual(len(data["commands"]), 1)
self.assertEqual(data["commands"][0]["command"], "python setup.py install")

def test_projects_builds_detail_expand_config(self):
"""Test that build detail can expand config."""
url = reverse(
"projects-builds-detail",
kwargs={
"parent_lookup_project__slug": self.project.slug,
"build_pk": self.build.pk,
},
)
url += "?expand=config"

self.client.logout()
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")

response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()

# Check config is expanded
self.assertIn("config", data)
self.assertEqual(data["config"]["property"], "test value")

def test_projects_builds_detail_expand_notifications(self):
"""Test that build detail can expand notifications."""
url = reverse(
"projects-builds-detail",
kwargs={
"parent_lookup_project__slug": self.project.slug,
"build_pk": self.build.pk,
},
)
url += "?expand=notifications"

self.client.logout()
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")

response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()

# Check notifications are expanded
self.assertIn("notifications", data)
self.assertIsInstance(data["notifications"], list)
# We should have the notification created in the test setup
self.assertEqual(len(data["notifications"]), 1)
1 change: 1 addition & 0 deletions readthedocs/api/v3/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ class BuildsViewSet(
permission_classes = [ReadOnlyPermission | (IsAuthenticated & IsProjectAdmin)]
permit_list_expands = [
"config",
"notifications",
]


Expand Down