Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build: fail builds without configuration file or using v1 #10355

Merged
merged 12 commits into from
Jun 14, 2023
18 changes: 14 additions & 4 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,10 +1069,20 @@ def can_rebuild(self):
def external_version_name(self):
return external_version_name(self)

def using_latest_config(self):
if self.config:
return int(self.config.get('version', '1')) == LATEST_CONFIGURATION_VERSION
return False
def deprecated_config_used(self):
"""
Check whether this particular build is using a deprecated config file.

When using v1 or not having a config file at all, it returns ``True``.
Returns ``False`` only when it has a config file and it is using v2.

Note we are using this to communicate deprecation of v1 file and not using a config file.
See https://github.com/readthedocs/readthedocs.org/issues/10342
"""
if not self.config:
return True

return int(self.config.get("version", "1")) != LATEST_CONFIGURATION_VERSION

def reset(self):
"""
Expand Down
6 changes: 6 additions & 0 deletions readthedocs/doc_builder/director.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ def checkout(self):
self.data.build["config"] = self.data.config.as_dict()
self.data.build["readthedocs_yaml_path"] = custom_config_file

# Raise a build error if the project is not using a config file or using v1
if self.data.project.has_feature(
Feature.NO_CONFIG_FILE_DEPRECATED
) and self.data.config.version not in ("2", 2):
raise BuildUserError(BuildUserError.NO_CONFIG_FILE_DEPRECATED)

if self.vcs_repository.supports_submodules:
self.vcs_repository.update_submodules(self.data.config)

Expand Down
5 changes: 5 additions & 0 deletions readthedocs/doc_builder/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class BuildUserError(BuildBaseException):
"Ensure your project is configured to use the output path "
"'$READTHEDOCS_OUTPUT/html' instead."
)
NO_CONFIG_FILE_DEPRECATED = gettext_noop(
"Not using a '.readthedocs.yaml' configuration file is deprecated. "
"Add a configuration file to your project to make it build successfully. "
"Read more at https://docs.readthedocs.io/en/stable/config-file/v2.html"
)


class BuildUserSkip(BuildUserError):
Expand Down
5 changes: 5 additions & 0 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1915,6 +1915,7 @@ def add_features(sender, **kwargs):
DONT_CREATE_INDEX = "dont_create_index"
USE_RCLONE = "use_rclone"
HOSTING_INTEGRATIONS = "hosting_integrations"
NO_CONFIG_FILE_DEPRECATED = "no_config_file"

FEATURES = (
(ALLOW_DEPRECATED_WEBHOOKS, _("Webhook: Allow deprecated webhook views")),
Expand Down Expand Up @@ -2111,6 +2112,10 @@ def add_features(sender, **kwargs):
"Proxito: Inject 'readthedocs-client.js' as <script> HTML tag in responses."
),
),
(
NO_CONFIG_FILE_DEPRECATED,
_("Build: Building without a configuration file is deprecated."),
),
)

FEATURES = sorted(FEATURES, key=lambda l: l[1])
Expand Down
13 changes: 12 additions & 1 deletion readthedocs/projects/tasks/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@
from ..signals import before_vcs
from .mixins import SyncRepositoryMixin
from .search import fileify
from .utils import BuildRequest, clean_build, send_external_build_status
from .utils import (
BuildRequest,
clean_build,
deprecated_config_file_used_notification,
send_external_build_status,
)

log = structlog.get_logger(__name__)

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

# Trigger a Celery task here to check if the build is using v1 or not a
# config file at all to create a on-site/email notifications. Note we
# can't create the notification from here since we don't have access to
# the database from the builders.
deprecated_config_file_used_notification.delay(self.data.build["id"])

self.update_build(build_state)
self.save_build_data()

Expand Down
81 changes: 81 additions & 0 deletions readthedocs/projects/tasks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

import structlog
from celery.worker.request import Request
from django.core.cache import cache
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from messages_extends.constants import WARNING_PERSISTENT

from readthedocs.builds.constants import (
BUILD_FINAL_STATES,
Expand All @@ -14,7 +16,11 @@
)
from readthedocs.builds.models import Build
from readthedocs.builds.tasks import send_build_status
from readthedocs.core.permissions import AdminPermission
from readthedocs.core.utils.filesystem import safe_rmtree
from readthedocs.notifications import Notification, SiteNotification
from readthedocs.notifications.backends import EmailBackend
from readthedocs.notifications.constants import REQUIREMENT
from readthedocs.storage import build_media_storage
from readthedocs.worker import app

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


class DeprecatedConfigFileSiteNotification(SiteNotification):

failure_message = (
"Your project '{{ object.slug }}' doesn't have a "
'<a href="https://docs.readthedocs.io/en/stable/config-file/v2.html">.readthedocs.yaml</a> '
"configuration file. "
"This feature is <strong>deprecated and will be removed soon</strong>. "
"Make sure to create one for your project to keep your builds working."
)
failure_level = WARNING_PERSISTENT


class DeprecatedConfigFileEmailNotification(Notification):

app_templates = "projects"
name = "deprecated_config_file_used"
context_object_name = "project"
subject = "Your project will start failing soon"
level = REQUIREMENT

def send(self):
"""Method overwritten to remove on-site backend."""
backend = EmailBackend(self.request)
backend.send(self)


@app.task(queue="web")
def deprecated_config_file_used_notification(build_pk):
"""
Create a notification about not using a config file for all the maintainers of the project.

This task is triggered by the build process to be executed on the webs,
since we don't have access to the db from the build.
"""
build = Build.objects.filter(pk=build_pk).first()
if not build or not build.deprecated_config_used:
return

log.bind(
build_pk=build_pk,
project_slug=build.project.slug,
)

users = AdminPermission.owners(build.project)
log.bind(users=len(users))

log.info("Sending deprecation config file onsite notification.")
for user in users:
n = DeprecatedConfigFileSiteNotification(
user=user,
context_object=build.project,
success=False,
)
n.send()

# Send email notifications only once a week
cache_prefix = "deprecated-config-file-notification"
cached = cache.get(f"{cache_prefix}-{build.project.slug}")
if cached:
log.info("Deprecation config file email sent recently. Skipping.")
return

log.info("Sending deprecation config file email notification.")
for user in users:
n = DeprecatedConfigFileEmailNotification(
user=user,
context_object=build.project,
)
n.send()

# Cache this notification for a week
# TODO: reduce this notification period to 3 days after having this deployed for some weeks
cache.set(f"{cache_prefix}-{build.project.slug}", "sent", timeout=7 * 24 * 60 * 60)


class BuildRequest(Request):

def on_timeout(self, soft, timeout):
Expand Down
6 changes: 3 additions & 3 deletions readthedocs/rtd_tests/tests/test_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def test_build_is_stale(self):
self.assertTrue(build_two.is_stale)
self.assertFalse(build_three.is_stale)

def test_using_latest_config(self):
def test_deprecated_config_used(self):
now = timezone.now()

build = get(
Expand All @@ -260,12 +260,12 @@ def test_using_latest_config(self):
state='finished',
)

self.assertFalse(build.using_latest_config())
self.assertTrue(build.deprecated_config_used())

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

self.assertTrue(build.using_latest_config())
self.assertFalse(build.deprecated_config_used())

def test_build_is_external(self):
# Turn the build version to EXTERNAL type.
Expand Down
12 changes: 7 additions & 5 deletions readthedocs/templates/builds/build_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,20 @@
</p>
</div>
{% endif %}
{% if build.finished and not build.using_latest_config %}

{# This message is not dynamic and only appears when loading the page after the build has finished #}
{% if build.finished and build.deprecated_config_used %}
<div class="build-ideas">
<p>
{% blocktrans trimmed with config_file_link="https://docs.readthedocs.io/page/config-file/v2.html" %}
<strong>Configure your documentation builds!</strong>
Adding a <a href="{{ config_file_link }}">.readthedocs.yaml</a> file to your project
is the recommended way to configure your documentation builds.
You can declare dependencies, set up submodules, and many other great features.
<strong>Your builds will stop working soon!</strong><br/>
Building without a configuration file (or using v1) is deprecated and will be removed soon.
Add a <a href="{{ config_file_link }}">.readthedocs.yaml</a> config file to your project to keep your builds working.
{% endblocktrans %}
</p>
</div>
{% endif %}

{% endif %}

{% if build.finished and build.config.build.commands %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- TODO: Copy the content from the TXT version once we are happy with it -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "core/email/common.txt" %}
{% block content %}
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.

We strongly recommend you to add a configuration file to keep your builds working.

Get in touch with us at {{ production_uri }}{% url 'support' %}
and let us know if you are unable to migrate to a config file for any reason.
{% endblock %}