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
41 changes: 41 additions & 0 deletions readthedocs/api/v2/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Utility functions that are used by both views and celery tasks."""

import itertools
import json
import re

import structlog
Expand All @@ -17,6 +18,7 @@
from readthedocs.builds.constants import TAG
from readthedocs.builds.models import RegexAutomationRule
from readthedocs.builds.models import Version
from readthedocs.storage import build_commands_storage


log = structlog.get_logger(__name__)
Expand Down Expand Up @@ -287,3 +289,42 @@ class RemoteProjectPagination(PageNumberPagination):
class ProjectPagination(PageNumberPagination):
page_size = 100
max_page_size = 1000


def get_commands_from_cold_storage(build_instance):
"""
Retrieve build commands from cold storage if available.

:param build_instance: Build instance to retrieve commands for
:returns: List of command dictionaries with normalized commands, or None if not available
"""
if not build_instance.cold_storage:
return None

storage_path = "{date}/{id}.json".format(
date=str(build_instance.date.date()),
id=build_instance.id,
)

if not build_commands_storage.exists(storage_path):
return None

try:
json_resp = build_commands_storage.open(storage_path).read()
commands = json.loads(json_resp)

# Normalize commands in the same way as when returning them using the serializer
for buildcommand in commands:
buildcommand["command"] = normalize_build_command(
buildcommand["command"],
build_instance.project.slug,
build_instance.get_version_slug(),
)

return commands
except Exception:
log.exception(
"Failed to read build data from storage.",
path=storage_path,
)
return None
30 changes: 7 additions & 23 deletions readthedocs/api/v2/views/model_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from ..serializers import SocialAccountSerializer
from ..serializers import VersionAdminSerializer
from ..serializers import VersionSerializer
from ..utils import get_commands_from_cold_storage
from ..utils import ProjectPagination
from ..utils import RemoteOrganizationPagination
from ..utils import RemoteProjectPagination
Expand Down Expand Up @@ -342,29 +343,12 @@ def retrieve(self, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
data = serializer.data
if instance.cold_storage:
storage_path = "{date}/{id}.json".format(
date=str(instance.date.date()),
id=instance.id,
)
if build_commands_storage.exists(storage_path):
try:
json_resp = build_commands_storage.open(storage_path).read()
data["commands"] = json.loads(json_resp)

# Normalize commands in the same way than when returning
# them using the serializer
for buildcommand in data["commands"]:
buildcommand["command"] = normalize_build_command(
buildcommand["command"],
instance.project.slug,
instance.get_version_slug(),
)
except Exception:
log.exception(
"Failed to read build data from storage.",
path=storage_path,
)

# Load commands from cold storage if available
commands_from_storage = get_commands_from_cold_storage(instance)
if commands_from_storage is not None:
data["commands"] = commands_from_storage

return Response(data)

@decorators.action(
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
Loading
Loading