Skip to content

Commit e3b5484

Browse files
authored
Config: deprecated notification for projects without config file (#10354)
* 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 * Test: invert logic * Notification's copy: feedback from review * It's a function * Notifications: use a scheduled Celery task to send them Instead of sending an onsite notification on each build, we use a scheduled Celery task that runs once a week to send them. It filter projects that are not using the v2 config file. * Feedback from the review * Darker failed on CircleCI because of this * Links pointing to blog post * Add more logging for this task * Space typo * Ignore projects that are potentially spam * Order queryset by PK so we can track it Also, add log for current project in case we need to recover from that task. * Improve query a little more * Make the query to work on .com as well * Query only active subscriptions on .com * Consistency on naming * Only check for `Project.default_version` * Log progress while iterating to know it's moving * Simplify versions query * More logging to the progress * Send only one notification per user The notification will include all the projects the user is admin that are affected by this deprecation. Users will receive at most one notification per week. * Modify email template to include all the projects and dates * Typo * Improve logging * Keep adding logging :) * Db query for active subscriptions on .com * Email subject * Update onsite notification message * Do not set emails just yet * Minor updates * Update emails with new dates
1 parent 524c24c commit e3b5484

File tree

7 files changed

+247
-14
lines changed

7 files changed

+247
-14
lines changed

readthedocs/builds/models.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -1084,10 +1084,20 @@ def can_rebuild(self):
10841084
def external_version_name(self):
10851085
return external_version_name(self)
10861086

1087-
def using_latest_config(self):
1088-
if self.config:
1089-
return int(self.config.get('version', '1')) == LATEST_CONFIGURATION_VERSION
1090-
return False
1087+
def deprecated_config_used(self):
1088+
"""
1089+
Check whether this particular build is using a deprecated config file.
1090+
1091+
When using v1 or not having a config file at all, it returns ``True``.
1092+
Returns ``False`` only when it has a config file and it is using v2.
1093+
1094+
Note we are using this to communicate deprecation of v1 file and not using a config file.
1095+
See https://github.com/readthedocs/readthedocs.org/issues/10342
1096+
"""
1097+
if not self.config:
1098+
return True
1099+
1100+
return int(self.config.get("version", "1")) != LATEST_CONFIGURATION_VERSION
10911101

10921102
def reset(self):
10931103
"""

readthedocs/projects/tasks/utils.py

+162-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33

44
import structlog
55
from celery.worker.request import Request
6-
from django.db.models import Q
6+
from django.conf import settings
7+
from django.contrib.auth.models import User
8+
from django.db.models import Q, Sum
79
from django.utils import timezone
810
from django.utils.translation import gettext_lazy as _
11+
from djstripe.enums import SubscriptionStatus
12+
from messages_extends.constants import WARNING_PERSISTENT
913

1014
from readthedocs.builds.constants import (
1115
BUILD_FINAL_STATES,
@@ -14,7 +18,12 @@
1418
)
1519
from readthedocs.builds.models import Build
1620
from readthedocs.builds.tasks import send_build_status
21+
from readthedocs.core.permissions import AdminPermission
1722
from readthedocs.core.utils.filesystem import safe_rmtree
23+
from readthedocs.notifications import Notification, SiteNotification
24+
from readthedocs.notifications.backends import EmailBackend
25+
from readthedocs.notifications.constants import REQUIREMENT
26+
from readthedocs.projects.models import Project
1827
from readthedocs.storage import build_media_storage
1928
from readthedocs.worker import app
2029

@@ -154,6 +163,158 @@ def send_external_build_status(version_type, build_pk, commit, status):
154163
send_build_status.delay(build_pk, commit, status)
155164

156165

166+
class DeprecatedConfigFileSiteNotification(SiteNotification):
167+
168+
# TODO: mention all the project slugs here
169+
# Maybe trim them to up to 5 projects to avoid sending a huge blob of text
170+
failure_message = _(
171+
'Your project(s) "{{ project_slugs }}" don\'t have a configuration file. '
172+
"Configuration files will <strong>soon be required</strong> by projects, "
173+
"and will no longer be optional. "
174+
'<a href="https://blog.readthedocs.com/migrate-configuration-v2/">Read our blog post to create one</a> ' # noqa
175+
"and ensure your project continues building successfully."
176+
)
177+
failure_level = WARNING_PERSISTENT
178+
179+
180+
class DeprecatedConfigFileEmailNotification(Notification):
181+
182+
app_templates = "projects"
183+
name = "deprecated_config_file_used"
184+
context_object_name = "project"
185+
subject = "[Action required] Add a configuration file to your project to prevent build failure"
186+
level = REQUIREMENT
187+
188+
def send(self):
189+
"""Method overwritten to remove on-site backend."""
190+
backend = EmailBackend(self.request)
191+
backend.send(self)
192+
193+
194+
@app.task(queue="web")
195+
def deprecated_config_file_used_notification():
196+
"""
197+
Create a notification about not using a config file for all the maintainers of the project.
198+
199+
This is a scheduled task to be executed on the webs.
200+
Note the code uses `.iterator` and `.only` to avoid killing the db with this query.
201+
Besdies, it excludes projects with enough spam score to be skipped.
202+
"""
203+
# Skip projects with a spam score bigger than this value.
204+
# Currently, this gives us ~250k in total (from ~550k we have in our database)
205+
spam_score = 300
206+
207+
projects = set()
208+
start_datetime = datetime.datetime.now()
209+
queryset = Project.objects.exclude(users__profile__banned=True)
210+
if settings.ALLOW_PRIVATE_REPOS:
211+
# Only send emails to active customers
212+
queryset = queryset.filter(
213+
organizations__stripe_subscription__status=SubscriptionStatus.active
214+
)
215+
else:
216+
# Take into account spam score on community
217+
queryset = queryset.annotate(spam_score=Sum("spam_rules__value")).filter(
218+
Q(spam_score__lt=spam_score) | Q(is_spam=False)
219+
)
220+
queryset = queryset.only("slug", "default_version").order_by("id")
221+
n_projects = queryset.count()
222+
223+
for i, project in enumerate(queryset.iterator()):
224+
if i % 500 == 0:
225+
log.info(
226+
"Finding projects without a configuration file.",
227+
progress=f"{i}/{n_projects}",
228+
current_project_pk=project.pk,
229+
current_project_slug=project.slug,
230+
projects_found=len(projects),
231+
time_elapsed=(datetime.datetime.now() - start_datetime).seconds,
232+
)
233+
234+
# Only check for the default version because if the project is using tags
235+
# they won't be able to update those and we will send them emails forever.
236+
# We can update this query if we consider later.
237+
version = (
238+
project.versions.filter(slug=project.default_version).only("id").first()
239+
)
240+
if version:
241+
build = (
242+
version.builds.filter(success=True)
243+
.only("_config")
244+
.order_by("-date")
245+
.first()
246+
)
247+
if build and build.deprecated_config_used():
248+
projects.add(project.slug)
249+
250+
# Store all the users we want to contact
251+
users = set()
252+
253+
n_projects = len(projects)
254+
queryset = Project.objects.filter(slug__in=projects).order_by("id")
255+
for i, project in enumerate(queryset.iterator()):
256+
if i % 500 == 0:
257+
log.info(
258+
"Querying all the users we want to contact.",
259+
progress=f"{i}/{n_projects}",
260+
current_project_pk=project.pk,
261+
current_project_slug=project.slug,
262+
users_found=len(users),
263+
time_elapsed=(datetime.datetime.now() - start_datetime).seconds,
264+
)
265+
266+
users.update(AdminPermission.owners(project).values_list("username", flat=True))
267+
268+
# Only send 1 email per user,
269+
# even if that user has multiple projects without a configuration file.
270+
# The notification will mention all the projects.
271+
queryset = User.objects.filter(username__in=users, profile__banned=False).order_by(
272+
"id"
273+
)
274+
n_users = queryset.count()
275+
for i, user in enumerate(queryset.iterator()):
276+
if i % 500 == 0:
277+
log.info(
278+
"Sending deprecated config file notification to users.",
279+
progress=f"{i}/{n_users}",
280+
current_user_pk=user.pk,
281+
current_user_username=user.username,
282+
time_elapsed=(datetime.datetime.now() - start_datetime).seconds,
283+
)
284+
285+
# All the projects for this user that don't have a configuration file
286+
user_projects = (
287+
AdminPermission.projects(user, admin=True)
288+
.filter(slug__in=projects)
289+
.only("slug")
290+
)
291+
292+
user_project_slugs = ", ".join([p.slug for p in user_projects[:5]])
293+
if user_projects.count() > 5:
294+
user_project_slugs += " and others..."
295+
296+
n_site = DeprecatedConfigFileSiteNotification(
297+
user=user,
298+
context_object=user_projects,
299+
extra_context={"project_slugs": user_project_slugs},
300+
success=False,
301+
)
302+
n_site.send()
303+
304+
# TODO: uncomment this code when we are ready to send email notifications
305+
# n_email = DeprecatedConfigFileEmailNotification(
306+
# user=user,
307+
# context_object=user_projects,
308+
# extra_context={"project_slugs": user_project_slugs},
309+
# )
310+
# n_email.send()
311+
312+
log.info(
313+
"Finish sending deprecated config file notifications.",
314+
time_elapsed=(datetime.datetime.now() - start_datetime).seconds,
315+
)
316+
317+
157318
class BuildRequest(Request):
158319

159320
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.assertTrue(build.deprecated_config_used())
264264

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

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

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

readthedocs/settings/base.py

+5
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,11 @@ def TEMPLATES(self):
532532
'schedule': crontab(minute='*/15'),
533533
'options': {'queue': 'web'},
534534
},
535+
'weekly-config-file-notification': {
536+
'task': 'readthedocs.projects.tasks.utils.deprecated_config_file_used_notification',
537+
'schedule': crontab(day_of_week='wednesday', hour=11, minute=15),
538+
'options': {'queue': 'web'},
539+
},
535540
}
536541

537542
MULTIPLE_BUILD_SERVERS = [CELERY_DEFAULT_QUEUE]

readthedocs/templates/builds/build_detail.html

+9-6
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,21 @@
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>
167-
{% 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.
169+
{% blocktrans trimmed with config_file_link="https://blog.readthedocs.com/migrate-configuration-v2/" %}
170+
<strong>Your builds will stop working soon!</strong><br/>
171+
Configuration files will <strong>soon be required</strong> by projects, and will no longer be optional.
172+
<a href="{{ config_file_link }}">Read our blog post to create one</a>
173+
and ensure your project continues building successfully.
172174
{% endblocktrans %}
173175
</p>
174176
</div>
175177
{% endif %}
178+
176179
{% endif %}
177180

178181
{% if build.finished and build.config.build.commands %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{% extends "core/email/common.html" %}
2+
{% block content %}
3+
The Read the Docs build system will start requiring a configuration file v2 (<code>.readthedocs.yaml</code>) starting on <strong>September 25, 2023</strong>.
4+
We are scheduling brownout days to provide extra reminders by failing build without a configuration file v2 during some hours before the final day.
5+
Keep these dates in mind to avoid unexpected behaviours:
6+
7+
<ul>
8+
<li><strong>Monday, July 24, 2023</strong>: Do the first brownout (temporarily enforce this deprecation) for 12 hours: 00:01 PST to 11:59 PST (noon)</li>
9+
<li><strong>Monday, August 14, 2023</strong>: Do a second brownout (temporarily enforce this deprecation) for 24 hours: 00:01 PST to 23:59 PST (midnight)</li>
10+
<li><strong>Monday, September 4, 2023</strong> Do a third and final brownout (temporarily enforce this deprecation) for 48 hours: 00:01 PST to 23:59 PST (midnight)</li>
11+
<li><strong>Monday, September 25, 2023</strong>: Fully remove support for building documentation without configuration file v2.</li>
12+
</ul>
13+
14+
We have identified the following projects where you are admin are impacted by this deprecation:
15+
16+
<ul>
17+
{% for project in object %}
18+
<li>{{ project.slug }}</li>
19+
{% endfor %}
20+
</ul>
21+
22+
You require to add a configuration file to your projects to ensure they continues building successfully and stop receiving these notifications.
23+
24+
For more information on how to create a required configuration file,
25+
<a href="https://blog.readthedocs.com/migrate-configuration-v2/">read our blog post</a>
26+
27+
Get in touch with us <a href="{{ production_uri }}{% url 'support' %}">via our support</a>
28+
and let us know if you are unable to use a configuration file for any reason.
29+
{% endblock %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% extends "core/email/common.txt" %}
2+
{% block content %}
3+
The Read the Docs build system will start requiring a configuration file v2 (.readthedocs.yaml) starting on September 25, 2023.
4+
We are scheduling brownout days to provide extra reminders by failing build without a configuration file v2 during some hours before the final day.
5+
Keep these dates in mind to avoid unexpected behaviours:
6+
7+
* Monday, July 24, 2023: Do the first brownout (temporarily enforce this deprecation) for 12 hours: 00:01 PST to 11:59 PST (noon)
8+
* Monday, August 14, 2023: Do a second brownout (temporarily enforce this deprecation) for 24 hours: 00:01 PST to 23:59 PST (midnight)
9+
* Monday, September 4, 2023: Do a third and final brownout (temporarily enforce this deprecation) for 48 hours: 00:01 PST to 23:59 PST (midnight)
10+
* Monday, September 25, 2023: Fully remove support for building documentation without configuration file v2.
11+
12+
We have identified the following projects where you are admin are impacted by this deprecation:
13+
14+
{% for project in object %}
15+
* {{ project.slug }}
16+
{% endfor %}
17+
18+
You require to add a configuration file to your projects to ensure they continues building successfully and stop receiving these notifications.
19+
20+
For more information on how to create a required configuration file, see:
21+
https://blog.readthedocs.com/migrate-configuration-v2/
22+
23+
Get in touch with us at {{ production_uri }}{% url 'support' %}
24+
and let us know if you are unable to use a configuration file for any reason.
25+
{% endblock %}

0 commit comments

Comments
 (0)