Skip to content

Commit ded4fab

Browse files
committed
Config: deprecated notification for builds without config file
When we detect a build is built without a Read the Docs configuration file (`.readthedocs.yaml`) we show multiple notifications: - a static warning message in the build detail's page - a persistent on-site notification to all maintainers/admin of the project - send a weekly email (at most) This is the initial step to attempt making users to migrate to our config file v2, giving them a enough window to do this and avoid breaking their builds in the future. Closes #10348
1 parent c9884a0 commit ded4fab

File tree

7 files changed

+127
-13
lines changed

7 files changed

+127
-13
lines changed

readthedocs/builds/models.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -1069,10 +1069,20 @@ def can_rebuild(self):
10691069
def external_version_name(self):
10701070
return external_version_name(self)
10711071

1072-
def using_latest_config(self):
1073-
if self.config:
1074-
return int(self.config.get('version', '1')) == LATEST_CONFIGURATION_VERSION
1075-
return False
1072+
def deprecated_config_used(self):
1073+
"""
1074+
Check whether this particular build is using a deprecated config file.
1075+
1076+
When using v1 or not having a config file at all, it returns ``True``.
1077+
Returns ``False`` only when it has a config file and it is using v2.
1078+
1079+
Note we are using this to communicate deprecation of v1 file and not using a config file.
1080+
See https://github.com/readthedocs/readthedocs.org/issues/10342
1081+
"""
1082+
if not self.config:
1083+
return True
1084+
1085+
return int(self.config.get("version", "1")) != LATEST_CONFIGURATION_VERSION
10761086

10771087
def reset(self):
10781088
"""

readthedocs/projects/tasks/builds.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@
6666
from ..signals import before_vcs
6767
from .mixins import SyncRepositoryMixin
6868
from .search import fileify
69-
from .utils import BuildRequest, clean_build, send_external_build_status
69+
from .utils import (
70+
BuildRequest,
71+
clean_build,
72+
deprecated_config_file_used_notification,
73+
send_external_build_status,
74+
)
7075

7176
log = structlog.get_logger(__name__)
7277

@@ -679,6 +684,12 @@ def after_return(self, status, retval, task_id, args, kwargs, einfo):
679684
if self.data.build.get("state") not in BUILD_FINAL_STATES:
680685
build_state = BUILD_STATE_FINISHED
681686

687+
# Trigger a Celery task here to check if the build is using v1 or not a
688+
# config file at all to create a on-site/email notifications. Note we
689+
# can't create the notification from here since we don't have access to
690+
# the database from the builders.
691+
deprecated_config_file_used_notification.delay(self.data.build["id"])
692+
682693
self.update_build(build_state)
683694
self.save_build_data()
684695

readthedocs/projects/tasks/utils.py

+81
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
import structlog
55
from celery.worker.request import Request
6+
from django.core.cache import cache
67
from django.db.models import Q
78
from django.utils import timezone
89
from django.utils.translation import gettext_lazy as _
10+
from messages_extends.constants import WARNING_PERSISTENT
911

1012
from readthedocs.builds.constants import (
1113
BUILD_FINAL_STATES,
@@ -14,7 +16,11 @@
1416
)
1517
from readthedocs.builds.models import Build
1618
from readthedocs.builds.tasks import send_build_status
19+
from readthedocs.core.permissions import AdminPermission
1720
from readthedocs.core.utils.filesystem import safe_rmtree
21+
from readthedocs.notifications import Notification, SiteNotification
22+
from readthedocs.notifications.backends import EmailBackend
23+
from readthedocs.notifications.constants import REQUIREMENT
1824
from readthedocs.storage import build_media_storage
1925
from readthedocs.worker import app
2026

@@ -154,6 +160,81 @@ def send_external_build_status(version_type, build_pk, commit, status):
154160
send_build_status.delay(build_pk, commit, status)
155161

156162

163+
class DeprecatedConfigFileSiteNotification(SiteNotification):
164+
165+
failure_message = (
166+
"Your project '{{ object.slug }}' doesn't have a "
167+
'<a href="https://docs.readthedocs.io/en/stable/config-file/v2.html">.readthedocs.yaml</a> '
168+
"configuration file. "
169+
"This feature is <strong>deprecated and will be removed soon</strong>. "
170+
"Make sure to create one for your project to keep your builds working."
171+
)
172+
failure_level = WARNING_PERSISTENT
173+
174+
175+
class DeprecatedConfigFileEmailNotification(Notification):
176+
177+
app_templates = "projects"
178+
name = "deprecated_config_file_used"
179+
context_object_name = "project"
180+
subject = "Your project will start failing soon"
181+
level = REQUIREMENT
182+
183+
def send(self):
184+
"""Method overwritten to remove on-site backend."""
185+
backend = EmailBackend(self.request)
186+
backend.send(self)
187+
188+
189+
@app.task(queue="web")
190+
def deprecated_config_file_used_notification(build_pk):
191+
"""
192+
Create a notification about not using a config file for all the maintainers of the project.
193+
194+
This task is triggered by the build process to be executed on the webs,
195+
since we don't have access to the db from the build.
196+
"""
197+
build = Build.objects.filter(pk=build_pk).first()
198+
if not build or not build.deprecated_config_used:
199+
return
200+
201+
log.bind(
202+
build_pk=build_pk,
203+
project_slug=build.project.slug,
204+
)
205+
206+
users = AdminPermission.owners(build.project)
207+
log.bind(users=len(users))
208+
209+
log.info("Sending deprecation config file onsite notification.")
210+
for user in users:
211+
n = DeprecatedConfigFileSiteNotification(
212+
user=user,
213+
context_object=build.project,
214+
success=False,
215+
)
216+
n.send()
217+
218+
# Send email notifications only once a week
219+
cache_prefix = "deprecated-config-file-notification"
220+
cached = cache.get(f"{cache_prefix}-{build.project.slug}")
221+
if cached:
222+
log.info("Deprecation config file email sent recently. Skipping.")
223+
return
224+
225+
log.info("Sending deprecation config file email notification.")
226+
for user in users:
227+
n = DeprecatedConfigFileEmailNotification(
228+
user=user,
229+
context_object=build.project,
230+
)
231+
n.send()
232+
233+
# Cache this notification for a week
234+
# TODO: reduce this notification period to 3 days after having this deployed for some weeks
235+
cache.set(f"{cache_prefix}-{build.project.slug}", "sent", timeout=7 * 24 * 60 * 60)
236+
237+
157238
class BuildRequest(Request):
158239

159240
def on_timeout(self, soft, timeout):

readthedocs/rtd_tests/tests/test_builds.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def test_build_is_stale(self):
249249
self.assertTrue(build_two.is_stale)
250250
self.assertFalse(build_three.is_stale)
251251

252-
def test_using_latest_config(self):
252+
def test_deprecated_config_used(self):
253253
now = timezone.now()
254254

255255
build = get(
@@ -260,12 +260,12 @@ def test_using_latest_config(self):
260260
state='finished',
261261
)
262262

263-
self.assertFalse(build.using_latest_config())
263+
self.assertFalse(build.deprecated_config_used())
264264

265265
build.config = {'version': 2}
266266
build.save()
267267

268-
self.assertTrue(build.using_latest_config())
268+
self.assertTrue(build.deprecated_config_used())
269269

270270
def test_build_is_external(self):
271271
# Turn the build version to EXTERNAL type.

readthedocs/templates/builds/build_detail.html

+7-5
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,20 @@
161161
</p>
162162
</div>
163163
{% endif %}
164-
{% if build.finished and not build.using_latest_config %}
164+
165+
{# This message is not dynamic and only appears when loading the page after the build has finished #}
166+
{% if build.finished and build.deprecated_config_used %}
165167
<div class="build-ideas">
166168
<p>
167169
{% blocktrans trimmed with config_file_link="https://docs.readthedocs.io/page/config-file/v2.html" %}
168-
<strong>Configure your documentation builds!</strong>
169-
Adding a <a href="{{ config_file_link }}">.readthedocs.yaml</a> file to your project
170-
is the recommended way to configure your documentation builds.
171-
You can declare dependencies, set up submodules, and many other great features.
170+
<strong>Your builds will stop working soon!</strong><br/>
171+
Building without a configuration file (or using v1) is deprecated and will be removed soon.
172+
Add a <a href="{{ config_file_link }}">.readthedocs.yaml</a> config file to your project to keep your builds working.
172173
{% endblocktrans %}
173174
</p>
174175
</div>
175176
{% endif %}
177+
176178
{% endif %}
177179

178180
{% if build.finished and build.config.build.commands %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!-- TODO: Copy the content from the TXT version once we are happy with it -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% extends "core/email/common.txt" %}
2+
{% block content %}
3+
Your project "{{ project.slug }}" is not using a <a href="https://docs.readthedocs.io/en/stable/config-file/v2.html">.readthedocs.yaml</a> configuration file and will stop working soon.
4+
5+
We strongly recommend you to add a configuration file to keep your builds working.
6+
7+
Get in touch with us at {{ production_uri }}{% url 'support' %}
8+
and let us know if you are unable to migrate to a config file for any reason.
9+
{% endblock %}

0 commit comments

Comments
 (0)