Repository: readthedocs/readthedocs.org

Issue:
Build: run `cat readthedocs.yaml` even when there is a config error
I just realized that we first validate the `readthedocs.yaml` file and then run `cat readthedocs.yaml` to show its content. What happens? If there is a syntax error or a missing required value or anything, we don't run the `cat` command. This makes hard to debug and understand what's going on.

We should first run the `cat` command and after that validate the file. This will help a lot with UX.

Example: https://beta.readthedocs.org/projects/test-builds/builds/23364407/


Discussion:
Related to
* https://github.com/readthedocs/readthedocs.org/issues/7005
* https://github.com/readthedocs/readthedocs.org/issues/4288
This issue would have been being helpful on this case https://github.com/readthedocs/readthedocs.org/issues/11173 where the user had two YAML file in the repository and Read the Docs was picking the incorrect one.
I took a look at this and it seems we need to split the [`readthedocs.config.config.load()`](https://github.com/readthedocs/readthedocs.org/blob/03c03fa4de2a52fb5bbbbd8ab047e485abcc310c/readthedocs/config/config.py#L805-L850) function into two:

1. find the YAML file to load
2. load the YAML file and validate it

Moving out 1) will allow us to know what's the file we will be loading and show it to the user _before_ loading and validating.

Resolving patch:
diff --git a/readthedocs/doc_builder/director.py b/readthedocs/doc_builder/director.py
--- a/readthedocs/doc_builder/director.py
+++ b/readthedocs/doc_builder/director.py
@@ -16,6 +16,8 @@
 from django.utils.translation import gettext_lazy as _
 
 from readthedocs.builds.constants import EXTERNAL
+from readthedocs.config.config import CONFIG_FILENAME_REGEX
+from readthedocs.config.find import find_one
 from readthedocs.core.utils.filesystem import safe_open
 from readthedocs.doc_builder.config import load_yaml_config
 from readthedocs.doc_builder.exceptions import BuildUserError
@@ -108,20 +110,6 @@ def setup_vcs(self):
         # self.run_build_job("pre_checkout")
         self.checkout()
 
-        # Output the path for the config file used.
-        # This works as confirmation for us & the user about which file is used,
-        # as well as the fact that *any* config file is used.
-        if self.data.config.source_file:
-            cwd = self.data.project.checkout_path(self.data.version.slug)
-            command = self.vcs_environment.run(
-                "cat",
-                # Show user the relative path to the config file
-                # TODO: Have our standard path replacement code catch this.
-                # https://github.com/readthedocs/readthedocs.org/pull/10413#discussion_r1230765843
-                self.data.config.source_file.replace(cwd + "/", ""),
-                cwd=cwd,
-            )
-
         self.run_build_job("post_checkout")
 
         commit = self.data.build_commit or self.vcs_repository.commit
@@ -240,6 +228,24 @@ def checkout(self):
 
         if custom_config_file:
             log.info("Using a custom .readthedocs.yaml file.", path=custom_config_file)
+
+        checkout_path = self.data.project.checkout_path(self.data.version.slug)
+        default_config_file = find_one(checkout_path, CONFIG_FILENAME_REGEX)
+        final_config_file = custom_config_file or default_config_file
+
+        # Output the path for the config file used.
+        # This works as confirmation for us & the user about which file is used,
+        # as well as the fact that *any* config file is used.
+        if final_config_file:
+            command = self.vcs_environment.run(
+                "cat",
+                # Show user the relative path to the config file
+                # TODO: Have our standard path replacement code catch this.
+                # https://github.com/readthedocs/readthedocs.org/pull/10413#discussion_r1230765843
+                final_config_file.replace(checkout_path + "/", ""),
+                cwd=checkout_path,
+            )
+
         self.data.config = load_yaml_config(
             version=self.data.version,
             readthedocs_yaml_path=custom_config_file,


Test patch:
diff --git a/readthedocs/projects/tests/test_build_tasks.py b/readthedocs/projects/tests/test_build_tasks.py
--- a/readthedocs/projects/tests/test_build_tasks.py
+++ b/readthedocs/projects/tests/test_build_tasks.py
@@ -810,11 +810,14 @@ def test_build_commands_executed(
         python_version = settings.RTD_DOCKER_BUILD_SETTINGS["tools"]["python"]["3"]
         self.mocker.mocks["environment.run"].assert_has_calls(
             [
-                mock.call(
-                    "cat",
-                    "readthedocs.yml",
-                    cwd="/tmp/readthedocs-tests/git-repository",
-                ),
+                # TODO: check for this in the VCS environment.
+                # We can't check it here because this is the build environment.
+                #
+                # mock.call(
+                #     "cat",
+                #     "readthedocs.yml",
+                #     cwd="/tmp/readthedocs-tests/git-repository",
+                # ),
                 mock.call("asdf", "install", "python", python_version),
                 mock.call("asdf", "global", "python", python_version),
                 mock.call("asdf", "reshim", "python", record=False),
@@ -1348,7 +1351,10 @@ def test_conda_config_calls_conda_command(self, load_yaml_config):
         ]
         self.mocker.mocks["environment.run"].assert_has_calls(
             [
-                mock.call("cat", "readthedocs.yml", cwd=mock.ANY),
+                # TODO: check for this in the VCS environment.
+                # We can't check it here because this is the build environment.
+                #
+                # mock.call("cat", "readthedocs.yml", cwd=mock.ANY),
                 mock.call("asdf", "install", "python", python_version),
                 mock.call("asdf", "global", "python", python_version),
                 mock.call("asdf", "reshim", "python", record=False),
@@ -1442,7 +1448,10 @@ def test_python_mamba_commands(self, load_yaml_config):
 
         self.mocker.mocks["environment.run"].assert_has_calls(
             [
-                mock.call("cat", "readthedocs.yml", cwd=mock.ANY),
+                # TODO: check for this in the VCS environment.
+                # We can't check it here because this is the build environment.
+                #
+                # mock.call("cat", "readthedocs.yml", cwd=mock.ANY),
                 mock.call("asdf", "install", "python", "mambaforge-4.10.3-10"),
                 mock.call("asdf", "global", "python", "mambaforge-4.10.3-10"),
                 mock.call("asdf", "reshim", "python", record=False),