Skip to content

Commit 8192187

Browse files
Alexandreclaude
andcommitted
Deploy one docs version per branch to Pages
GitHub Pages allows a single deployment per repository, so the workflow now rebuilds the whole site from every branch on each run instead of publishing only the branch that was pushed. main -> / <other branch> -> /<other branch>/ Adds a sidebar version switcher, injected into every page at deploy time. It keeps the reader on the same page across versions, falling back to that version's index when the page does not exist there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 36feaa2 commit 8192187

2 files changed

Lines changed: 181 additions & 5 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
"""Inject a version switcher into the sidebar of an assembled multi-version site.
3+
4+
Usage: inject_version_switcher.py <site_dir> <branches_file>
5+
6+
<branches_file> is a newline-delimited list of branch names. "main" is served
7+
from the site root, every other branch from a directory of the same name. The
8+
switcher is inserted into the sphinx_rtd_theme sidebar, just above the search
9+
box, and is skipped entirely when there is only one version to choose from.
10+
"""
11+
12+
import html
13+
import os
14+
import sys
15+
16+
ANCHOR = '<div role="search">'
17+
MARKER = "<!-- version-switcher -->"
18+
19+
STYLE = """
20+
<style>
21+
.version-switcher{margin:.5em 0 .809em;text-align:left}
22+
.version-switcher label{display:block;margin-bottom:.3em;font-size:80%;
23+
text-transform:uppercase;letter-spacing:.08em;opacity:.75;color:inherit}
24+
.version-switcher select{width:100%;padding:.35em .5em;font-size:90%;
25+
border:1px solid rgba(255,255,255,.25);border-radius:2px;
26+
background-color:rgba(255,255,255,.12);color:#fcfcfc;cursor:pointer}
27+
.version-switcher select option{color:#404040;background-color:#fcfcfc}
28+
html[data-theme="dark"] .version-switcher select{border-color:#111;
29+
background-color:#141414;color:#ddd}
30+
html[data-theme="dark"] .version-switcher select option{color:#ddd;
31+
background-color:#141414}
32+
</style>
33+
"""
34+
35+
SCRIPT = """
36+
<script>
37+
(function(){
38+
var sel=document.getElementById("version-switcher-select");
39+
if(!sel){return;}
40+
sel.addEventListener("change",function(){
41+
var base=sel.getAttribute("data-site-root")+sel.value;
42+
var>
43+
var fallback=base+"index.html";
44+
// Keep the reader on the same page when that page exists in the chosen
45+
// version; otherwise land on that version's index.
46+
fetch(target,{method:"HEAD"}).then(function(r){
47+
window.location.href=r.ok?target:fallback;
48+
}).catch(function(){window.location.href=fallback;});
49+
});
50+
})();
51+
</script>
52+
"""
53+
54+
55+
def order(branches):
56+
"""main first, then the rest newest-looking first (reverse alphabetical)."""
57+
rest = sorted((b for b in branches if b != "main"), reverse=True)
58+
return (["main"] if "main" in branches else []) + rest
59+
60+
61+
def widget(versions, current, site_root, page_path):
62+
options = []
63+
for name, directory in versions:
64+
selected = " selected" if name == current else ""
65+
options.append(
66+
'<option value="%s"%s>%s</option>'
67+
% (html.escape(directory), selected, html.escape(name))
68+
)
69+
return (
70+
'%s<div class="version-switcher">'
71+
'<label for="version-switcher-select">Version</label>'
72+
'<select id="version-switcher-select" data-site-root="%s" data-page-path="%s">'
73+
"%s</select></div>%s%s"
74+
% (
75+
MARKER,
76+
html.escape(site_root),
77+
html.escape(page_path),
78+
"".join(options),
79+
STYLE,
80+
SCRIPT,
81+
)
82+
)
83+
84+
85+
def main():
86+
if len(sys.argv) != 3:
87+
sys.exit("usage: inject_version_switcher.py <site_dir> <branches_file>")
88+
site_dir, branches_file = sys.argv[1], sys.argv[2]
89+
90+
with open(branches_file) as fh:
91+
branches = [line.strip() for line in fh if line.strip()]
92+
93+
if len(branches) < 2:
94+
print("Only %d version(s); skipping version switcher." % len(branches))
95+
return
96+
97+
versions = [(b, "" if b == "main" else b + "/") for b in order(branches)]
98+
# Longest directory first so "Autograph-2027/" wins over main's "".
99+
lookup = sorted(versions, key=lambda v: len(v[1]), reverse=True)
100+
101+
injected = skipped = 0
102+
for dirpath, _, filenames in os.walk(site_dir):
103+
for filename in filenames:
104+
if not filename.endswith(".html"):
105+
continue
106+
path = os.path.join(dirpath, filename)
107+
rel = os.path.relpath(path, site_dir).replace(os.sep, "/")
108+
109+
current, directory = next(
110+
(v for v in lookup if rel.startswith(v[1])), (None, None)
111+
)
112+
if current is None:
113+
continue
114+
115+
page_path = rel[len(directory):]
116+
site_root = "../" * rel.count("/") or "./"
117+
118+
with open(path, encoding="utf-8") as fh:
119+
doc = fh.read()
120+
if MARKER in doc or ANCHOR not in doc:
121+
skipped += 1
122+
continue
123+
124+
doc = doc.replace(
125+
ANCHOR, widget(versions, current, site_root, page_path) + ANCHOR, 1
126+
)
127+
with open(path, "w", encoding="utf-8") as fh:
128+
fh.write(doc)
129+
injected += 1
130+
131+
print(
132+
"Version switcher: %d page(s) updated, %d skipped, %d version(s): %s"
133+
% (injected, skipped, len(versions), ", ".join(n for n, _ in versions))
134+
)
135+
136+
137+
if __name__ == "__main__":
138+
main()

‎.github/workflows/static.yml‎

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
# Simple workflow for deploying static content to GitHub Pages
1+
# Deploy every documentation version (one per branch) to GitHub Pages.
2+
# main -> https://maxon-computer.github.io/AutographPythonDoc/
3+
# <other branch> -> https://maxon-computer.github.io/AutographPythonDoc/<other branch>/
4+
#
5+
# GitHub Pages allows a single deployment per repository, so every run rebuilds
6+
# the whole site from all branches rather than updating one version in place.
27
name: Deploy static content to Pages
38

49
on:
5-
# Runs on pushes targeting the default branch
10+
# A push to any branch rebuilds the site, so every version stays in sync
611
push:
7-
branches: ["main"]
12+
branches: ["**"]
13+
14+
# Rebuild after a branch is deleted so its version drops off the site
15+
delete:
816

917
# Allows you to run this workflow manually from the Actions tab
1018
workflow_dispatch:
@@ -31,13 +39,43 @@ jobs:
3139
steps:
3240
- name: Checkout
3341
uses: actions/checkout@v4
42+
with:
43+
# Every branch is needed, not just the one that triggered the run
44+
fetch-depth: 0
45+
46+
- name: Assemble one version per branch
47+
run: |
48+
set -euo pipefail
49+
rm -rf _site && mkdir -p _site
50+
51+
git for-each-ref --format='%(refname:short)' refs/remotes/origin \
52+
| grep -v '^origin/HEAD$' \
53+
| sed 's|^origin/||' > branches.txt
54+
55+
while read -r branch; do
56+
if [ "$branch" = "main" ]; then dest="_site"; else dest="_site/$branch"; fi
57+
echo "::group::$branch -> ${dest#_site}/"
58+
mkdir -p "$dest"
59+
# Export the committed tree straight from the object database
60+
git archive "origin/$branch" | tar -x -C "$dest"
61+
rm -rf "$dest/.github"
62+
echo "::endgroup::"
63+
done < branches.txt
64+
65+
touch _site/.nojekyll
66+
67+
- name: Add sidebar version switcher
68+
run: python3 .github/scripts/inject_version_switcher.py _site branches.txt
69+
3470
- name: Setup Pages
3571
uses: actions/configure-pages@v5
72+
3673
- name: Upload artifact
3774
uses: actions/upload-pages-artifact@v3
3875
with:
39-
# Upload entire repository
40-
path: '.'
76+
# Upload the assembled multi-version site
77+
path: '_site'
78+
4179
- name: Deploy to GitHub Pages
4280
id: deployment
4381
uses: actions/deploy-pages@v5

0 commit comments

Comments
 (0)