From 1073f61e0e9a1ff9db39863906c4b1bccfd9019b Mon Sep 17 00:00:00 2001 From: AbhiRKeesara Date: Thu, 17 Sep 2026 00:49:20 -0700 Subject: [PATCH] DOC: Add version switcher to documentation Add a version dropdown to the documentation navbar, modeled after NumPy's official docs pattern. This allows users to easily switch between different documentation versions. Changes: - Add versions.json with entries for dev, 1.1.0, 0.2.0, 0.1.0 - Update conf.py with html_theme_options for version switcher - Add GitHub icon link to navbar - Update copyright year to dynamic range (2005-current) per LICENSE.txt - Extend publish_docs_to_pages.yml to: - Build versioned docs on release tags (v*) - Auto-update versions.json when new releases are published - Deploy dev docs to /dev/ and releases to /version/X.Y.Z/ - Update 'latest' symlink to point to newest release - Add root index.html redirect to /latest/ - Update RELEASE.md to reflect automated docs deployment Closes #78 --- .github/workflows/publish_docs_to_pages.yml | 159 ++++++++++++++++++-- RELEASE.md | 32 ++-- doc/source/_static/versions.json | 13 ++ doc/source/conf.py | 42 +++++- 4 files changed, 210 insertions(+), 36 deletions(-) create mode 100644 doc/source/_static/versions.json diff --git a/.github/workflows/publish_docs_to_pages.yml b/.github/workflows/publish_docs_to_pages.yml index 5eb67ae..1903442 100644 --- a/.github/workflows/publish_docs_to_pages.yml +++ b/.github/workflows/publish_docs_to_pages.yml @@ -4,12 +4,14 @@ on: push: branches: - main + tags: + - v* permissions: contents: write jobs: - build: + build-and-deploy: if: github.repository_owner == 'numpy' runs-on: ubuntu-latest steps: @@ -24,32 +26,157 @@ jobs: run: | python -m pip install --upgrade pip pip install .[doc] + - name: Determine docs version + id: version + run: | + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + # Extract version from tag (e.g., v1.2.0 -> 1.2.0) + VERSION="${GITHUB_REF#refs/tags/v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "is_release=true" >> $GITHUB_OUTPUT + echo "docs_path=version/$VERSION" >> $GITHUB_OUTPUT + else + echo "version=dev" >> $GITHUB_OUTPUT + echo "is_release=false" >> $GITHUB_OUTPUT + echo "docs_path=dev" >> $GITHUB_OUTPUT + fi - name: Build documentation with Sphinx + env: + DOCS_VERSION: ${{ steps.version.outputs.version }} run: | cd doc make html - mv build/html /tmp + mv build/html /tmp/html cd .. - name: Deploy to gh-pages branch + env: + DOCS_PATH: ${{ steps.version.outputs.docs_path }} + IS_RELEASE: ${{ steps.version.outputs.is_release }} + VERSION: ${{ steps.version.outputs.version }} run: | git config user.name "${GITHUB_ACTOR}" git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" echo "Checking out gh-pages branch" git fetch origin gh-pages git checkout gh-pages - echo "Removing old dev documentation" - git clean -xdf . - cd dev - git rm -r '*' - cd .. - echo "Committing emptied dev directory" - git commit -m "Remove old dev documentation" - mkdir -p dev - cd dev - echo "Copying /tmp/html here" - cp -v -r /tmp/html/* . - echo "Adding new dev documentation" + + echo "Deploying docs to $DOCS_PATH" + + if [ "$IS_RELEASE" = "true" ]; then + # For releases, create new version directory + mkdir -p "$DOCS_PATH" + rm -rf "${DOCS_PATH:?}"/* + else + # For dev builds, clean and recreate dev directory + git clean -xdf . + if [ -d "dev" ]; then + cd dev + git rm -r '*' || true + cd .. + git commit -m "Remove old dev documentation" || true + fi + mkdir -p dev + fi + + # Copy the built docs + cp -r /tmp/html/* "$DOCS_PATH/" + + # For releases, update versions.json at the root of gh-pages + if [ "$IS_RELEASE" = "true" ]; then + echo "Updating versions.json for release $VERSION" + python3 << 'EOF' + import json + import os + + version = os.environ['VERSION'] + versions_file = '_static/versions.json' + + # Read existing versions.json or start fresh + if os.path.exists(versions_file): + with open(versions_file, 'r') as f: + versions = json.load(f) + else: + versions = [] + + # Create entry for new version + new_entry = { + "name": version, + "version": version, + "url": f"https://numpy.org/numpy-financial/version/{version}/" + } + + # Check if this version already exists (update it) or add new + version_exists = False + for v in versions: + if v.get('version') == version: + v.update(new_entry) + version_exists = True + break + + if not version_exists: + # Insert after dev (index 1) or at the beginning if no dev + dev_index = next((i for i, v in enumerate(versions) if v.get('version') == 'dev'), -1) + insert_index = dev_index + 1 if dev_index >= 0 else 0 + versions.insert(insert_index, new_entry) + + # Update "preferred" flag - newest release should be preferred + # First, remove preferred from all non-dev entries + for v in versions: + if v.get('version') != 'dev' and 'preferred' in v: + del v['preferred'] + + # Find the highest version (excluding dev) and mark it as preferred + from packaging.version import Version, InvalidVersion + release_versions = [] + for v in versions: + ver = v.get('version', '') + if ver != 'dev': + try: + release_versions.append((Version(ver), v)) + except InvalidVersion: + pass + + if release_versions: + release_versions.sort(key=lambda x: x[0], reverse=True) + highest = release_versions[0][1] + highest['preferred'] = True + # Update the name to indicate stable + highest['name'] = f"{highest['version']} (stable)" + + # Ensure dev is always first + versions.sort(key=lambda v: (0 if v.get('version') == 'dev' else 1, v.get('version', '')), reverse=False) + # Re-sort releases by version descending (dev stays first) + dev_entry = [v for v in versions if v.get('version') == 'dev'] + release_entries = [v for v in versions if v.get('version') != 'dev'] + try: + release_entries.sort(key=lambda v: Version(v.get('version', '0')), reverse=True) + except: + pass + versions = dev_entry + release_entries + + # Write updated versions.json + os.makedirs('_static', exist_ok=True) + with open(versions_file, 'w') as f: + json.dump(versions, f, indent=4) + EOF + + # Also copy versions.json to the new version's _static directory + # so it's available when viewing that version's docs + cp _static/versions.json "$DOCS_PATH/_static/versions.json" + + # Update 'latest' symlink to point to the new version + rm -f latest + ln -s "version/$VERSION" latest + git add latest + fi + + # Stage and commit changes git add -A . - git commit -m "Add new dev documentation" - echo "Pushing new documentation to origin" + if [ "$IS_RELEASE" = "true" ]; then + git commit -m "Add documentation for version $VERSION" + else + git commit -m "Update dev documentation" + fi + + echo "Pushing documentation to origin" git push -v origin gh-pages diff --git a/RELEASE.md b/RELEASE.md index d108c16..a475c7d 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -77,25 +77,19 @@ PyPI trusted publishing. Do not build or upload artifacts by hand. - paste the contents of ${CHANGES} in the `Describe this release section` - if pre-release check the box labelled `Set as a pre-release` -- Publish the documentation for the release. A GH workflow builds - `dev/` from `main`; the released versions have to be published by - hand. Skip this step for a pre-release. - - - wait for the workflow to rebuild `dev/` from the release - commit, and check that https://numpy.org/numpy-financial/dev/ - shows ${VERSION}. Do this before the version bump below, which - returns `dev/` to a development version. - - - in a clone of the `gh-pages` branch, copy those docs to their - permanent location and point `latest` at them: - - cp -r dev version/${VERSION} - git add version/${VERSION} - git rm -rf latest - ln -s version/${VERSION} latest - git add latest - - - commit and push `gh-pages` +- Publish the documentation for the release. The `Publish docs to gh-pages` + workflow automatically builds and deploys versioned documentation when + a `v*` tag is pushed: + + - Dev docs are deployed to `/dev/` from `main` branch pushes + - Release docs are deployed to `/version/${VERSION}/` from `v*` tags + - The workflow automatically updates `_static/versions.json` with the + new version and marks the highest version as stable + + Verify the deployment: + - Check https://numpy.org/numpy-financial/version/${VERSION}/ shows + the new version's documentation + - Check the version switcher dropdown includes the new version - Update https://github.com/numpy/numpy-financial/milestones: diff --git a/doc/source/_static/versions.json b/doc/source/_static/versions.json new file mode 100644 index 0000000..eee7e38 --- /dev/null +++ b/doc/source/_static/versions.json @@ -0,0 +1,13 @@ +[ + { + "name": "dev", + "version": "dev", + "url": "https://numpy.org/numpy-financial/dev/" + }, + { + "name": "1.1.0 (stable)", + "version": "1.1.0", + "url": "https://numpy.org/numpy-financial/version/1.1.0/", + "preferred": true + } +] diff --git a/doc/source/conf.py b/doc/source/conf.py index d564272..821971a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -4,6 +4,9 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +from datetime import datetime + import numpy_financial # -- Path setup -------------------------------------------------------------- @@ -20,7 +23,7 @@ # -- Project information ----------------------------------------------------- project = 'numpy-financial' -copyright = '2023, numpy-financial developers' +copyright = f'2005-{datetime.now().year}, NumPy Developers' author = 'numpy-financial developers' @@ -61,3 +64,40 @@ html_logo = "_static/numpy_financial_logov.svg" html_favicon = "_static/numpy_financial_favicon.png" + +# -- Version switcher configuration ------------------------------------------ + +# Determine the version string for the switcher. +# Must match a "version" field in versions.json for the dropdown to show it selected. +# +# In production (CI), DOCS_VERSION is set explicitly: +# - "dev" for main branch builds (deployed to /dev/) +# - "X.Y.Z" for release tag builds (deployed to /version/X.Y.Z/) +# +# For local development, default to showing stable version to test the typical UX. +if os.environ.get("DOCS_VERSION"): + # CI sets this explicitly based on branch/tag + switcher_version = os.environ["DOCS_VERSION"] +else: + # Local development: show stable version (1.1.0) as default for UX testing + # This matches what users see when visiting the main docs site + switcher_version = "1.1.0" + +# For local development, use relative path to test version switcher UI. +# In CI/production, use the absolute URL. +if os.environ.get("READTHEDOCS") or os.environ.get("CI"): + json_url = "https://numpy.org/numpy-financial/_static/versions.json" +else: + # Local development: use relative path (requires serving with http server) + json_url = "_static/versions.json" + +html_theme_options = { + "github_url": "https://github.com/numpy/numpy-financial", + # Navbar layout: theme switcher, version switcher, then GitHub icon + "navbar_end": ["theme-switcher", "version-switcher", "navbar-icon-links"], + "switcher": { + "json_url": json_url, + "version_match": switcher_version, + }, + "show_version_warning_banner": True, +}