diff --git a/dockerfiles/nginx/proxito.conf.template b/dockerfiles/nginx/proxito.conf.template index 65ee44bb7c8..dfe4fbd18c5 100644 --- a/dockerfiles/nginx/proxito.conf.template +++ b/dockerfiles/nginx/proxito.conf.template @@ -88,6 +88,17 @@ server { proxy_hide_header Content-Security-Policy; set $content_security_policy $upstream_http_content_security_policy; add_header Content-Security-Policy $content_security_policy always; + + # This header allows us to decide whether or not inject the script at CloudFlare level + # Now, I'm injecting it in all the NGINX responses because `sub_filter` is not allowed inside an `if` statement. + set $rtd_hosting_integrations $upstream_http_x_rtd_hosting_integrations; + add_header X-RTD-Hosting-Integrations $rtd_hosting_integrations always; + + # Inject our own script dynamically + # TODO: find a way to make this work _without_ running `npm run dev` from the `readthedocs-client` repository + sub_filter '' '\n'; + sub_filter_last_modified on; + sub_filter_once on; } # Serve 404 pages here diff --git a/readthedocs/api/v2/serializers.py b/readthedocs/api/v2/serializers.py index 4a5c03e758a..aad6ac57c43 100644 --- a/readthedocs/api/v2/serializers.py +++ b/readthedocs/api/v2/serializers.py @@ -101,7 +101,7 @@ class VersionSerializer(serializers.ModelSerializer): class Meta: model = Version - fields = ( + fields = [ 'id', 'project', 'slug', @@ -116,7 +116,7 @@ class Meta: 'has_epub', 'has_htmlzip', 'documentation_type', - ) + ] class VersionAdminSerializer(VersionSerializer): @@ -124,6 +124,12 @@ class VersionAdminSerializer(VersionSerializer): """Version serializer that returns admin project data.""" project = ProjectAdminSerializer() + build_data = serializers.JSONField(required=False, write_only=True) + + class Meta(VersionSerializer.Meta): + fields = VersionSerializer.Meta.fields + [ + "build_data", + ] class BuildCommandSerializer(serializers.ModelSerializer): diff --git a/readthedocs/builds/migrations/0048_add_build_data.py b/readthedocs/builds/migrations/0048_add_build_data.py new file mode 100644 index 00000000000..94a68c30fa0 --- /dev/null +++ b/readthedocs/builds/migrations/0048_add_build_data.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.18 on 2023-03-13 15:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("builds", "0047_build_default_triggered"), + ] + + operations = [ + migrations.AddField( + model_name="version", + name="build_data", + field=models.JSONField( + default=None, + null=True, + verbose_name="Data generated at build time by the doctool (`readthedocs-build.yaml`).", + ), + ), + ] diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index fafc68cecaa..dc83b11c34b 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -65,11 +65,9 @@ BITBUCKET_COMMIT_URL, BITBUCKET_URL, DOCTYPE_CHOICES, - GITHUB_BRAND, GITHUB_COMMIT_URL, GITHUB_PULL_REQUEST_COMMIT_URL, GITHUB_URL, - GITLAB_BRAND, GITLAB_COMMIT_URL, GITLAB_MERGE_REQUEST_COMMIT_URL, GITLAB_URL, @@ -186,6 +184,12 @@ class Version(TimeStampedModel): ), ) + build_data = models.JSONField( + _("Data generated at build time by the doctool (`readthedocs-build.yaml`)."), + default=None, + null=True, + ) + objects = VersionManager.from_queryset(VersionQuerySet)() # Only include BRANCH, TAG, UNKNOWN type Versions. internal = InternalVersionManager.from_queryset(partial(VersionQuerySet, internal_only=True))() diff --git a/readthedocs/doc_builder/director.py b/readthedocs/doc_builder/director.py index c7aecd62910..3c7e824374a 100644 --- a/readthedocs/doc_builder/director.py +++ b/readthedocs/doc_builder/director.py @@ -2,6 +2,7 @@ import tarfile import structlog +import yaml from django.conf import settings from django.utils.translation import gettext_lazy as _ @@ -187,6 +188,7 @@ def build(self): self.build_epub() self.run_build_job("post_build") + self.store_readthedocs_build_yaml() after_build.send( sender=self.data.version, @@ -392,6 +394,7 @@ def run_build_commands(self): # Update the `Version.documentation_type` to match the doctype defined # by the config file. When using `build.commands` it will be `GENERIC` self.data.version.documentation_type = self.data.config.doctype + self.store_readthedocs_build_yaml() def install_build_tools(self): """ @@ -625,3 +628,37 @@ def get_build_env_vars(self): def is_type_sphinx(self): """Is documentation type Sphinx.""" return "sphinx" in self.data.config.doctype + + def store_readthedocs_build_yaml(self): + # load YAML from user + yaml_path = os.path.join( + self.data.project.artifact_path( + version=self.data.version.slug, type_="html" + ), + "readthedocs-build.yaml", + ) + + if not os.path.exists(yaml_path): + log.debug("Build output YAML file (readtehdocs-build.yaml) does not exist.") + return + + try: + with open(yaml_path, "r") as f: + data = yaml.safe_load(f) + except Exception: + # NOTE: skip this work for now until we decide whether or not this + # YAML file is required. + # + # NOTE: decide whether or not we want this + # file to be mandatory and raise an exception here. + return + + log.info("readthedocs-build.yaml loaded.", path=yaml_path) + + # TODO: validate the YAML generated by the user + # self._validate_readthedocs_build_yaml(data) + + # Copy the YAML data into `Version.build_data`. + # It will be saved when the API is hit. + # This data will be used by the `/_/readthedocs-config.json` API endpoint. + self.data.version.build_data = data diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index bf502c1fce5..e42a381c3cf 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -1884,6 +1884,7 @@ def add_features(sender, **kwargs): CANCEL_OLD_BUILDS = "cancel_old_builds" DONT_CREATE_INDEX = "dont_create_index" USE_RCLONE = "use_rclone" + HOSTING_INTEGRATIONS = "hosting_integrations" FEATURES = ( (ALLOW_DEPRECATED_WEBHOOKS, _('Allow deprecated webhook views')), @@ -2058,6 +2059,10 @@ def add_features(sender, **kwargs): USE_RCLONE, _("Use rclone for syncing files to the media storage."), ), + ( + HOSTING_INTEGRATIONS, + _("Inject 'readthedocs-client.js' as