Skip to content

Commit 94e5601

Browse files
committed
fix(docs): replace sphinx-multiversion with plain sphinx-build per ref
The entire sphinx-multiversion ecosystem (original + forks) is broken with Sphinx 7+ due to an incompatible Config.read() API change. Replace it with a CI workflow that runs sphinx-build once per matching tag and once for main, producing the same versioned output structure. - Remove sphinx-multiversion-contrib dependency - Remove smv_* config from conf.py - Rewrite CI workflow to checkout and build each ref with sphinx-build - Generate versions.json for the sidebar version switcher - Template reads versions.json via fetch instead of Jinja2 context - Remove docs-multi Makefile target (versioned builds are CI-only)
1 parent de976a3 commit 94e5601

5 files changed

Lines changed: 127 additions & 51 deletions

File tree

.github/workflows/docs.yml

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,36 +46,103 @@ jobs:
4646
- name: Install docs dependencies
4747
run: pip install -r docs/requirements.txt
4848

49-
- name: Copy example notebooks
50-
run: python docs/copy_notebooks.py
51-
5249
- name: Build versioned documentation
53-
run: sphinx-multiversion docs docs/_build/html
54-
55-
- name: Create root redirect
5650
run: |
57-
# Find the latest tag that matches our whitelist, fall back to main
58-
LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^v0\.4\.([1-9][0-9]*)$|^v0\.([5-9]|[0-9]{2,})\.[0-9]+$|^v[1-9][0-9]*\.[0-9]+\.[0-9]+$' | head -1)
51+
set -e
52+
OUTPUT_DIR="docs/_build/html"
53+
mkdir -p "$OUTPUT_DIR"
54+
55+
# Tag regex: match v0.4.1+ and v1.0.0+ (v0.4.0 and earlier lack docs/)
56+
TAG_REGEX='^v0\.4\.([1-9][0-9]*)$|^v0\.([5-9]|[0-9]{2,})\.[0-9]+$|^v[1-9][0-9]*\.[0-9]+\.[0-9]+$'
57+
58+
# Collect matching tags (newest first)
59+
TAGS=$(git tag --sort=-v:refname | grep -E "$TAG_REGEX" || true)
60+
61+
# Track versions for versions.json
62+
VERSIONS_JSON='[]'
63+
LATEST_TAG=""
64+
65+
# Build docs for each matching tag
66+
for tag in $TAGS; do
67+
echo "=== Building docs for tag: $tag ==="
68+
# Check if this tag has a docs/ directory
69+
if ! git ls-tree --name-only "$tag" -- docs/ > /dev/null 2>&1; then
70+
echo " Skipping $tag (no docs/ directory)"
71+
continue
72+
fi
73+
74+
git checkout "$tag" -- docs/ examples/ || git checkout "$tag" -- docs/
75+
python docs/copy_notebooks.py || true
76+
sphinx-build -b html docs "$OUTPUT_DIR/$tag"
77+
git checkout HEAD -- docs/ examples/ 2>/dev/null || git checkout HEAD -- docs/
78+
79+
if [ -z "$LATEST_TAG" ]; then
80+
LATEST_TAG="$tag"
81+
fi
82+
VERSIONS_JSON=$(echo "$VERSIONS_JSON" | python3 -c "
83+
import json, sys
84+
v = json.load(sys.stdin)
85+
v.append({'name': '$tag', 'tag': True})
86+
print(json.dumps(v))")
87+
done
88+
89+
# Build docs for main (current HEAD)
90+
echo "=== Building docs for main ==="
91+
git checkout HEAD -- docs/ examples/ 2>/dev/null || true
92+
python docs/copy_notebooks.py
93+
sphinx-build -b html docs "$OUTPUT_DIR/main"
94+
95+
VERSIONS_JSON=$(echo "$VERSIONS_JSON" | python3 -c "
96+
import json, sys
97+
v = json.load(sys.stdin)
98+
v.append({'name': 'main', 'tag': False})
99+
print(json.dumps(v))")
100+
101+
# Determine redirect target
59102
TARGET="${LATEST_TAG:-main}"
60-
cat > docs/_build/html/index.html << 'REDIRECT_EOF'
103+
104+
# Write versions.json
105+
python3 -c "
106+
import json
107+
versions = json.loads('$VERSIONS_JSON')
108+
data = {'versions': versions, 'latest': '${LATEST_TAG}', 'current': ''}
109+
with open('$OUTPUT_DIR/versions.json', 'w') as f:
110+
json.dump(data, f, indent=2)
111+
"
112+
113+
# Inject current version into each build's versions.json copy
114+
for dir in "$OUTPUT_DIR"/*/; do
115+
name=$(basename "$dir")
116+
cp "$OUTPUT_DIR/versions.json" "$dir/versions.json"
117+
python3 -c "
118+
import json
119+
with open('$dir/versions.json') as f:
120+
data = json.load(f)
121+
data['current'] = '$name'
122+
with open('$dir/versions.json', 'w') as f:
123+
json.dump(data, f, indent=2)
124+
"
125+
done
126+
127+
# Create root redirect
128+
cat > "$OUTPUT_DIR/index.html" << REDIRECT_EOF
61129
<!DOCTYPE html>
62130
<html>
63131
<head>
64132
<meta charset="utf-8">
65133
<title>Redirecting...</title>
66-
<script>
67-
// Redirect to latest tagged version, or main if none exist
68-
var defined_target = "DEFINED_TARGET_PLACEHOLDER";
69-
window.location.href = defined_target + "/index.html";
70-
</script>
71-
<meta http-equiv="refresh" content="0; url=DEFINED_TARGET_PLACEHOLDER/index.html">
134+
<meta http-equiv="refresh" content="0; url=${TARGET}/index.html">
72135
</head>
73136
<body>
74-
<p>Redirecting to <a href="DEFINED_TARGET_PLACEHOLDER/index.html">latest documentation</a>...</p>
137+
<p>Redirecting to <a href="${TARGET}/index.html">latest documentation</a>...</p>
75138
</body>
76139
</html>
77140
REDIRECT_EOF
78-
sed -i "s|DEFINED_TARGET_PLACEHOLDER|${TARGET}|g" docs/_build/html/index.html
141+
142+
echo "=== Build complete ==="
143+
echo "Versions built:"
144+
ls -d "$OUTPUT_DIR"/*/
145+
echo "Root redirects to: $TARGET"
79146
80147
- name: Upload artifact
81148
uses: actions/upload-pages-artifact@v3

Makefile

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: install format lint test test-all test-sentinel clean redis-start redis-stop check-types check docs docs-clean docs-serve docs-multi
1+
.PHONY: install format lint test test-all test-sentinel clean redis-start redis-stop check-types check docs docs-clean docs-serve
22

33
install:
44
poetry install --all-extras
@@ -49,10 +49,6 @@ docs-clean:
4949
rm -rf docs/_build
5050
rm -rf docs/examples/checkpoints docs/examples/human_in_the_loop docs/examples/memory docs/examples/middleware docs/examples/react_agent
5151

52-
docs-multi:
53-
python docs/copy_notebooks.py
54-
sphinx-multiversion docs docs/_build/html
55-
5652
docs-serve: docs
5753
python -m http.server 8085 --directory docs/_build/html
5854

docs/_templates/versioning.html

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,46 @@
1-
{% if versions %}
21
<div class="version-switcher">
32
<h4>Version</h4>
4-
<select onchange="if (this.value) window.location.href = this.value;">
5-
{%- for item in versions.tags %}
6-
<option
7-
value="{{ vpathto(item.name) }}"
8-
{% if item.name == current_version.name %}selected{% endif %}
9-
>
10-
{{ item.name }}{% if loop.first %} (latest){% endif %}
11-
</option>
12-
{%- endfor %}
13-
{%- for item in versions.branches %}
14-
<option
15-
value="{{ vpathto(item.name) }}"
16-
{% if item.name == current_version.name %}selected{% endif %}
17-
>
18-
{{ item.name }}{% if item.name == "main" %} (dev){% endif %}
19-
</option>
20-
{%- endfor %}
3+
<select id="version-select" onchange="if (this.value) window.location.href = this.value;">
4+
<option selected>Loading...</option>
215
</select>
226
</div>
23-
{% endif %}
7+
<script>
8+
(function() {
9+
// Walk up from current page to find versions.json at the site root
10+
var depth = window.location.pathname.replace(/\/+$/, '').split('/').length - 1;
11+
// Try relative paths — handles both GitHub Pages subpath and root deployments
12+
var candidates = [];
13+
for (var i = 0; i <= depth; i++) {
14+
candidates.push('../'.repeat(i) + 'versions.json');
15+
}
16+
17+
function tryFetch(urls) {
18+
if (urls.length === 0) return;
19+
var url = urls.shift();
20+
fetch(url).then(function(r) {
21+
if (!r.ok) throw new Error(r.status);
22+
return r.json();
23+
}).then(populate).catch(function() { tryFetch(urls); });
24+
}
25+
26+
function populate(data) {
27+
var sel = document.getElementById('version-select');
28+
sel.innerHTML = '';
29+
var current = data.current || '';
30+
data.versions.forEach(function(v) {
31+
var opt = document.createElement('option');
32+
var label = v.name;
33+
if (v.tag) label += v.name === data.latest ? ' (latest)' : '';
34+
else label += ' (dev)';
35+
opt.textContent = label;
36+
// Build path relative to site root
37+
var base = url.replace('versions.json', '');
38+
opt.value = base + v.name + '/';
39+
if (v.name === current) opt.selected = true;
40+
sel.appendChild(opt);
41+
});
42+
}
43+
44+
tryFetch(candidates);
45+
})();
46+
</script>

docs/conf.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"_extension.gallery_directive",
3232
"myst_nb",
3333
"sphinx_favicon",
34-
"sphinx_multiversion",
3534
]
3635

3736
templates_path = ["_templates"]
@@ -100,14 +99,6 @@
10099
"Redis_Favicon_144x144_Red.png",
101100
]
102101

103-
# -- sphinx-multiversion options ---------------------------------------------
104-
# Tag whitelist: match v0.4.1+ and v1.0.0+ (v0.4.0 and earlier lack docs/)
105-
smv_tag_whitelist = r"^v0\.4\.([1-9]\d*)$|^v0\.([5-9]|\d{2,})\.\d+$|^v([1-9]\d*)\.\d+\.\d+$"
106-
smv_branch_whitelist = r"^main$"
107-
smv_remote_whitelist = r"^origin$"
108-
smv_released_pattern = r"^refs/tags/.*$"
109-
smv_outputdir_format = "{ref.name}"
110-
111102
# -- Sidebar with version switcher ------------------------------------------
112103
html_sidebars = {
113104
"**": [

docs/requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ myst-nb>=1.0
44
sphinx-design>=0.5
55
sphinx-copybutton>=0.5
66
sphinx-favicon>=1.0
7-
sphinx-multiversion-contrib>=0.2.13
87
pyyaml>=6.0

0 commit comments

Comments
 (0)