Skip to content

Commit 3efaa7b

Browse files
authored
Builds: restart build commands before a new build (#7999)
This avoids ending up with duplicate commands. This can be tested by manually triggering a retry in any part of the task. The user may still see duplicate commands, as our js always adds new commands to the end, but after a refresh the correct number of commands are shown.
1 parent 0b3d41b commit 3efaa7b

File tree

6 files changed

+93
-9
lines changed

6 files changed

+93
-9
lines changed

readthedocs/api/v2/views/model_views.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,17 @@ def retrieve(self, *args, **kwargs):
272272
)
273273
return Response(data)
274274

275+
@decorators.action(
276+
detail=True,
277+
permission_classes=[permissions.IsAdminUser],
278+
methods=['post'],
279+
)
280+
def reset(self, request, **kwargs):
281+
"""Reset the build so it can be re-used when re-trying."""
282+
instance = self.get_object()
283+
instance.reset()
284+
return Response(status=status.HTTP_204_NO_CONTENT)
285+
275286

276287
class BuildCommandViewSet(UserSelectViewSet):
277288
parser_classes = [JSONParser, MultiPartParser]

readthedocs/builds/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,24 @@ def external_version_name(self):
921921
def using_latest_config(self):
922922
return int(self.config.get('version', '1')) == LATEST_CONFIGURATION_VERSION
923923

924+
def reset(self):
925+
"""
926+
Reset the build so it can be re-used when re-trying.
927+
928+
Dates and states are usually overriden by the build,
929+
we care more about deleting the commands.
930+
"""
931+
self.state = BUILD_STATE_TRIGGERED
932+
self.status = ''
933+
self.success = True
934+
self.output = ''
935+
self.error = ''
936+
self.exit_code = None
937+
self.builder = ''
938+
self.cold_storage = False
939+
self.commands.all().delete()
940+
self.save()
941+
924942

925943
class BuildCommandResultMixin:
926944

readthedocs/projects/tasks.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
from readthedocs.projects.models import APIProject, Feature
7373
from readthedocs.search.utils import index_new_files, remove_indexed_files
7474
from readthedocs.sphinx_domains.models import SphinxDomain
75-
from readthedocs.storage import build_media_storage, build_environment_storage
75+
from readthedocs.storage import build_environment_storage, build_media_storage
7676
from readthedocs.vcs_support import utils as vcs_support_utils
7777
from readthedocs.worker import app
7878

@@ -670,6 +670,10 @@ def run_setup(self, record=True):
670670
671671
Return True if successful.
672672
"""
673+
# Reset build only if it has some commands already.
674+
if self.build.get('commands'):
675+
api_v2.build(self.build['id']).reset.post()
676+
673677
if settings.DOCKER_ENABLE:
674678
env_cls = DockerBuildEnvironment
675679
else:

readthedocs/rtd_tests/tests/test_api.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@
3737
GitLabWebhookView,
3838
)
3939
from readthedocs.api.v2.views.task_views import get_status_data
40-
from readthedocs.builds.constants import EXTERNAL, LATEST
40+
from readthedocs.builds.constants import (
41+
BUILD_STATE_CLONING,
42+
BUILD_STATE_TRIGGERED,
43+
BUILD_STATUS_DUPLICATED,
44+
EXTERNAL,
45+
LATEST,
46+
)
4147
from readthedocs.builds.models import Build, BuildCommandResult, Version
4248
from readthedocs.integrations.models import Integration
4349
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
@@ -55,6 +61,11 @@
5561
class APIBuildTests(TestCase):
5662
fixtures = ['eric.json', 'test_data.json']
5763

64+
def setUp(self):
65+
self.user = User.objects.get(username='eric')
66+
self.project = get(Project, users=[self.user])
67+
self.version = self.project.versions.get(slug=LATEST)
68+
5869
def test_make_build(self):
5970
"""Test that a superuser can use the API."""
6071
client = APIClient()
@@ -82,6 +93,47 @@ def test_make_build(self):
8293
self.assertEqual(build['output'], 'Test Output')
8394
self.assertEqual(build['state_display'], 'Cloning')
8495

96+
def test_reset_build(self):
97+
build = get(
98+
Build,
99+
project=self.project,
100+
version=self.version,
101+
state=BUILD_STATE_CLONING,
102+
status=BUILD_STATUS_DUPLICATED,
103+
success=False,
104+
output='Output',
105+
error='Error',
106+
exit_code=9,
107+
builder='Builder',
108+
cold_storage=True,
109+
)
110+
command = get(
111+
BuildCommandResult,
112+
build=build,
113+
)
114+
build.commands.add(command)
115+
116+
self.assertEqual(build.commands.count(), 1)
117+
118+
client = APIClient()
119+
client.force_login(self.user)
120+
r = client.post(reverse('build-reset', args=(build.pk,)))
121+
122+
self.assertEqual(r.status_code, 204)
123+
build.refresh_from_db()
124+
self.assertEqual(build.project, self.project)
125+
self.assertEqual(build.version, self.version)
126+
self.assertEqual(build.state, BUILD_STATE_TRIGGERED)
127+
self.assertEqual(build.status, '')
128+
self.assertTrue(build.success)
129+
self.assertEqual(build.output, '')
130+
self.assertEqual(build.error, '')
131+
self.assertIsNone(build.exit_code)
132+
self.assertEqual(build.builder, '')
133+
self.assertFalse(build.cold_storage)
134+
self.assertEqual(build.commands.count(), 0)
135+
136+
85137
def test_api_does_not_have_private_config_key_superuser(self):
86138
client = APIClient()
87139
client.login(username='super', password='test')

readthedocs/rtd_tests/tests/test_builds.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1-
# -*- coding: utf-8 -*-
21
import datetime
32
import os
4-
53
from unittest import mock
4+
5+
from allauth.socialaccount.models import SocialAccount
66
from django.contrib.auth.models import User
77
from django.test import TestCase
8-
from django_dynamic_fixture import fixture, get
98
from django.utils import timezone
10-
11-
from allauth.socialaccount.models import SocialAccount
9+
from django_dynamic_fixture import fixture, get
1210

1311
from readthedocs.builds.constants import (
1412
BRANCH,
1513
EXTERNAL,
14+
GENERIC_EXTERNAL_VERSION_NAME,
1615
GITHUB_EXTERNAL_VERSION_NAME,
1716
GITLAB_EXTERNAL_VERSION_NAME,
18-
GENERIC_EXTERNAL_VERSION_NAME
1917
)
2018
from readthedocs.builds.models import Build, Version
2119
from readthedocs.core.utils import trigger_build
@@ -325,7 +323,7 @@ def test_save_config_in_build_model(self, load_config, api_v2):
325323
task.run_setup()
326324
build_config = task.build['config']
327325
# For patch
328-
api_v2.build.assert_called_once()
326+
api_v2.build().patch.assert_called_once()
329327
assert build_config['version'] == '1'
330328
assert 'sphinx' in build_config
331329
assert build_config['doctype'] == 'sphinx'

readthedocs/rtd_tests/tests/test_privacy_urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ def setUp(self):
382382
}
383383
self.response_data = {
384384
'build-concurrent': {'status_code': 403},
385+
'build-reset': {'status_code': 403},
385386
'project-sync-versions': {'status_code': 403},
386387
'project-token': {'status_code': 403},
387388
'emailhook-list': {'status_code': 403},

0 commit comments

Comments
 (0)