Skip to content

Commit 99090e8

Browse files
committed
docs: publish versioned documentation
1 parent f032dcc commit 99090e8

4 files changed

Lines changed: 147 additions & 7 deletions

File tree

‎.github/workflows/docs.yml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
steps:
2222
- name: Checkout
2323
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
2426

2527
- name: Set up Python
2628
uses: actions/setup-python@v5
@@ -32,16 +34,16 @@ jobs:
3234
python -m pip install --upgrade pip
3335
pip install -r docs/requirements.txt
3436
35-
- name: Build Sphinx HTML
36-
run: sphinx-build -b html docs docs/_build/html
37+
- name: Build versioned docs site
38+
run: python scripts/build_versioned_docs.py
3739

3840
- name: Setup GitHub Pages
3941
uses: actions/configure-pages@v3
4042

4143
- name: Upload artifact
4244
uses: actions/upload-pages-artifact@v3
4345
with:
44-
path: docs/_build/html
46+
path: docs/_build/site
4547

4648
deploy:
4749
needs: build

‎docs/_static/version_switcher.json‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,25 @@
33
"name": "latest",
44
"version": "latest",
55
"url": "https://robotcontrolstack.org/"
6+
},
7+
{
8+
"name": "0.6.3",
9+
"version": "0.6.3",
10+
"url": "https://robotcontrolstack.org/0.6.3/"
11+
},
12+
{
13+
"name": "0.6.2",
14+
"version": "0.6.2",
15+
"url": "https://robotcontrolstack.org/0.6.2/"
16+
},
17+
{
18+
"name": "0.6.1",
19+
"version": "0.6.1",
20+
"url": "https://robotcontrolstack.org/0.6.1/"
21+
},
22+
{
23+
"name": "0.6.0",
24+
"version": "0.6.0",
25+
"url": "https://robotcontrolstack.org/0.6.0/"
626
}
7-
]
27+
]

‎docs/conf.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import os
22
import sys
3+
import tomllib
4+
from pathlib import Path
35

46
# inject path to rcs package to enable autodoc/autoapi to find packages
57
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../python")))
68

9+
ROOT_DIR = Path(__file__).resolve().parents[1]
10+
711
project = "Robot Control Stack"
812
copyright = "2025, RCS Contributors"
913
author = "Tobias Jülg"
10-
release = "0.5.2"
11-
version = "0.5.2"
14+
15+
with (ROOT_DIR / "pyproject.toml").open("rb") as f:
16+
_pyproject = tomllib.load(f)
17+
18+
release = os.environ.get("RCS_DOCS_RELEASE", _pyproject["project"]["version"])
19+
version = release
20+
_docs_version_match = os.environ.get("RCS_DOCS_VERSION", "latest")
1221

1322
extensions = [
1423
"sphinx.ext.autodoc",
@@ -50,7 +59,7 @@
5059
"show_version_warning_banner": False,
5160
"switcher": {
5261
"json_url": "/_static/version_switcher.json",
53-
"version_match": "latest",
62+
"version_match": _docs_version_match,
5463
},
5564
}
5665

‎scripts/build_versioned_docs.py‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import json
5+
import os
6+
import shutil
7+
import subprocess
8+
import tempfile
9+
import tomllib
10+
from pathlib import Path
11+
12+
REPO_ROOT = Path(__file__).resolve().parents[1]
13+
DOCS_DIR = REPO_ROOT / "docs"
14+
OUTPUT_DIR = DOCS_DIR / "_build" / "site"
15+
BASE_URL = os.environ.get("RCS_DOCS_BASE_URL", "https://robotcontrolstack.org").rstrip("/")
16+
TAG_FILTER = [tag.strip() for tag in os.environ.get("RCS_DOCS_TAGS", "").split(",") if tag.strip()]
17+
18+
19+
def run(*args: str, cwd: Path = REPO_ROOT, env: dict[str, str] | None = None) -> str:
20+
result = subprocess.run(args, cwd=cwd, env=env, check=True, text=True, capture_output=True)
21+
return result.stdout.strip()
22+
23+
24+
def list_release_tags() -> list[str]:
25+
if TAG_FILTER:
26+
return TAG_FILTER
27+
tags = run("git", "tag", "--list", "v*", "--sort=-version:refname")
28+
return [tag for tag in tags.splitlines() if tag]
29+
30+
31+
def has_docs(ref: str) -> bool:
32+
try:
33+
run("git", "cat-file", "-e", f"{ref}:docs/conf.py")
34+
return True
35+
except subprocess.CalledProcessError:
36+
return False
37+
38+
39+
def read_release(repo_root: Path) -> str:
40+
with (repo_root / "pyproject.toml").open("rb") as f:
41+
return tomllib.load(f)["project"]["version"]
42+
43+
44+
def build_docs(repo_root: Path, output_dir: Path, version_match: str) -> None:
45+
env = os.environ.copy()
46+
env["RCS_DOCS_VERSION"] = version_match
47+
env["RCS_DOCS_RELEASE"] = read_release(repo_root)
48+
subprocess.run(
49+
["sphinx-build", "-b", "html", "docs", str(output_dir)],
50+
cwd=repo_root,
51+
env=env,
52+
check=True,
53+
text=True,
54+
)
55+
56+
57+
def overwrite_switcher_json(site_dir: Path, entries: list[dict[str, str]]) -> None:
58+
payload = json.dumps(entries, indent=4) + "\n"
59+
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"}]]:
60+
static_dir = root / "_static"
61+
if static_dir.exists():
62+
(static_dir / "version_switcher.json").write_text(payload)
63+
64+
65+
def main() -> None:
66+
if OUTPUT_DIR.exists():
67+
shutil.rmtree(OUTPUT_DIR)
68+
OUTPUT_DIR.mkdir(parents=True)
69+
70+
entries: list[dict[str, str]] = [
71+
{
72+
"name": "latest",
73+
"version": "latest",
74+
"url": f"{BASE_URL}/",
75+
}
76+
]
77+
78+
with tempfile.TemporaryDirectory(prefix="rcs-docs-versioned-") as temp_dir_str:
79+
temp_dir = Path(temp_dir_str)
80+
81+
latest_dir = OUTPUT_DIR / "latest"
82+
build_docs(REPO_ROOT, latest_dir, "latest")
83+
84+
for tag in list_release_tags():
85+
if not has_docs(tag):
86+
continue
87+
88+
worktree_dir = temp_dir / tag
89+
subprocess.run(["git", "worktree", "add", "--detach", str(worktree_dir), tag], cwd=REPO_ROOT, check=True)
90+
try:
91+
release = read_release(worktree_dir)
92+
release_dir = OUTPUT_DIR / release
93+
build_docs(worktree_dir, release_dir, release)
94+
entries.append(
95+
{
96+
"name": release,
97+
"version": release,
98+
"url": f"{BASE_URL}/{release}/",
99+
}
100+
)
101+
finally:
102+
subprocess.run(["git", "worktree", "remove", "--force", str(worktree_dir)], cwd=REPO_ROOT, check=True)
103+
104+
shutil.copytree(latest_dir, OUTPUT_DIR, dirs_exist_ok=True)
105+
overwrite_switcher_json(OUTPUT_DIR, entries)
106+
107+
108+
if __name__ == "__main__":
109+
main()

0 commit comments

Comments
 (0)