Skip to content

Commit 8186b65

Browse files
authored
set up docs build to serve multiple versions (#492)
* store multiversion * fix address * fix address * restore
1 parent 8d1ca56 commit 8186b65

File tree

5 files changed

+237
-7
lines changed

5 files changed

+237
-7
lines changed

.github/workflows/CI.yml

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ name: CI
22

33
on:
44
push:
5+
tags:
6+
- 'v[0-9]*.[0-9]*.[0-9]*'
7+
- 'v[0-9]*.[0-9]*.[0-9]*a[0-9]*'
8+
- 'v[0-9]*.[0-9]*.[0-9]*b[0-9]*'
9+
- 'v[0-9]*.[0-9]*.[0-9]*rc[0-9]*'
510
branches:
611
- master
12+
- next2026
713
pull_request:
814
branches:
915
- master
16+
- next2026
1017
schedule:
1118
- cron: "9 16 * * 1"
1219

@@ -280,9 +287,82 @@ jobs:
280287
cd docs
281288
make html
282289
290+
- name: Compute docs deploy target
291+
id: target
292+
shell: bash
293+
run: |
294+
set -euo pipefail
295+
296+
echo GITHUB_REF_TYPE ${GITHUB_REF_TYPE}
297+
echo GITHUB_REF_NAME ${GITHUB_REF_NAME}
298+
echo GITHUB_REF ${GITHUB_REF}
299+
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
300+
echo "target_folder=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
301+
echo "version_label=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
302+
303+
elif [[ "${GITHUB_REF_NAME}" == "master" ]]; then
304+
echo "target_folder=dev" >> "$GITHUB_OUTPUT"
305+
echo "version_label=dev" >> "$GITHUB_OUTPUT"
306+
307+
elif [[ "${GITHUB_REF_NAME}" == "next2026" ]]; then
308+
echo "target_folder=next" >> "$GITHUB_OUTPUT"
309+
echo "version_label=next" >> "$GITHUB_OUTPUT"
310+
311+
else
312+
echo "target_folder=" >> "$GITHUB_OUTPUT"
313+
echo "version_label=" >> "$GITHUB_OUTPUT"
314+
fi
315+
echo GITHUB_OUTPUT ${GITHUB_OUTPUT}
316+
283317
- name: GitHub Pages Deploy
284-
uses: JamesIves/github-pages-deploy-action@4.1.1
318+
uses: JamesIves/github-pages-deploy-action@v4
285319
if: github.event_name == 'push' && github.repository == 'MolSSI/QCEngine' && ( startsWith( github.ref, 'refs/tags/' ) || github.ref == 'refs/heads/master' )
286320
with:
287321
branch: gh-pages
288322
folder: docs/build/html
323+
target-folder: ${{ steps.target.outputs.target_folder }}
324+
clean: false
325+
326+
- name: Update versions.json
327+
if: github.event_name == 'push' && github.repository == 'MolSSI/QCEngine' && ( startsWith( github.ref, 'refs/tags/' ) || github.ref == 'refs/heads/master' )
328+
shell: bash
329+
env:
330+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
331+
VERSION_LABEL: ${{ steps.target.outputs.version_label }}
332+
TARGET_FOLDER: ${{ steps.target.outputs.target_folder }}
333+
run: |
334+
set -euo pipefail
335+
rm -rf _ghp
336+
git clone --depth 1 --branch gh-pages "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" _ghp
337+
cd _ghp
338+
# Create versions.json if it doesn't exist yet
339+
if [[ ! -f versions.json ]]; then
340+
cat > versions.json <<'JSON'
341+
[
342+
{"label": "dev", "path": "dev/"},
343+
{"label": "next", "path": "next/"}
344+
]
345+
JSON
346+
fi
347+
python - <<'PY'
348+
import json, pathlib, os
349+
p = pathlib.Path("versions.json")
350+
data = json.loads(p.read_text())
351+
pinned = [
352+
{"label": "dev", "path": "dev/"},
353+
{"label": "next", "path": "next/"},
354+
]
355+
lbl = os.environ.get("VERSION_LABEL", "")
356+
tgt = os.environ.get("TARGET_FOLDER", "")
357+
if lbl and lbl not in ("dev", "next") and tgt:
358+
pinned.insert(1, {"label": lbl, "path": tgt.rstrip("/") + "/"})
359+
p.write_text(json.dumps(pinned, indent=2) + "\n")
360+
PY
361+
if ! git diff --quiet; then
362+
git config user.name "github-actions[bot]"
363+
git config user.email "github-actions[bot]@users.noreply.github.com"
364+
git add versions.json
365+
git commit -m "docs: update versions.json (${GITHUB_REF})"
366+
git push origin gh-pages
367+
fi
368+

docs/source/_extra/versions.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
{"label":"dev","path":"dev/"},
3+
{"label":"next","path":"next/"}
4+
]
5+
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
(async function () {
2+
function sleep(ms) {
3+
return new Promise((r) => setTimeout(r, ms));
4+
}
5+
6+
// Wait for RTD sidebar to exist (theme may build it after our script loads)
7+
async function waitForSidebar(maxTries = 50, delayMs = 100) {
8+
for (let i = 0; i < maxTries; i++) {
9+
const search = document.querySelector(".wy-side-nav-search");
10+
const scroll = document.querySelector(".wy-side-scroll");
11+
if (search || scroll) return { search, scroll };
12+
await sleep(delayMs);
13+
}
14+
return { search: null, scroll: null };
15+
}
16+
17+
const { search, scroll } = await waitForSidebar();
18+
if (!search && !scroll) {
19+
console.warn("[version-switcher] RTD sidebar not found.");
20+
return;
21+
}
22+
23+
// Avoid duplicates (important if theme re-renders and script runs again)
24+
if (document.querySelector(".version-switcher")) return;
25+
26+
// --- UI ---
27+
const container = document.createElement("div");
28+
container.className = "version-switcher";
29+
container.style.padding = "0.5rem 1rem";
30+
container.style.display = "flex";
31+
container.style.gap = "0.5rem";
32+
container.style.alignItems = "center";
33+
container.style.justifyContent = "center";
34+
35+
const label = document.createElement("span");
36+
label.textContent = "Version:";
37+
label.style.fontSize = "0.9em";
38+
label.className = "vs-label";
39+
40+
const select = document.createElement("select");
41+
select.setAttribute("aria-label", "Select documentation version");
42+
select.style.width = "50%";
43+
select.style.borderRadius = "20px";
44+
45+
container.appendChild(label);
46+
container.appendChild(select);
47+
48+
// Insert under the search box if possible
49+
if (search && search.parentNode) {
50+
search.insertAdjacentElement("afterend", container);
51+
} else {
52+
scroll.prepend(container);
53+
}
54+
55+
// --- Load versions.json ---
56+
select.disabled = true;
57+
select.innerHTML = `<option>Loading…</option>`;
58+
59+
async function fetchJson(url) {
60+
const resp = await fetch(url, { cache: "no-store" });
61+
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
62+
return resp.json();
63+
}
64+
65+
// For local http://localhost:8000/ this will work.
66+
// For GitHub project pages it will also work if versions.json is at the root of the published site.
67+
let versions;
68+
try {
69+
versions = await fetchJson(`${window.location.origin}/versions.json`);
70+
} catch (e) {
71+
// GitHub project page fallback: /QCElemental/versions.json
72+
const parts = window.location.pathname.split("/").filter(Boolean);
73+
if (parts.length) {
74+
try {
75+
versions = await fetchJson(`${window.location.origin}/${parts[0]}/versions.json`);
76+
} catch (e2) {
77+
console.warn("[version-switcher] Could not load versions.json", e, e2);
78+
select.innerHTML = `<option>No versions.json</option>`;
79+
return;
80+
}
81+
} else {
82+
console.warn("[version-switcher] Could not load versions.json", e);
83+
select.innerHTML = `<option>No versions.json</option>`;
84+
return;
85+
}
86+
}
87+
88+
// Populate
89+
select.innerHTML = "";
90+
for (const v of versions) {
91+
const opt = document.createElement("option");
92+
opt.value = v.path; // "dev/" etc.
93+
opt.textContent = v.label; // "dev"
94+
select.appendChild(opt);
95+
}
96+
97+
// Determine current "version folder" (dev / v0.30.1 / etc.)
98+
// If you're at /dev/index.html => currentVersion = "dev"
99+
// If you're at /index.html => currentVersion = ""
100+
const pathParts = window.location.pathname.split("/").filter(Boolean);
101+
const currentVersion = pathParts.length >= 2 ? pathParts[1] : pathParts[0] || "";
102+
103+
for (const opt of select.options) {
104+
if (opt.value.replace(/\/+$/, "") === currentVersion) {
105+
opt.selected = true;
106+
break;
107+
}
108+
}
109+
110+
select.disabled = false;
111+
112+
// Navigate on change; for local dev it’s simplest to go to version root
113+
select.addEventListener("change", () => {
114+
const target = select.value; // e.g. "dev/"
115+
// If on GitHub project site, keep the project prefix automatically.
116+
const parts = window.location.pathname.split("/").filter(Boolean);
117+
const prefix = parts.length ? `/${parts[0]}/` : "/";
118+
window.location.href = `${window.location.origin}${prefix}${target}`;
119+
});
120+
})();
121+

docs/source/changelog.rst

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,23 @@ Changelog
2828
.. - UNSOLVED (:issue:`397`) extras failed
2929
3030
31-
v0.35.0 / 2026-MM-DD (Unreleased)
32-
--------------------
31+
.. _`sec:cl0341`:
32+
33+
0.34.1 / 2026-02-15
34+
-------------------
35+
36+
`Docs <https://MolSSI.github.io/QCEngine/v0.34.1/>`_
37+
38+
Enhancements
39+
++++++++++++
40+
- (:pr:`492`) Docs - Set up documentation build to store versions of documentation for each tag plus dev.
41+
Note that this changes intersphinx links. Below is current.
42+
43+
.. code:: python
44+
45+
"qcelemental": ("https://molssi.github.io/QCElemental/dev/", None),
46+
"qcengine": ("https://molssi.github.io/QCEngine/dev/", None),
47+
"qcfractal": ("https://molssi.github.io/QCFractal/", None),
3348
3449
Misc.
3550
+++++

docs/source/conf.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,22 @@
112112
# further. For a list of options available for each theme, see the
113113
# documentation.
114114
#
115-
# html_theme_options = {}
115+
html_theme_options = {
116+
# keep these off unless you're actually on RTD
117+
"version_selector": False,
118+
"language_selector": False,
119+
}
116120

117121
# Add any paths that contain custom static files (such as style sheets) here,
118122
# relative to this directory. They are copied after the builtin static files,
119123
# so a file named "default.css" will overwrite the builtin "default.css".
120124
html_static_path = ['_static']
125+
html_extra_path = ["_extra"]
126+
127+
128+
def setup(app):
129+
app.add_js_file("version-switcher.js")
130+
121131

122132
# Custom sidebar templates, must be a dictionary that maps document names
123133
# to template names.
@@ -201,9 +211,8 @@
201211
"numpy": ("https://numpy.org/doc/stable/", None),
202212
'scipy': ('https://docs.scipy.org/doc/scipy/', None),
203213
'matplotlib': ('https://matplotlib.org/stable/', None),
204-
"qcelemental": ("http://docs.qcarchive.molssi.org/projects/QCElemental/en/latest/", None),
205-
"qcportal": ("http://docs.qcarchive.molssi.org/projects/QCPortal/en/latest/", None),
206-
"qcfractal": ("http://docs.qcarchive.molssi.org/projects/QCFractal/en/latest/", None),
214+
"qcelemental": ("https://molssi.github.io/QCElemental/dev/", None),
215+
"qcfractal": ("https://molssi.github.io/QCFractal/", None),
207216
}
208217

209218
# -- Options for todo extension ----------------------------------------------

0 commit comments

Comments
 (0)