You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2020/08/18 17:40:50 UTC

[airflow] branch master updated: You can disable spellcheck or documentation when building docs. (#10377)

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/master by this push:
     new 9228bf2  You can disable spellcheck or documentation when building docs. (#10377)
9228bf2 is described below

commit 9228bf2bd08fad26f89831c7c163e02d3d3b7e31
Author: Jarek Potiuk <ja...@polidea.com>
AuthorDate: Tue Aug 18 19:40:18 2020 +0200

    You can disable spellcheck or documentation when building docs. (#10377)
    
    This cleans up the document building process and replaces it
    with breeze-only. The original instructions with
    `pip install -e .[doc]` stopped working so there is no
    point keeping them.
    
    Extracted from #10368
---
 .pre-commit-config.yaml                   |  12 +-
 BREEZE.rst                                |   6 +-
 CONTRIBUTING.rst                          |  25 ++--
 breeze                                    |   8 +-
 docs/{build => build_docs.py}             | 238 +++++++++++++++++++-----------
 scripts/ci/docs/ci_docs.sh                |   2 +-
 scripts/ci/in_container/run_docs_build.sh |   4 +-
 scripts/ci/libraries/_runs.sh             |   2 +-
 8 files changed, 182 insertions(+), 115 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a4cd756..264c04d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,7 +22,7 @@ default_language_version:
 minimum_pre_commit_version: "1.20.0"
 repos:
   - repo: https://github.com/Lucas-C/pre-commit-hooks
-    rev: v1.1.7
+    rev: v1.1.9
     hooks:
       - id: forbid-tabs
         exclude: ^docs/Makefile$|^clients/gen/go.sh
@@ -100,7 +100,7 @@ repos:
       - id: insert-license
         name: Add license for all XML files
         exclude: ^\.github/.*$
-        types: [xml]
+        files: .*\.xml$
         args:
           - --comment-style
           - "<!--||-->"
@@ -141,7 +141,7 @@ repos:
     hooks:
       - id: check-hooks-apply
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v3.1.0
+    rev: v3.2.0
     hooks:
       - id: check-merge-conflict
       - id: debug-statements
@@ -156,12 +156,12 @@ repos:
         args:
           - --remove
   - repo: https://github.com/pre-commit/pygrep-hooks
-    rev: v1.5.1
+    rev: v1.6.0
     hooks:
       - id: rst-backticks
       - id: python-no-log-warn
   - repo: https://github.com/adrienverge/yamllint
-    rev: v1.23.0
+    rev: v1.24.2
     hooks:
       - id: yamllint
         name: Check yaml files with yamllint
@@ -169,7 +169,7 @@ repos:
         types: [yaml]
         exclude: ^.*init_git_sync\.template\.yaml$|^.*airflow\.template\.yaml$|^chart/templates/.*\.yaml$
   - repo: https://github.com/timothycrosley/isort
-    rev: 5.0.8
+    rev: 5.4.2
     hooks:
       - id: isort
         name: Run isort to sort imports
diff --git a/BREEZE.rst b/BREEZE.rst
index d492897..c3823e3 100644
--- a/BREEZE.rst
+++ b/BREEZE.rst
@@ -1111,14 +1111,16 @@ This is the current syntax for  `./breeze <./breeze>`_:
   Detailed usage for command: build-docs
 
 
-  breeze build-docs
+  breeze build-docs [-- <EXTRA_ARGS>]
 
         Builds Airflow documentation. The documentation is build inside docker container - to
         maintain the same build environment for everyone. Appropriate sources are mapped from
         the host to the container so that latest sources are used. The folders where documentation
-        is generated ('docs/build') are also mounted to the container - this way results of
+        is generated ('docs/_build') are also mounted to the container - this way results of
         the documentation build is available in the host.
 
+        The possible extra args are: --docs-only, --spellcheck-only, --help
+
 
   ####################################################################################################
 
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index e44286e..cb5d600 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -587,27 +587,32 @@ Documentation
 The latest API documentation (for the master branch) is usually available
 `here <https://airflow.readthedocs.io/en/latest/>`__.
 
-To generate a local version:
+To generate a local version you can use `<BREEZE.rst>`_.
 
-1.  Set up an Airflow development environment.
+The documentation build consists of verifying consistency of documentation and two steps:
 
-2.  Install the ``doc`` extra.
+* spell checking
+* building documentation
+
+You can only run one of the steps via ``--spellcheck-only`` or ``--docs-only``.
 
 .. code-block:: bash
 
-    pip install -e '.[doc]'
+    ./breeze build-docs
+
+or just to run spell-check
+
+.. code-block:: bash
 
+     ./breeze build-docs -- --spellcheck-only
 
-3.  Generate and serve the documentation as follows:
+or just to run documentation building
 
 .. code-block:: bash
 
-    cd docs
-    ./build
-    ./start_doc_server.sh
+     ./breeze build-docs -- --docs-only
 
-.. note::
-    The docs build script ``build`` requires Python 3.6 or greater.
+Also documentation is available as downloadable artifact in GitHub Actions after the CI builds your PR.
 
 **Known issues:**
 
diff --git a/breeze b/breeze
index d8c8103..29573cd 100755
--- a/breeze
+++ b/breeze
@@ -1126,13 +1126,15 @@ ${CMDNAME} exec [-- <EXTRA_ARGS>]
       webserver, workers, database console and interactive terminal.
 "
     export DETAILED_USAGE_BUILD_DOCS="
-${CMDNAME} build-docs
+${CMDNAME} build-docs [-- <EXTRA_ARGS>]
 
       Builds Airflow documentation. The documentation is build inside docker container - to
       maintain the same build environment for everyone. Appropriate sources are mapped from
       the host to the container so that latest sources are used. The folders where documentation
-      is generated ('docs/build') are also mounted to the container - this way results of
+      is generated ('docs/_build') are also mounted to the container - this way results of
       the documentation build is available in the host.
+
+      The possible extra args are: --docs-only, --spellcheck-only, --help
 "
     # shellcheck disable=SC2089
     DETAILED_USAGE_BUILD_IMAGE="
@@ -2210,7 +2212,7 @@ function run_breeze_command {
             perform_kind_cluster_operation "${KIND_CLUSTER_OPERATION}"
             ;;
         build_docs)
-            run_docs
+            run_docs "${@}"
             ;;
         *)
           echo >&2
diff --git a/docs/build b/docs/build_docs.py
similarity index 75%
rename from docs/build
rename to docs/build_docs.py
index e831e7f..a0a7124 100755
--- a/docs/build
+++ b/docs/build_docs.py
@@ -15,8 +15,8 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+import argparse
 import ast
-import atexit
 import os
 import re
 import shlex
@@ -33,12 +33,15 @@ from typing import Iterable, List, NamedTuple, Optional, Set
 if __name__ != "__main__":
     raise Exception(
         "This file is intended to be executed as an executable program. You cannot use it as a module."
-        "To run this script, run the ./build command"
+        "To run this script, run the ./build_docs.py command"
     )
 
 
 @total_ordering
 class DocBuildError(NamedTuple):
+    """
+    Errors found in docs build.
+    """
     file_path: Optional[str]
     line_no: Optional[int]
     message: str
@@ -49,7 +52,7 @@ class DocBuildError(NamedTuple):
         return left == right
 
     def __ne__(self, other):
-        return not (self == other)
+        return not self == other
 
     def __lt__(self, other):
         file_path_a = self.file_path or ''
@@ -61,60 +64,29 @@ class DocBuildError(NamedTuple):
 
 build_errors: List[DocBuildError] = []
 
-os.chdir(os.path.dirname(os.path.abspath(__file__)))
-
-ROOT_PROJECT_DIR = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
+ROOT_PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
 ROOT_PACKAGE_DIR = os.path.join(ROOT_PROJECT_DIR, "airflow")
+DOCS_DIR = os.path.join(ROOT_PROJECT_DIR, "docs")
+
+_API_DIR = os.path.join(DOCS_DIR, "_api")
+_BUILD_DIR = os.path.join(DOCS_DIR, "_build")
 
 
 def clean_files() -> None:
-    print("Removing content of the _build and _api folders")
-    with suppress(FileNotFoundError):
-        for filename in glob("_build/*"):
-            shutil.rmtree(f"_build/{filename}")
-    with suppress(FileNotFoundError):
-        for filename in glob("_api/*"):
-            shutil.rmtree(f"_api/{filename}")
-    print("Removed content of the _build and _api folders")
-
-
-def prepare_directories() -> None:
-    if os.path.exists("/.dockerenv"):
-        # This script can be run both - in container and outside of it.
-        # Here we are inside the container which means that we should (when the host is Linux)
-        # fix permissions of the _build and _api folders via sudo.
-        # Those files are mounted from the host via docs folder and we might not have permissions to
-        # write to those directories (and remove the _api folder).
-        # We know we have sudo capabilities inside the container.
-        print("Creating the _build and _api folders in case they do not exist")
-        run(["sudo", "mkdir", "-pv", "_build"], check=True)
-        run(["sudo", "mkdir", "-pv", "_api"], check=True)
-        print("Created the _build and _api folders in case they do not exist")
-
-        def restore_ownership() -> None:
-            # We are inside the container which means that we should fix back the permissions of the
-            # _build and _api folder files, so that they can be accessed by the host user
-            # The _api folder should be deleted by then but just in case we should change the ownership
-            host_user_id = os.environ["HOST_USER_ID"]
-            host_group_id = os.environ["HOST_GROUP_ID"]
-            print(f"Changing ownership of docs/_build folder back to {host_user_id}:{host_group_id}")
-            run(["sudo", "chown", "-R", f'{host_user_id}:{host_group_id}', "_build"], check=True)
-            if os.path.exists("_api"):
-                run(["sudo", "chown", "-R", f'{host_user_id}:{host_group_id}', "_api"], check=True)
-
-            print(f"Changed ownership of docs/_build folder back to {host_user_id}:{host_group_id}")
-
-        atexit.register(restore_ownership)
-
-    else:
-        # We are outside the container so we simply make sure that the directories exist
-        print("Creating the _build and _api folders in case they do not exist")
-        run(["mkdir", "-pv", "_build"], check=True)
-        run(["mkdir", "-pv", "_api"], check=True)
-        print("Creating the _build and _api folders in case they do not exist")
+    """
+    Cleanup all artifacts generated by previous builds.
+    """
+    shutil.rmtree(_API_DIR, ignore_errors=True)
+    shutil.rmtree(_BUILD_DIR, ignore_errors=True)
+    os.makedirs(_API_DIR, exist_ok=True)
+    os.makedirs(_BUILD_DIR, exist_ok=True)
+    print(f"Recreated content of the ${_BUILD_DIR} and ${_API_DIR} folders")
 
 
 def display_errors_summary() -> None:
+    """
+    Displays summary of errors
+    """
     for warning_no, error in enumerate(sorted(build_errors), 1):
         print("=" * 20, f"Error {warning_no:3}", "=" * 20)
         print(error.message)
@@ -129,10 +101,14 @@ def display_errors_summary() -> None:
     print("=" * 50)
 
 
-def find_existing_guide_operator_names():
+def find_existing_guide_operator_names() -> Set[str]:
+    """
+    Find names of existing operators.
+    :return names of existing operators.
+    """
     operator_names = set()
 
-    paths = glob("howto/operator/**/*.rst", recursive=True)
+    paths = glob(f"${DOCS_DIR}/howto/operator/**/*.rst", recursive=True)
     for path in paths:
         with open(path) as f:
             operator_names |= set(re.findall(".. _howto/operator:(.+?):", f.read()))
@@ -141,11 +117,25 @@ def find_existing_guide_operator_names():
 
 
 def extract_ast_class_def_by_name(ast_tree, class_name):
+    """
+    Extracts class definition by name
+    :param ast_tree: AST tree
+    :param class_name: name of the class.
+    :return: class node found
+    """
     class ClassVisitor(ast.NodeVisitor):
+        """
+        Visitor.
+        """
         def __init__(self):
             self.found_class_node = None
 
-        def visit_ClassDef(self, node):
+        def visit_ClassDef(self, node):  # pylint: disable=invalid-name
+            """
+            Visit class definition.
+            :param node: node.
+            :return:
+            """
             if node.name == class_name:
                 self.found_class_node = node
 
@@ -155,19 +145,23 @@ def extract_ast_class_def_by_name(ast_tree, class_name):
     return visitor.found_class_node
 
 
-def check_guide_links_in_operator_descriptions():
+def check_guide_links_in_operator_descriptions() -> None:
+    """
+    Check if there are links to guides in operator's descriptions.
+
+    """
     def generate_build_error(path, line_no, operator_name):
         return DocBuildError(
-                    file_path=path,
-                    line_no=line_no,
-                    message=(
-                        f"Link to the guide is missing in operator's description: {operator_name}.\n"
-                        f"Please add link to the guide to the description in the following form:\n"
-                        f"\n"
-                        f".. seealso::\n"
-                        f"    For more information on how to use this operator, take a look at the guide:\n"
-                        f"    :ref:`howto/operator:{operator_name}`\n"
-                    )
+            file_path=path,
+            line_no=line_no,
+            message=(
+                f"Link to the guide is missing in operator's description: {operator_name}.\n"
+                f"Please add link to the guide to the description in the following form:\n"
+                f"\n"
+                f".. seealso::\n"
+                f"    For more information on how to use this operator, take a look at the guide:\n"
+                f"    :ref:`howto/operator:{operator_name}`\n"
+            )
         )
 
     # Extract operators for which there are existing .rst guides
@@ -213,6 +207,12 @@ def check_guide_links_in_operator_descriptions():
 
 
 def assert_file_not_contains(file_path: str, pattern: str, message: str) -> None:
+    """
+    Asserts that file does not contain the pattern. Return message error if it does.
+    :param file_path: file
+    :param pattern: pattern
+    :param message: message to return
+    """
     with open(file_path, "rb", 0) as doc_file:
         pattern_compiled = re.compile(pattern)
 
@@ -223,6 +223,12 @@ def assert_file_not_contains(file_path: str, pattern: str, message: str) -> None
 
 
 def filter_file_list_by_pattern(file_paths: Iterable[str], pattern: str) -> List[str]:
+    """
+    Filters file list to those tha content matches the pattern
+    :param file_paths: file paths to check
+    :param pattern: pattern to match
+    :return: list of files matching the pattern
+    """
     output_paths = []
     pattern_compiled = re.compile(pattern)
     for file_path in file_paths:
@@ -234,20 +240,28 @@ def filter_file_list_by_pattern(file_paths: Iterable[str], pattern: str) -> List
 
 
 def find_modules(deprecated_only: bool = False) -> Set[str]:
+    """
+    Finds all modules.
+    :param deprecated_only: whether only deprecated modules should be found.
+    :return: set of all modules found
+    """
     file_paths = glob(f"{ROOT_PACKAGE_DIR}/**/*.py", recursive=True)
     # Exclude __init__.py
-    file_paths = (f for f in file_paths if not f.endswith("__init__.py"))
+    file_paths = [f for f in file_paths if not f.endswith("__init__.py")]
     if deprecated_only:
         file_paths = filter_file_list_by_pattern(file_paths, r"This module is deprecated.")
     # Make path relative
-    file_paths = (os.path.relpath(f, ROOT_PROJECT_DIR) for f in file_paths)
+    file_paths = [os.path.relpath(f, ROOT_PROJECT_DIR) for f in file_paths]
     # Convert filename to module
     modules_names = {file_path.rpartition(".")[0].replace("/", ".") for file_path in file_paths}
     return modules_names
 
 
 def check_class_links_in_operators_and_hooks_ref() -> None:
-    with open("operators-and-hooks-ref.rst") as ref_file:
+    """
+    Checks classes and links in the operators and hooks ref.
+    """
+    with open(os.path.join(DOCS_DIR, "operators-and-hooks-ref.rst")) as ref_file:
         content = ref_file.read()
     current_modules_in_file = set(re.findall(r":mod:`(.+?)`", content))
 
@@ -278,21 +292,24 @@ def check_class_links_in_operators_and_hooks_ref() -> None:
 
 
 def check_guide_links_in_operators_and_hooks_ref() -> None:
-    all_guides = glob("howto/operator/**/*.rst", recursive=True)
+    """
+    Checks all guide links in operators and hooks references.
+    """
+    all_guides = glob(f"{DOCS_DIR}/howto/operator/**/*.rst", recursive=True)
     # Remove extension
-    all_guides = (
-        guide.rpartition(".")[0]
+    all_guides = [
+        os.path.relpath(guide, DOCS_DIR).rpartition(".")[0]
         for guide in all_guides
         if "_partials" not in guide
-    )
+    ]
     # Remove partials and index
-    all_guides = (
+    all_guides = [
         guide
         for guide in all_guides
         if "/_partials/" not in guide and not guide.endswith("index")
-    )
+    ]
 
-    with open("operators-and-hooks-ref.rst") as ref_file:
+    with open(os.path.join(DOCS_DIR, "operators-and-hooks-ref.rst")) as ref_file:
         content = ref_file.read()
 
     missing_guides = [
@@ -320,7 +337,10 @@ def check_guide_links_in_operators_and_hooks_ref() -> None:
 
 
 def check_exampleinclude_for_example_dags():
-    all_docs_files = glob("**/*rst", recursive=True)
+    """
+    Checks all exampleincludes for  example dags.
+    """
+    all_docs_files = glob(f"${DOCS_DIR}/**/*rst", recursive=True)
 
     for doc_file in all_docs_files:
         assert_file_not_contains(
@@ -328,13 +348,16 @@ def check_exampleinclude_for_example_dags():
             pattern=r"literalinclude::.+example_dags",
             message=(
                 "literalinclude directive is is prohibited for example DAGs. \n"
-                "You should use a exampleinclude directive to include example DAGs."
+                "You should use the exampleinclude directive to include example DAGs."
             )
         )
 
 
 def check_enforce_code_block():
-    all_docs_files = glob("**/*rst", recursive=True)
+    """
+    Checks all code:: blocks.
+    """
+    all_docs_files = glob(f"${DOCS_DIR}/**/*rst", recursive=True)
 
     for doc_file in all_docs_files:
         assert_file_not_contains(
@@ -367,11 +390,15 @@ MISSING_GOOGLE_DOC_GUIDES = {
 
 
 def check_google_guides():
-    doc_files = glob(f"{ROOT_PROJECT_DIR}/docs/howto/operator/google/**/*.rst", recursive=True)
+    """
+    Checks Google guides.
+
+    """
+    doc_files = glob(f"{DOCS_DIR}/howto/operator/google/**/*.rst", recursive=True)
     doc_names = {f.split("/")[-1].rsplit(".")[0] for f in doc_files}
 
     operators_files = chain(*[
-        glob(f"{ROOT_PROJECT_DIR}/airflow/providers/google/*/{resource_type}/*.py")
+        glob(f"{ROOT_PACKAGE_DIR}/providers/google/*/{resource_type}/*.py")
         for resource_type in ["operators", "sensors", "transfers"]
     ])
     operators_files = (f for f in operators_files if not f.endswith("__init__.py"))
@@ -405,6 +432,13 @@ def check_google_guides():
 
 
 def prepare_code_snippet(file_path: str, line_no: int, context_lines_count: int = 5) -> str:
+    """
+    Prepares code snippet.
+    :param file_path: file path
+    :param line_no: line number
+    :param context_lines_count: number of lines of context.
+    :return:
+    """
     def guess_lexer_for_filename(filename):
         from pygments.lexers import get_lexer_for_filename
         from pygments.util import ClassNotFound
@@ -439,12 +473,18 @@ def prepare_code_snippet(file_path: str, line_no: int, context_lines_count: int
 
 
 def parse_sphinx_warnings(warning_text: str) -> List[DocBuildError]:
+    """
+    Parses warnings from Sphinx.
+    :param warning_text: warning to parse
+    :return: list of DocBuildErrors.
+    """
     sphinx_build_errors = []
     for sphinx_warning in warning_text.split("\n"):
         if not sphinx_warning:
             continue
         warning_parts = sphinx_warning.split(":", 2)
         if len(warning_parts) == 3:
+            # noinspection PyBroadException
             try:
                 sphinx_build_errors.append(
                     DocBuildError(
@@ -464,6 +504,11 @@ def parse_sphinx_warnings(warning_text: str) -> List[DocBuildError]:
 
 
 def check_spelling() -> None:
+    """
+    Checks spelling for sphinx.
+
+    :return:
+    """
     build_cmd = [
         "sphinx-build",
         "-W",
@@ -471,7 +516,7 @@ def check_spelling() -> None:
         "spelling",
         "-d",  # path for the cached environment and doctree files
         "_build/doctrees",
-        "-D", # override the extensions because one of them throws an error on the spelling builder
+        "-D",  # override the extensions because one of them throws an error on the spelling builder
         """extensions=sphinxarg.ext\
 ,autoapi.extension\
 ,sphinxcontrib.spelling\
@@ -490,7 +535,7 @@ def check_spelling() -> None:
     ]
     print("Executing cmd: ", " ".join([shlex.quote(c) for c in build_cmd]))
 
-    completed_proc = run(build_cmd)
+    completed_proc = run(build_cmd, cwd=DOCS_DIR)  # pylint: disable=subprocess-run-check
     if completed_proc.returncode != 0:
         build_errors.append(
             DocBuildError(
@@ -502,6 +547,9 @@ def check_spelling() -> None:
 
 
 def build_sphinx_docs() -> None:
+    """
+    Build documentation for sphinx.
+    """
     with NamedTemporaryFile() as tmp_file:
         build_cmd = [
             "sphinx-build",
@@ -517,7 +565,7 @@ def build_sphinx_docs() -> None:
         ]
         print("Executing cmd: ", " ".join([shlex.quote(c) for c in build_cmd]))
 
-        completed_proc = run(build_cmd)
+        completed_proc = run(build_cmd, cwd=DOCS_DIR)  # pylint: disable=subprocess-run-check
         if completed_proc.returncode != 0:
             build_errors.append(
                 DocBuildError(
@@ -535,6 +583,11 @@ def build_sphinx_docs() -> None:
 
 
 def print_build_errors_and_exit(message) -> None:
+    """
+    Prints build errors and exists.
+    :param message:
+    :return:
+    """
     if build_errors:
         display_errors_summary()
         print()
@@ -544,9 +597,14 @@ def print_build_errors_and_exit(message) -> None:
         sys.exit(1)
 
 
-print("Current working directory: ", os.getcwd())
+parser = argparse.ArgumentParser(description='Builds documentation and runs spell checking')
+parser.add_argument('--docs-only', dest='docs_only', action='store_true',
+                    help='Only build documentation')
+parser.add_argument('--spellcheck-only', dest='spellcheck_only', action='store_true',
+                    help='Only perform spellchecking')
+
+args = parser.parse_args()
 
-prepare_directories()
 clean_files()
 
 check_guide_links_in_operator_descriptions()
@@ -564,10 +622,10 @@ Invitation link: https://apache-airflow-slack.herokuapp.com/\
 
 print_build_errors_and_exit("The documentation has errors. Fix them to build documentation.")
 
-check_spelling()
-
-print_build_errors_and_exit("The documentation has spelling errors. Fix them to build documentation.")
-
-build_sphinx_docs()
+if not args.docs_only:
+    check_spelling()
+    print_build_errors_and_exit("The documentation has spelling errors. Fix them to build documentation.")
 
-print_build_errors_and_exit("The documentation has errors.")
+if not args.spellcheck_only:
+    build_sphinx_docs()
+    print_build_errors_and_exit("The documentation has errors.")
diff --git a/scripts/ci/docs/ci_docs.sh b/scripts/ci/docs/ci_docs.sh
index 605ce85..3e370f7 100755
--- a/scripts/ci/docs/ci_docs.sh
+++ b/scripts/ci/docs/ci_docs.sh
@@ -34,4 +34,4 @@ prepare_ci_build
 
 rebuild_ci_image_if_needed
 
-run_docs
+run_docs "${@}"
diff --git a/scripts/ci/in_container/run_docs_build.sh b/scripts/ci/in_container/run_docs_build.sh
index 6e1b022..498ec02 100755
--- a/scripts/ci/in_container/run_docs_build.sh
+++ b/scripts/ci/in_container/run_docs_build.sh
@@ -26,9 +26,9 @@ trap "${HANDLERS}${HANDLERS:+;}in_container_fix_ownership" EXIT
 sudo rm -rf "${AIRFLOW_SOURCES}/docs/_build/*"
 sudo rm -rf "${AIRFLOW_SOURCES}/docs/_api/*"
 
-sudo -E "${AIRFLOW_SOURCES}/docs/build"
+sudo -E "${AIRFLOW_SOURCES}/docs/build_docs.py" "${@}"
 
-if [[ ${CI} == "true" ]]; then
+if [[ ${CI} == "true" && -d "${AIRFLOW_SOURCES}/docs/_build/html" ]]; then
     rm -rf "/files/documentation"
     cp -r "${AIRFLOW_SOURCES}/docs/_build/html" "/files/documentation"
 fi
diff --git a/scripts/ci/libraries/_runs.sh b/scripts/ci/libraries/_runs.sh
index 9608d57..53d1055 100644
--- a/scripts/ci/libraries/_runs.sh
+++ b/scripts/ci/libraries/_runs.sh
@@ -21,7 +21,7 @@ function run_docs() {
     verbose_docker run "${EXTRA_DOCKER_FLAGS[@]}" -t \
             --entrypoint "/usr/local/bin/dumb-init"  \
             "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/docs/build" \
+            "--" "/opt/airflow/docs/build_docs.py" "${@}" \
             | tee -a "${OUTPUT_LOG}"
 }