diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 55dad374..cdcb1e99 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 @@ -32,8 +34,8 @@ jobs: python -m pip install --upgrade pip pip install -r docs/requirements.txt - - name: Build Sphinx HTML - run: sphinx-build -b html docs docs/_build/html + - name: Build versioned docs site + run: python ci_scripts/build_versioned_docs.py - name: Setup GitHub Pages uses: actions/configure-pages@v3 @@ -41,7 +43,7 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: docs/_build/html + path: docs/_build/site deploy: needs: build diff --git a/Makefile b/Makefile index 7fd799e2..e3830ac8 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ stubgen: sed -i 's/ q_home: numpy\.ndarray\[tuple\[M\], numpy\.dtype\[numpy\.float64\]\] | None/ q_home: numpy.ndarray | None/' python/rcs/_core/common.pyi python -c "from pathlib import Path; p=Path('python/rcs/_core/common.pyi'); t=p.read_text(); t=t.replace('numpy.ndarray[tuple[typing.Literal[2], N], numpy.dtype[numpy.float64]]', 'numpy.ndarray[tuple[typing.Literal[2], typing.Any], numpy.dtype[numpy.float64]]'); p.write_text(t)" python -c "from pathlib import Path; p=Path('python/rcs/_core/sim.pyi'); t=p.read_text(); t=t.replace('numpy.ndarray[tuple[typing.Literal[2], N], numpy.dtype[numpy.float64]]', 'numpy.ndarray[tuple[typing.Literal[2], typing.Any], numpy.dtype[numpy.float64]]'); t=t.replace(', max_buffer_frames: int = 100', ''); p.write_text(t)" - python scripts/generate_common_typing.py + python ci_scripts/generate_common_typing.py ruff check --fix python/rcs/_core python/rcs/common_typing.py isort python/rcs/_core python/rcs/common_typing.py black python/rcs/_core python/rcs/common_typing.py diff --git a/ci_scripts/build_versioned_docs.py b/ci_scripts/build_versioned_docs.py new file mode 100644 index 00000000..00ca6ad8 --- /dev/null +++ b/ci_scripts/build_versioned_docs.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import tempfile +import tomllib +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +DOCS_DIR = REPO_ROOT / "docs" +OUTPUT_DIR = DOCS_DIR / "_build" / "site" +BASE_URL = os.environ.get("RCS_DOCS_BASE_URL", "https://robotcontrolstack.org").rstrip("/") +TAG_FILTER = [tag.strip() for tag in os.environ.get("RCS_DOCS_TAGS", "").split(",") if tag.strip()] + + +def run(*args: str, cwd: Path = REPO_ROOT, env: dict[str, str] | None = None) -> str: + result = subprocess.run(args, cwd=cwd, env=env, check=True, text=True, capture_output=True) + return result.stdout.strip() + + +def list_release_tags() -> list[str]: + if TAG_FILTER: + return TAG_FILTER + tags = run("git", "tag", "--list", "v*", "--sort=-version:refname") + return [tag for tag in tags.splitlines() if tag] + + +def has_docs(ref: str) -> bool: + try: + run("git", "cat-file", "-e", f"{ref}:docs/conf.py") + return True + except subprocess.CalledProcessError: + return False + + +def read_release(repo_root: Path) -> str: + with (repo_root / "pyproject.toml").open("rb") as f: + return tomllib.load(f)["project"]["version"] + + +def build_docs(repo_root: Path, output_dir: Path, version_match: str) -> None: + env = os.environ.copy() + env["RCS_DOCS_VERSION"] = version_match + env["RCS_DOCS_RELEASE"] = read_release(repo_root) + subprocess.run( + ["sphinx-build", "-b", "html", "docs", str(output_dir)], + cwd=repo_root, + env=env, + check=True, + text=True, + ) + + +def overwrite_switcher_json(site_dir: Path, entries: list[dict[str, str]]) -> None: + payload = json.dumps(entries, indent=4) + "\n" + for root in [site_dir, site_dir / "latest", *[p for p in site_dir.iterdir() if p.is_dir() and p.name not in {"latest", "_sources", "_static"}]]: + static_dir = root / "_static" + if static_dir.exists(): + (static_dir / "version_switcher.json").write_text(payload) + + +def main() -> None: + if OUTPUT_DIR.exists(): + shutil.rmtree(OUTPUT_DIR) + OUTPUT_DIR.mkdir(parents=True) + + entries: list[dict[str, str]] = [ + { + "name": "latest", + "version": "latest", + "url": f"{BASE_URL}/", + } + ] + + with tempfile.TemporaryDirectory(prefix="rcs-docs-versioned-") as temp_dir_str: + temp_dir = Path(temp_dir_str) + + latest_dir = OUTPUT_DIR / "latest" + build_docs(REPO_ROOT, latest_dir, "latest") + + for tag in list_release_tags(): + if not has_docs(tag): + continue + + worktree_dir = temp_dir / tag + subprocess.run(["git", "worktree", "add", "--detach", str(worktree_dir), tag], cwd=REPO_ROOT, check=True) + try: + release = read_release(worktree_dir) + release_dir = OUTPUT_DIR / release + build_docs(worktree_dir, release_dir, release) + entries.append( + { + "name": release, + "version": release, + "url": f"{BASE_URL}/{release}/", + } + ) + finally: + subprocess.run(["git", "worktree", "remove", "--force", str(worktree_dir)], cwd=REPO_ROOT, check=True) + + shutil.copytree(latest_dir, OUTPUT_DIR, dirs_exist_ok=True) + overwrite_switcher_json(OUTPUT_DIR, entries) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_common_typing.py b/ci_scripts/generate_common_typing.py similarity index 100% rename from scripts/generate_common_typing.py rename to ci_scripts/generate_common_typing.py diff --git a/docs/_static/version_switcher.json b/docs/_static/version_switcher.json index b51d0254..053bb50b 100644 --- a/docs/_static/version_switcher.json +++ b/docs/_static/version_switcher.json @@ -3,5 +3,25 @@ "name": "latest", "version": "latest", "url": "https://robotcontrolstack.org/" + }, + { + "name": "0.6.3", + "version": "0.6.3", + "url": "https://robotcontrolstack.org/0.6.3/" + }, + { + "name": "0.6.2", + "version": "0.6.2", + "url": "https://robotcontrolstack.org/0.6.2/" + }, + { + "name": "0.6.1", + "version": "0.6.1", + "url": "https://robotcontrolstack.org/0.6.1/" + }, + { + "name": "0.6.0", + "version": "0.6.0", + "url": "https://robotcontrolstack.org/0.6.0/" } -] \ No newline at end of file +] diff --git a/docs/conf.py b/docs/conf.py index 2049f264..62ca3302 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,14 +1,23 @@ import os import sys +import tomllib +from pathlib import Path # inject path to rcs package to enable autodoc/autoapi to find packages sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../python"))) +ROOT_DIR = Path(__file__).resolve().parents[1] + project = "Robot Control Stack" copyright = "2025, RCS Contributors" author = "Tobias Jülg" -release = "0.5.2" -version = "0.5.2" + +with (ROOT_DIR / "pyproject.toml").open("rb") as f: + _pyproject = tomllib.load(f) + +release = os.environ.get("RCS_DOCS_RELEASE", _pyproject["project"]["version"]) +version = release +_docs_version_match = os.environ.get("RCS_DOCS_VERSION", "latest") extensions = [ "sphinx.ext.autodoc", @@ -50,7 +59,7 @@ "show_version_warning_banner": False, "switcher": { "json_url": "/_static/version_switcher.json", - "version_match": "latest", + "version_match": _docs_version_match, }, }