Skip to content

Commit 827c9d2

Browse files
committed
External versions: delete after 3 months of being merged/closed
Currently we are deleting external versions when they are closed, but their docs remain in storage. With this change we mark them as inactive instead. Later a task will delete all external versions that are inactive and their last activity was from 3 months ago. Before being deleted the status is updated to link to the build page instead (the build will be deleted currently, but I'm changing that in another PR). Ref #7674
1 parent af3bf8c commit 827c9d2

File tree

14 files changed

+195
-64
lines changed

14 files changed

+195
-64
lines changed

docs/guides/autobuild-docs-for-pull-requests.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ Searches will default to the default experience for your tool.
5858
This is a feature we plan to add,
5959
but don't want to overwhelm our search indexes used in production.
6060

61+
The documentation will be keep only for 90 days after the pull/merge request has been closed or merged.
62+
6163
Troubleshooting
6264
---------------
6365

readthedocs/api/v2/views/integrations.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@
2121
)
2222
from readthedocs.core.views.hooks import (
2323
build_branches,
24-
trigger_sync_versions,
25-
get_or_create_external_version,
26-
delete_external_version,
2724
build_external_version,
25+
deactivate_external_version,
26+
get_or_create_external_version,
27+
trigger_sync_versions,
2828
)
2929
from readthedocs.integrations.models import HttpExchange, Integration
30-
from readthedocs.projects.models import Project, Feature
31-
30+
from readthedocs.projects.models import Feature, Project
3231

3332
log = logging.getLogger(__name__)
3433

@@ -243,14 +242,14 @@ def get_external_version_response(self, project):
243242
'versions': [to_build],
244243
}
245244

246-
def get_delete_external_version_response(self, project):
245+
def get_deactivated_external_version_response(self, project):
247246
"""
248-
Delete External version on pull/merge request `closed` events and return API response.
247+
Deactivate the external version on pull/merge request closed events and return the API response.
249248
250249
Return a JSON response with the following::
251250
252251
{
253-
"version_deleted": true,
252+
"version_deactivated": true,
254253
"project": "project_name",
255254
"versions": [verbose_name]
256255
}
@@ -259,14 +258,13 @@ def get_delete_external_version_response(self, project):
259258
:type project: Project
260259
"""
261260
identifier, verbose_name = self.get_external_version_data()
262-
# Delete external version
263-
deleted_version = delete_external_version(
261+
deactivated_version = deactivate_external_version(
264262
project, identifier, verbose_name
265263
)
266264
return {
267-
'version_deleted': deleted_version is not None,
265+
'version_deactivated': bool(deactivated_version),
268266
'project': project.slug,
269-
'versions': [deleted_version],
267+
'versions': [deactivated_version] if deactivated_version else [],
270268
}
271269

272270

@@ -430,7 +428,7 @@ def handle_webhook(self):
430428

431429
if action == GITHUB_PULL_REQUEST_CLOSED:
432430
# Delete external version when PR is closed
433-
return self.get_delete_external_version_response(self.project)
431+
return self.get_deactivated_external_version_response(self.project)
434432

435433
# Sync versions when push event is created/deleted action
436434
if all([
@@ -589,7 +587,7 @@ def handle_webhook(self):
589587

590588
if action in [GITLAB_MERGE_REQUEST_CLOSE, GITLAB_MERGE_REQUEST_MERGE]:
591589
# Handle merge and close merge_request event.
592-
return self.get_delete_external_version_response(self.project)
590+
return self.get_deactivated_external_version_response(self.project)
593591
return None
594592

595593
def _normalize_ref(self, ref):

readthedocs/builds/models.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,7 @@ def get_absolute_url(self):
345345
def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
346346
from readthedocs.projects import tasks
347347
log.info('Removing files for version %s', self.slug)
348-
# Remove resources if the version is not external
349-
if self.type != EXTERNAL:
350-
tasks.clean_project_resources(self.project, self)
348+
tasks.clean_project_resources(self.project, self)
351349
super().delete(*args, **kwargs)
352350

353351
@property

readthedocs/builds/tasks.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
from django.core.files.storage import get_storage_class
99

1010
from readthedocs.api.v2.serializers import BuildSerializer
11-
from readthedocs.builds.constants import MAX_BUILD_COMMAND_SIZE
11+
from readthedocs.builds.constants import (
12+
BUILD_STATUS_FAILURE,
13+
BUILD_STATUS_PENDING,
14+
BUILD_STATUS_SUCCESS,
15+
MAX_BUILD_COMMAND_SIZE,
16+
)
1217
from readthedocs.builds.models import Build, Version
1318
from readthedocs.builds.utils import memcache_lock
19+
from readthedocs.projects.tasks import send_build_status
1420
from readthedocs.worker import app
1521

1622
log = logging.getLogger(__name__)
@@ -163,3 +169,40 @@ def archive_builds_task(days=14, limit=200, include_cold=False, delete=False):
163169
build.commands.all().delete()
164170
except IOError:
165171
log.exception('Cold Storage save failure')
172+
173+
174+
def delete_inactive_external_versions(limit=200, days=30 * 3):
175+
"""
176+
Delete external versions that have been marked as inactive after ``days``.
177+
178+
The commit status is updated to link to the build page, as the docs are removed.
179+
"""
180+
days_ago = datetime.now() - timedelta(days=days)
181+
queryset = Version.external.filter(
182+
active=False,
183+
modified__lte=days_ago,
184+
)[:limit]
185+
for version in queryset:
186+
try:
187+
last_build = version.last_build
188+
if last_build:
189+
status = BUILD_STATUS_PENDING
190+
if last_build.finished:
191+
status = BUILD_STATUS_SUCCESS if last_build.success else BUILD_STATUS_FAILURE
192+
send_build_status(
193+
build_pk=last_build.pk,
194+
commit=last_build.commit,
195+
status=status,
196+
link_to_build=True,
197+
)
198+
except Exception:
199+
log.exception(
200+
"Failed to send status: project=%s version=%s",
201+
version.project.slug, version.slug,
202+
)
203+
else:
204+
log.info(
205+
"Removing external version. project=%s version=%s",
206+
version.project.slug, version.slug,
207+
)
208+
version.delete()

readthedocs/builds/tests/__init__.py

Whitespace-only changes.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from datetime import datetime, timedelta
2+
3+
from django.test import TestCase
4+
from django_dynamic_fixture import get
5+
6+
from readthedocs.builds.constants import BRANCH, EXTERNAL, TAG
7+
from readthedocs.builds.models import Version
8+
from readthedocs.builds.tasks import delete_inactive_external_versions
9+
from readthedocs.projects.models import Project
10+
11+
12+
class TestTasks(TestCase):
13+
14+
def test_delete_inactive_external_versions(self):
15+
project = get(Project)
16+
project.versions.all().delete()
17+
get(
18+
Version,
19+
project=project,
20+
slug='branch',
21+
type=BRANCH,
22+
active=False,
23+
modified=datetime.now() - timedelta(days=7),
24+
)
25+
get(
26+
Version,
27+
project=project,
28+
slug='tag',
29+
type=TAG,
30+
active=True,
31+
modified=datetime.now() - timedelta(days=7),
32+
)
33+
get(
34+
Version,
35+
project=project,
36+
slug='external-active',
37+
type=EXTERNAL,
38+
active=True,
39+
modified=datetime.now() - timedelta(days=7),
40+
)
41+
get(
42+
Version,
43+
project=project,
44+
slug='external-inactive',
45+
type=EXTERNAL,
46+
active=False,
47+
modified=datetime.now() - timedelta(days=3),
48+
)
49+
get(
50+
Version,
51+
project=project,
52+
slug='external-inactive-old',
53+
type=EXTERNAL,
54+
active=False,
55+
modified=datetime.now() - timedelta(days=7),
56+
)
57+
58+
self.assertEqual(Version.objects.all().count(), 5)
59+
self.assertEqual(Version.external.all().count(), 3)
60+
61+
# We don't have inactive external versions from 9 days ago.
62+
delete_inactive_external_versions(days=9)
63+
self.assertEqual(Version.objects.all().count(), 5)
64+
self.assertEqual(Version.external.all().count(), 3)
65+
66+
# We have one inactive external versions from 6 days ago.
67+
delete_inactive_external_versions(days=6)
68+
self.assertEqual(Version.objects.all().count(), 4)
69+
self.assertEqual(Version.external.all().count(), 2)
70+
self.assertFalse(Version.objects.filter(slug='external-inactive-old').exists())

readthedocs/core/views/hooks.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44

55
from readthedocs.builds.constants import EXTERNAL
66
from readthedocs.core.utils import trigger_build
7-
from readthedocs.projects.models import Project, Feature
7+
from readthedocs.projects.models import Feature, Project
88
from readthedocs.projects.tasks import sync_repository_task
99

10-
1110
log = logging.getLogger(__name__)
1211

1312

@@ -139,46 +138,49 @@ def get_or_create_external_version(project, identifier, verbose_name):
139138

140139
if created:
141140
log.info(
142-
'(Create External Version) Added Version: [%s] ',
143-
external_version.slug
141+
'External version created. project=%s version=%s',
142+
project.slug, external_version.slug,
144143
)
145144
else:
146-
# identifier will change if there is a new commit to the Pull/Merge Request
147-
if external_version.identifier != identifier:
148-
external_version.identifier = identifier
149-
external_version.save()
145+
# Identifier will change if there is a new commit to the Pull/Merge Request.
146+
external_version.identifier = identifier
147+
# If the PR was previously closed it was marked as inactive.
148+
external_version.active = True
149+
external_version.save()
150150

151-
log.info(
152-
'(Update External Version) Updated Version: [%s] ',
153-
external_version.slug
154-
)
151+
log.info(
152+
'External version updated: project=%s version=%s',
153+
project.slug, external_version.slug,
154+
)
155155
return external_version
156156

157157

158-
def delete_external_version(project, identifier, verbose_name):
158+
def deactivate_external_version(project, identifier, verbose_name):
159159
"""
160-
Delete external versions using `identifier` and `verbose_name`.
160+
Deactivate external versions using `identifier` and `verbose_name`.
161161
162162
if external version does not exist then returns `None`.
163163
164+
We mark the version as inactive,
165+
so another celery task will remove it after some days.
166+
164167
:param project: Project instance
165168
:param identifier: Commit Hash
166169
:param verbose_name: pull/merge request number
167-
:returns: verbose_name (pull/merge request number).
170+
:returns: verbose_name (pull/merge request number).
168171
:rtype: str
169172
"""
170173
external_version = project.versions(manager=EXTERNAL).filter(
171174
verbose_name=verbose_name, identifier=identifier
172175
).first()
173176

174177
if external_version:
175-
# Delete External Version
176-
external_version.delete()
178+
external_version.active = False
179+
external_version.save()
177180
log.info(
178-
'(Delete External Version) Deleted Version: [%s]',
179-
external_version.slug
181+
'External version marked as inactive. project=%s version=%s',
182+
project.slug, external_version.slug,
180183
)
181-
182184
return external_version.verbose_name
183185
return None
184186

readthedocs/oauth/services/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def update_webhook(self, project, integration):
297297
"""
298298
raise NotImplementedError
299299

300-
def send_build_status(self, build, commit, state):
300+
def send_build_status(self, build, commit, state, link_to_build=False):
301301
"""
302302
Create commit status for project.
303303
@@ -307,6 +307,7 @@ def send_build_status(self, build, commit, state):
307307
:type commit: str
308308
:param state: build state failure, pending, or success.
309309
:type state: str
310+
:param link_to_build: If true, link to the build page regardless the state.
310311
:returns: boolean based on commit status creation was successful or not.
311312
:rtype: Bool
312313
"""

readthedocs/oauth/services/github.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def update_webhook(self, project, integration):
420420
integration.remove_secret()
421421
return (False, resp)
422422

423-
def send_build_status(self, build, commit, state):
423+
def send_build_status(self, build, commit, state, link_to_build=False):
424424
"""
425425
Create GitHub commit status for project.
426426
@@ -430,6 +430,7 @@ def send_build_status(self, build, commit, state):
430430
:type state: str
431431
:param commit: commit sha of the pull request
432432
:type commit: str
433+
:param link_to_build: If true, link to the build page regardless the state.
433434
:returns: boolean based on commit status creation was successful or not.
434435
:rtype: Bool
435436
"""
@@ -443,7 +444,7 @@ def send_build_status(self, build, commit, state):
443444

444445
target_url = build.get_full_url()
445446

446-
if state == BUILD_STATUS_SUCCESS:
447+
if not link_to_build and state == BUILD_STATUS_SUCCESS:
447448
target_url = build.version.get_absolute_url()
448449

449450
context = f'{settings.RTD_BUILD_STATUS_API_NAME}:{project.slug}'

readthedocs/oauth/services/gitlab.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ def update_webhook(self, project, integration):
508508
integration.remove_secret()
509509
return (False, resp)
510510

511-
def send_build_status(self, build, commit, state):
511+
def send_build_status(self, build, commit, state, link_to_build=False):
512512
"""
513513
Create GitLab commit status for project.
514514
@@ -518,6 +518,7 @@ def send_build_status(self, build, commit, state):
518518
:type state: str
519519
:param commit: commit sha of the pull request
520520
:type commit: str
521+
:param link_to_build: If true, link to the build page regardless the state.
521522
:returns: boolean based on commit status creation was successful or not.
522523
:rtype: Bool
523524
"""
@@ -536,7 +537,7 @@ def send_build_status(self, build, commit, state):
536537

537538
target_url = build.get_full_url()
538539

539-
if state == BUILD_STATUS_SUCCESS:
540+
if not link_to_build and state == BUILD_STATUS_SUCCESS:
540541
target_url = build.version.get_absolute_url()
541542

542543
context = f'{settings.RTD_BUILD_STATUS_API_NAME}:{project.slug}'

readthedocs/projects/tasks.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,7 +1892,7 @@ def retry_domain_verification(domain_pk):
18921892

18931893

18941894
@app.task(queue='web')
1895-
def send_build_status(build_pk, commit, status):
1895+
def send_build_status(build_pk, commit, status, link_to_build=False):
18961896
"""
18971897
Send Build Status to Git Status API for project external versions.
18981898
@@ -1932,7 +1932,12 @@ def send_build_status(build_pk, commit, status):
19321932

19331933
if service is not None:
19341934
# Send status report using the API.
1935-
success = service.send_build_status(build, commit, status)
1935+
success = service.send_build_status(
1936+
build=build,
1937+
commit=commit,
1938+
state=status,
1939+
link_to_build=link_to_build,
1940+
)
19361941

19371942
if success:
19381943
log.info(

0 commit comments

Comments
 (0)