From 95a466cb90968d655fa62ddf9f616c65d418cff5 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Tue, 17 Mar 2026 14:55:41 +0530 Subject: [PATCH 01/11] Handling no-tag presence --- run.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/run.py b/run.py index d4ada1a..8fea2e4 100644 --- a/run.py +++ b/run.py @@ -562,6 +562,14 @@ def _build_html_docs_incremental(build_output, skip_switcher, include_current): # pylint: disable=R0913, R0917 +def _run_sphinx_build(target): + """Run a standard single-version Sphinx build.""" + run_command( + [sys.executable, '-m', 'sphinx', 'build', '-M', target, DOCS_SOURCE_DIR, DOCS_BUILD_DIR], + ROOT_DIR, + ) + + def build_docs( target, skip_build, local=False, skip_switcher=False, include_current=False, incremental=False ): @@ -580,7 +588,9 @@ def build_docs( logging.info('Incremental build mode: preserving existing documentation...') try: - if target == 'html' and not local: + has_version_tags = any(tag.name.startswith('v') for tag in GIT_REPO.tags) + + if target == 'html' and not local and has_version_tags: build_output = os.path.join(DOCS_BUILD_DIR, 'html') if incremental: @@ -590,22 +600,16 @@ def build_docs( create_latest_alias(build_output) create_root_redirect(build_output) + elif target == 'html' and not local and not has_version_tags: + logging.warning( + 'No version tags found in the repository. ' + 'Falling back to a single-version documentation build.' + ) + _run_sphinx_build(target) else: if target == 'html' and not skip_switcher: generate_switcher(include_current=include_current) - run_command( - [ - sys.executable, - '-m', - 'sphinx', - 'build', - '-M', - target, - DOCS_SOURCE_DIR, - DOCS_BUILD_DIR, - ], - ROOT_DIR, - ) + _run_sphinx_build(target) logging.info('Sphinx documentation built successfully.') except Exception as err: logging.error( From c155fd44eadcae8d5ff4ba1fc9e0ebe25b4196df Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 08:17:38 +0530 Subject: [PATCH 02/11] documentation updates --- README.md | 1 + docs/source/readme.rst | 3 +++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index f659df9..ebf42b7 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Then open http://localhost:8000 in your browser. The root automatically redirect - A `/latest/` directory points to the newest version - Root (`/`) automatically redirects to `/latest/` - Run `git fetch --tags` before building to ensure all version tags are available +- If no version tags (tags starting with `v`) are found in the repository, the build automatically falls back to a single-version Sphinx build instead of attempting the multi-version build ### Running the Formatter diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 02ec996..53aba8b 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1300,6 +1300,9 @@ Then open http://localhost:8000 in your browser. - A ``/latest/`` directory points to the newest version (symlink on Unix, copy on Windows) - Root (``/``) automatically redirects to ``/latest/`` for convenience - Version switcher dropdown in the navigation bar allows switching between versions +- If no version tags (tags starting with ``v``) are found in the repository, the build + automatically falls back to a single-version Sphinx build instead of attempting the + multi-version build Contributing ============ From 28d2e5b65ea5594e60b63734b7593cf38530c18d Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 08:26:56 +0530 Subject: [PATCH 03/11] Review comments --- run.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/run.py b/run.py index 8fea2e4..a2196eb 100644 --- a/run.py +++ b/run.py @@ -602,13 +602,19 @@ def build_docs( create_root_redirect(build_output) elif target == 'html' and not local and not has_version_tags: logging.warning( - 'No version tags found in the repository. ' + 'No version tags starting with \'v\' found in the repository. ' 'Falling back to a single-version documentation build.' ) _run_sphinx_build(target) else: if target == 'html' and not skip_switcher: - generate_switcher(include_current=include_current) + if has_version_tags: + generate_switcher(include_current=include_current) + else: + logging.warning( + 'No version tags starting with \'v\' found in the repository. ' + 'Skipping switcher.json generation.' + ) _run_sphinx_build(target) logging.info('Sphinx documentation built successfully.') except Exception as err: From 758245759a104ab80ea684f36a0ced7ba5f9fddf Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 09:35:22 +0530 Subject: [PATCH 04/11] lint fix --- run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/run.py b/run.py index a2196eb..eb32f4f 100644 --- a/run.py +++ b/run.py @@ -570,6 +570,7 @@ def _run_sphinx_build(target): ) +# pylint: disable=R0912 def build_docs( target, skip_build, local=False, skip_switcher=False, include_current=False, incremental=False ): From 3186ccb86b1ee2d8805b128ea66a8a624285bba6 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 09:56:51 +0530 Subject: [PATCH 05/11] stale switcher remover --- run.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/run.py b/run.py index eb32f4f..d6e4cce 100644 --- a/run.py +++ b/run.py @@ -570,6 +570,13 @@ def _run_sphinx_build(target): ) +def _remove_stale_switcher(): + """Remove any leftover switcher.json so a single-version build doesn't ship stale data.""" + if os.path.exists(SWITCHER_JSON): + os.remove(SWITCHER_JSON) + logging.info('Removed stale %s', SWITCHER_JSON) + + # pylint: disable=R0912 def build_docs( target, skip_build, local=False, skip_switcher=False, include_current=False, incremental=False @@ -606,6 +613,7 @@ def build_docs( 'No version tags starting with \'v\' found in the repository. ' 'Falling back to a single-version documentation build.' ) + _remove_stale_switcher() _run_sphinx_build(target) else: if target == 'html' and not skip_switcher: @@ -616,6 +624,7 @@ def build_docs( 'No version tags starting with \'v\' found in the repository. ' 'Skipping switcher.json generation.' ) + _remove_stale_switcher() _run_sphinx_build(target) logging.info('Sphinx documentation built successfully.') except Exception as err: From 476dfa2b5e5866f17c9870fe3ec83f70fe8729c3 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 10:38:08 +0530 Subject: [PATCH 06/11] Review comments --- README.md | 6 ++--- docs/source/conf.py | 2 +- docs/source/readme.rst | 15 ++++++------ run.py | 47 ++++++++++++++++++++++-------------- scripts/generate_switcher.py | 2 +- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ebf42b7..bc076f8 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ python run.py build python run.py build-docs ``` -The documentation version switcher (`switcher.json`) is automatically generated from git tags during the build process. Only tagged versions are included by default to ensure all links work correctly. +The documentation version switcher (`switcher.json`) is automatically generated from git tags matching the `vX.Y.Z` format during the build process. Only tagged versions are included by default to ensure all links work correctly. Options: - `--skip-build` (`-s`): Skip building before generating docs @@ -118,11 +118,11 @@ python -m http.server 8000 Then open http://localhost:8000 in your browser. The root automatically redirects to the latest version documentation. **Versioned Documentation:** -- Each git tag creates a separate documentation version (e.g., `/v26.0.5/`) +- Each git tag matching `vX.Y.Z` creates a separate documentation version (e.g., `/v26.0.5/`) - A `/latest/` directory points to the newest version - Root (`/`) automatically redirects to `/latest/` - Run `git fetch --tags` before building to ensure all version tags are available -- If no version tags (tags starting with `v`) are found in the repository, the build automatically falls back to a single-version Sphinx build instead of attempting the multi-version build +- If no version tags matching `vX.Y.Z` are found in the repository, the build automatically falls back to a single-version Sphinx build instead of attempting the multi-version build ### Running the Formatter diff --git a/docs/source/conf.py b/docs/source/conf.py index 71053fd..9adc10d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -34,7 +34,7 @@ ] templates_path = ['_templates'] -smv_tag_whitelist = r'^v?\d+\.\d+\.\d+$' +smv_tag_whitelist = r'^v\d+\.\d+\.\d+$' smv_branch_whitelist = r'^$' smv_remote_whitelist = r'^origin$' smv_latest_version = 'latest' diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 53aba8b..2d6c84e 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1254,8 +1254,9 @@ The project includes a ``run.py`` script with several useful commands: - ``python run.py test`` - Run tests - ``python run.py lint`` - Run code linting - ``python run.py format`` - Format code with black -- ``python run.py build-docs`` - Build versioned documentation (HTML uses git tags for the - navigation dropdown; run ``git fetch --tags`` locally before building) +- ``python run.py build-docs`` - Build versioned documentation (HTML uses git tags matching + ``vX.Y.Z`` for the navigation dropdown; run ``git fetch --tags`` locally before building). + Falls back to a single-version build when no matching version tags are found. - ``--skip-build`` (``-s``): Skip building the package before generating docs - ``--local`` (``-l``): Build documentation locally for a single version (skips multi-version build) @@ -1265,9 +1266,9 @@ The project includes a ``run.py`` script with several useful commands: .. note:: The documentation version switcher (``switcher.json``) is automatically generated from - git tags during the build process. Only tagged versions are included to ensure all - documentation links work correctly. If ``version.json`` is newer than the latest tag, - create a git tag to include it in the version switcher. + git tags matching the ``vX.Y.Z`` format during the build process. Only tagged versions + are included to ensure all documentation links work correctly. If ``version.json`` is + newer than the latest tag, create a git tag to include it in the version switcher. **Incremental Builds for Development:** @@ -1296,11 +1297,11 @@ Then open http://localhost:8000 in your browser. **Versioned Documentation Features:** -- Each git tag creates a separate documentation version (e.g., ``/v26.0.5/``) +- Each git tag matching ``vX.Y.Z`` creates a separate documentation version (e.g., ``/v26.0.5/``) - A ``/latest/`` directory points to the newest version (symlink on Unix, copy on Windows) - Root (``/``) automatically redirects to ``/latest/`` for convenience - Version switcher dropdown in the navigation bar allows switching between versions -- If no version tags (tags starting with ``v``) are found in the repository, the build +- If no version tags matching ``vX.Y.Z`` are found in the repository, the build automatically falls back to a single-version Sphinx build instead of attempting the multi-version build diff --git a/run.py b/run.py index d6e4cce..50de75c 100644 --- a/run.py +++ b/run.py @@ -28,7 +28,7 @@ build-docs Build the documentation. format Format all Python files in the repository using black. generate-expected-data Generate expected data for integration tests. - generate-switcher Generate switcher.json from git tags. + generate-switcher Generate switcher.json from git tags (vX.Y.Z format). install Install the moldflow-api package. install-package-requirements Install package dependencies. lint Lint all Python files in the repository. @@ -67,6 +67,7 @@ """ import os +import re import sys import json import logging @@ -118,6 +119,14 @@ PYTHON_FILES = [MOLDFLOW_DIR, DOCS_SOURCE_DIR, TEST_DIR, "run.py"] SWITCHER_JSON = os.path.join(DOCS_STATIC_DIR, 'switcher.json') +# Must match smv_tag_whitelist in docs/source/conf.py +VERSION_TAG_RE = re.compile(r'^v\d+\.\d+\.\d+$') + + +def _is_version_tag(name): + """Return True if *name* matches the vX.Y.Z version tag format.""" + return VERSION_TAG_RE.match(name) is not None + def run_command(args, cwd=os.getcwd(), extra_env=None): """Runs native executable command, args is an array of strings""" @@ -367,7 +376,7 @@ def create_root_redirect(build_output: str) -> None: def create_latest_alias(build_output: str) -> None: """Create a 'latest' alias pointing to the newest version using symlinks when possible.""" - version_dirs = [d for d in os.listdir(build_output) if d.startswith('v')] + version_dirs = [d for d in os.listdir(build_output) if _is_version_tag(d)] if not version_dirs: return @@ -429,9 +438,9 @@ def _build_html_docs_full(build_output, skip_switcher, include_current): "Failed to build documentation with " "sphinx_multiversion.\n" "This can happen if no Git tags or branches match " - "your version pattern.\n" + "the required vX.Y.Z version pattern.\n" "Try running 'git fetch --tags' and ensure version " - "tags exist in the repo.\n" + "tags (e.g. v1.2.3) exist in the repo.\n" "Underlying error: %s", str(err), ) @@ -445,11 +454,11 @@ def _get_missing_version_tags(build_output): if os.path.exists(build_output): for item in os.listdir(build_output): item_path = os.path.join(build_output, item) - if os.path.isdir(item_path) and item.startswith('v'): + if os.path.isdir(item_path) and _is_version_tag(item): if item != 'latest': existing_versions.add(item) - all_tags = {tag.name for tag in GIT_REPO.tags if tag.name.startswith('v')} + all_tags = {tag.name for tag in GIT_REPO.tags if _is_version_tag(tag.name)} missing = list(all_tags - existing_versions) @@ -570,11 +579,14 @@ def _run_sphinx_build(target): ) -def _remove_stale_switcher(): - """Remove any leftover switcher.json so a single-version build doesn't ship stale data.""" +def _clean_stale_docs(): + """Remove stale switcher.json from the source tree and wipe the build directory.""" if os.path.exists(SWITCHER_JSON): os.remove(SWITCHER_JSON) logging.info('Removed stale %s', SWITCHER_JSON) + if os.path.exists(DOCS_BUILD_DIR): + logging.info('Removing stale build directory %s', DOCS_BUILD_DIR) + shutil.rmtree(DOCS_BUILD_DIR) # pylint: disable=R0912 @@ -590,13 +602,12 @@ def build_docs( if not incremental: logging.info('Removing existing Sphinx documentation...') - if os.path.exists(DOCS_BUILD_DIR): - shutil.rmtree(DOCS_BUILD_DIR) + _clean_stale_docs() else: logging.info('Incremental build mode: preserving existing documentation...') try: - has_version_tags = any(tag.name.startswith('v') for tag in GIT_REPO.tags) + has_version_tags = any(_is_version_tag(tag.name) for tag in GIT_REPO.tags) if target == 'html' and not local and has_version_tags: build_output = os.path.join(DOCS_BUILD_DIR, 'html') @@ -610,10 +621,10 @@ def build_docs( create_root_redirect(build_output) elif target == 'html' and not local and not has_version_tags: logging.warning( - 'No version tags starting with \'v\' found in the repository. ' + 'No version tags matching vX.Y.Z found in the repository. ' 'Falling back to a single-version documentation build.' ) - _remove_stale_switcher() + _clean_stale_docs() _run_sphinx_build(target) else: if target == 'html' and not skip_switcher: @@ -621,10 +632,10 @@ def build_docs( generate_switcher(include_current=include_current) else: logging.warning( - 'No version tags starting with \'v\' found in the repository. ' + 'No version tags matching vX.Y.Z found in the repository. ' 'Skipping switcher.json generation.' ) - _remove_stale_switcher() + _clean_stale_docs() _run_sphinx_build(target) logging.info('Sphinx documentation built successfully.') except Exception as err: @@ -876,7 +887,7 @@ def _get_current_version_if_newer(): ) # Get latest git tag - tags = [tag.name for tag in GIT_REPO.tags if tag.name.startswith('v')] + tags = [tag.name for tag in GIT_REPO.tags if _is_version_tag(tag.name)] if not tags: return None @@ -898,8 +909,8 @@ def _get_current_version_if_newer(): def generate_switcher(include_current=False): - """Generate switcher.json from git tags""" - logging.info('Generating switcher.json from git tags') + """Generate switcher.json from git tags (vX.Y.Z format).""" + logging.info('Generating switcher.json from git tags (vX.Y.Z)') switcher_script = os.path.join(ROOT_DIR, 'scripts', 'generate_switcher.py') cmd = [sys.executable, switcher_script] if include_current: diff --git a/scripts/generate_switcher.py b/scripts/generate_switcher.py index c6c6eac..80646bd 100644 --- a/scripts/generate_switcher.py +++ b/scripts/generate_switcher.py @@ -24,7 +24,7 @@ # Must match smv_tag_whitelist in docs/source/conf.py -SMV_TAG_PATTERN = re.compile(r'^v?\d+\.\d+\.\d+$') +SMV_TAG_PATTERN = re.compile(r'^v\d+\.\d+\.\d+$') # Paths From 07d10b3d5d8c859a0d07a39bdbb0690d5216e5f5 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 10:48:54 +0530 Subject: [PATCH 07/11] test update --- scripts/generate_switcher.py | 6 +++--- tests/core/test_generate_switcher.py | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/generate_switcher.py b/scripts/generate_switcher.py index 80646bd..70e9d17 100644 --- a/scripts/generate_switcher.py +++ b/scripts/generate_switcher.py @@ -49,13 +49,13 @@ def get_git_tags(): def parse_version_tags(tags): """ - Filter tags to those matching strict X.Y.Z releases. + Filter tags to those matching the required vX.Y.Z format. Uses the same pattern as smv_tag_whitelist in docs/source/conf.py so that switcher.json stays in sync with the versions that - sphinx-multiversion actually builds. Accepts both vX.Y.Z and X.Y.Z. + sphinx-multiversion actually builds. - Returns the original tag strings (preserving any 'v' prefix). + Returns the matching tag strings. """ version_tags = [] diff --git a/tests/core/test_generate_switcher.py b/tests/core/test_generate_switcher.py index 2c7b98b..50c0732 100644 --- a/tests/core/test_generate_switcher.py +++ b/tests/core/test_generate_switcher.py @@ -37,19 +37,19 @@ @pytest.mark.scripts @pytest.mark.generate_switcher class TestParseVersionTags: - """Tests for tag filtering against the sphinx-multiversion whitelist.""" + """Tests for tag filtering against the vX.Y.Z version tag pattern.""" def test_accepts_v_prefixed_tags(self): tags = ['v1.0.0', 'v2.3.4', 'v27.0.0'] assert parse_version_tags(tags) == tags - def test_accepts_tags_without_v_prefix(self): + def test_rejects_tags_without_v_prefix(self): tags = ['1.0.0', '2.3.4', '27.0.0'] - assert parse_version_tags(tags) == tags + assert parse_version_tags(tags) == [] - def test_accepts_mixed_prefix_tags(self): + def test_filters_tags_without_v_prefix(self): tags = ['v1.0.0', '2.0.0', 'v3.1.2'] - assert parse_version_tags(tags) == tags + assert parse_version_tags(tags) == ['v1.0.0', 'v3.1.2'] def test_rejects_prerelease_tags(self): tags = ['v1.0.0rc1', 'v1.0.0a1', 'v1.0.0b2'] @@ -70,7 +70,7 @@ def test_rejects_non_version_tags(self): def test_filters_mixed_valid_and_invalid(self): tags = ['v1.0.0', 'v2.0.0rc1', 'latest', '3.0.0', 'v4.0.0.dev1'] - assert parse_version_tags(tags) == ['v1.0.0', '3.0.0'] + assert parse_version_tags(tags) == ['v1.0.0'] def test_empty_input(self): assert parse_version_tags([]) == [] @@ -95,9 +95,9 @@ def test_patch_ordering(self): tags = ['v1.0.2', 'v1.0.0', 'v1.0.1'] assert sort_versions(tags) == ['v1.0.2', 'v1.0.1', 'v1.0.0'] - def test_mixed_prefix_ordering(self): - tags = ['1.0.0', 'v2.0.0', '3.0.0'] - assert sort_versions(tags) == ['3.0.0', 'v2.0.0', '1.0.0'] + def test_many_versions_ordering(self): + tags = ['v1.0.0', 'v2.0.0', 'v3.0.0'] + assert sort_versions(tags) == ['v3.0.0', 'v2.0.0', 'v1.0.0'] def test_single_tag(self): assert sort_versions(['v1.0.0']) == ['v1.0.0'] @@ -138,10 +138,10 @@ def test_url_uses_tag_name(self, _): result = generate_switcher_json(['v1.0.0']) assert result[0]['url'] == '../v1.0.0/' - @patch(MOCK_VERSION_JSON, return_value='1.0.0') - def test_url_preserves_no_v_prefix(self, _): - result = generate_switcher_json(['1.0.0']) - assert result[0]['url'] == '../1.0.0/' + @patch(MOCK_VERSION_JSON, return_value='v1.0.0') + def test_url_preserves_v_prefix(self, _): + result = generate_switcher_json(['v1.0.0']) + assert result[0]['url'] == '../v1.0.0/' @patch(MOCK_VERSION_JSON, return_value='v2.0.0') def test_all_entries_have_required_keys(self, _): From a444497b8885147525cfd5026b77b9b27a0707f6 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 11:09:21 +0530 Subject: [PATCH 08/11] Adding multiversion artifact cleaner --- run.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/run.py b/run.py index 50de75c..635f120 100644 --- a/run.py +++ b/run.py @@ -579,14 +579,52 @@ def _run_sphinx_build(target): ) -def _clean_stale_docs(): - """Remove stale switcher.json from the source tree and wipe the build directory.""" +def _remove_stale_switcher(): + """Remove leftover switcher.json from the source tree. + + Prevents Sphinx from copying a stale version switcher into the build + output when no version tags exist to populate it. + """ if os.path.exists(SWITCHER_JSON): os.remove(SWITCHER_JSON) logging.info('Removed stale %s', SWITCHER_JSON) - if os.path.exists(DOCS_BUILD_DIR): - logging.info('Removing stale build directory %s', DOCS_BUILD_DIR) - shutil.rmtree(DOCS_BUILD_DIR) + + +def _clean_multiversion_artifacts(build_output): + """Selectively remove multi-version artifacts from a previous build. + + Removes vX.Y.Z/ directories, the latest/ alias, and the root redirect + index.html while preserving any single-version Sphinx output so that + incremental builds remain effective. + """ + if not os.path.isdir(build_output): + return + + removed = [] + + for item in os.listdir(build_output): + item_path = os.path.join(build_output, item) + if os.path.isdir(item_path) and _is_version_tag(item): + shutil.rmtree(item_path) + removed.append(item) + + latest_path = os.path.join(build_output, 'latest') + if os.path.islink(latest_path): + os.unlink(latest_path) + removed.append('latest (symlink)') + elif os.path.isdir(latest_path): + shutil.rmtree(latest_path) + removed.append('latest') + + redirect = os.path.join(build_output, 'index.html') + if os.path.isfile(redirect): + os.remove(redirect) + removed.append('index.html') + + if removed: + logging.info( + 'Cleaned multi-version artifacts from %s: %s', build_output, ', '.join(removed) + ) # pylint: disable=R0912 @@ -602,7 +640,8 @@ def build_docs( if not incremental: logging.info('Removing existing Sphinx documentation...') - _clean_stale_docs() + if os.path.exists(DOCS_BUILD_DIR): + shutil.rmtree(DOCS_BUILD_DIR) else: logging.info('Incremental build mode: preserving existing documentation...') @@ -624,7 +663,9 @@ def build_docs( 'No version tags matching vX.Y.Z found in the repository. ' 'Falling back to a single-version documentation build.' ) - _clean_stale_docs() + if not skip_switcher: + _remove_stale_switcher() + _clean_multiversion_artifacts(os.path.join(DOCS_BUILD_DIR, 'html')) _run_sphinx_build(target) else: if target == 'html' and not skip_switcher: @@ -635,7 +676,7 @@ def build_docs( 'No version tags matching vX.Y.Z found in the repository. ' 'Skipping switcher.json generation.' ) - _clean_stale_docs() + _remove_stale_switcher() _run_sphinx_build(target) logging.info('Sphinx documentation built successfully.') except Exception as err: From 0d2fde8c4a78a92b8640ad9d451d3439c9604319 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 11:29:13 +0530 Subject: [PATCH 09/11] handling switchers --- run.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/run.py b/run.py index 635f120..ec4b86b 100644 --- a/run.py +++ b/run.py @@ -593,9 +593,9 @@ def _remove_stale_switcher(): def _clean_multiversion_artifacts(build_output): """Selectively remove multi-version artifacts from a previous build. - Removes vX.Y.Z/ directories, the latest/ alias, and the root redirect - index.html while preserving any single-version Sphinx output so that - incremental builds remain effective. + Removes vX.Y.Z/ directories, the latest/ alias, the root redirect + index.html, and any distributed switcher.json copies while preserving + single-version Sphinx output so that incremental builds remain effective. """ if not os.path.isdir(build_output): return @@ -621,6 +621,12 @@ def _clean_multiversion_artifacts(build_output): os.remove(redirect) removed.append('index.html') + for switcher in glob.glob( + os.path.join(build_output, '**', '_static', 'switcher.json'), recursive=True + ): + os.remove(switcher) + removed.append(os.path.relpath(switcher, build_output)) + if removed: logging.info( 'Cleaned multi-version artifacts from %s: %s', build_output, ', '.join(removed) From 7cb11792b6b2c0bfd27c69a38069d7c4dd15ae22 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 11:35:29 +0530 Subject: [PATCH 10/11] removing latest comparison Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- run.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/run.py b/run.py index ec4b86b..9f6ea09 100644 --- a/run.py +++ b/run.py @@ -455,8 +455,7 @@ def _get_missing_version_tags(build_output): for item in os.listdir(build_output): item_path = os.path.join(build_output, item) if os.path.isdir(item_path) and _is_version_tag(item): - if item != 'latest': - existing_versions.add(item) + existing_versions.add(item) all_tags = {tag.name for tag in GIT_REPO.tags if _is_version_tag(tag.name)} From efed32c4049feb873f990aa2427d26ca7c99f12e Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 18 Mar 2026 13:43:11 +0530 Subject: [PATCH 11/11] test updates --- tests/core/test_generate_switcher.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/core/test_generate_switcher.py b/tests/core/test_generate_switcher.py index 50c0732..0b12c7b 100644 --- a/tests/core/test_generate_switcher.py +++ b/tests/core/test_generate_switcher.py @@ -138,10 +138,12 @@ def test_url_uses_tag_name(self, _): result = generate_switcher_json(['v1.0.0']) assert result[0]['url'] == '../v1.0.0/' - @patch(MOCK_VERSION_JSON, return_value='v1.0.0') - def test_url_preserves_v_prefix(self, _): - result = generate_switcher_json(['v1.0.0']) - assert result[0]['url'] == '../v1.0.0/' + @patch(MOCK_VERSION_JSON, return_value='v2.0.0') + def test_non_latest_url_uses_tag_name(self, _): + result = generate_switcher_json(['v1.0.0', 'v2.0.0']) + older = [e for e in result if e['version'] == 'v1.0.0'][0] + assert older['url'] == '../v1.0.0/' + assert older['is_latest'] is False @patch(MOCK_VERSION_JSON, return_value='v2.0.0') def test_all_entries_have_required_keys(self, _):