Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,16 +34,16 @@ 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

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/_build/html
path: docs/_build/site

deploy:
needs: build
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 109 additions & 0 deletions ci_scripts/build_versioned_docs.py
Original file line number Diff line number Diff line change
@@ -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()
File renamed without changes.
22 changes: 21 additions & 1 deletion docs/_static/version_switcher.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
}
]
]
15 changes: 12 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -50,7 +59,7 @@
"show_version_warning_banner": False,
"switcher": {
"json_url": "/_static/version_switcher.json",
"version_match": "latest",
"version_match": _docs_version_match,
},
}

Expand Down
Loading