diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 8216c23f..27056f74 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -2,11 +2,18 @@ name: CI
on:
push:
+ tags:
+ - 'v[0-9]*.[0-9]*.[0-9]*'
+ - 'v[0-9]*.[0-9]*.[0-9]*a[0-9]*'
+ - 'v[0-9]*.[0-9]*.[0-9]*b[0-9]*'
+ - 'v[0-9]*.[0-9]*.[0-9]*rc[0-9]*'
branches:
- master
+ - next2026
pull_request:
branches:
- master
+ - next2026
schedule:
- cron: "9 16 * * 1"
@@ -280,9 +287,82 @@ jobs:
cd docs
make html
+ - name: Compute docs deploy target
+ id: target
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ echo GITHUB_REF_TYPE ${GITHUB_REF_TYPE}
+ echo GITHUB_REF_NAME ${GITHUB_REF_NAME}
+ echo GITHUB_REF ${GITHUB_REF}
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
+ echo "target_folder=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
+ echo "version_label=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
+
+ elif [[ "${GITHUB_REF_NAME}" == "master" ]]; then
+ echo "target_folder=dev" >> "$GITHUB_OUTPUT"
+ echo "version_label=dev" >> "$GITHUB_OUTPUT"
+
+ elif [[ "${GITHUB_REF_NAME}" == "next2026" ]]; then
+ echo "target_folder=next" >> "$GITHUB_OUTPUT"
+ echo "version_label=next" >> "$GITHUB_OUTPUT"
+
+ else
+ echo "target_folder=" >> "$GITHUB_OUTPUT"
+ echo "version_label=" >> "$GITHUB_OUTPUT"
+ fi
+ echo GITHUB_OUTPUT ${GITHUB_OUTPUT}
+
- name: GitHub Pages Deploy
- uses: JamesIves/github-pages-deploy-action@4.1.1
+ uses: JamesIves/github-pages-deploy-action@v4
if: github.event_name == 'push' && github.repository == 'MolSSI/QCEngine' && ( startsWith( github.ref, 'refs/tags/' ) || github.ref == 'refs/heads/master' )
with:
branch: gh-pages
folder: docs/build/html
+ target-folder: ${{ steps.target.outputs.target_folder }}
+ clean: false
+
+ - name: Update versions.json
+ if: github.event_name == 'push' && github.repository == 'MolSSI/QCEngine' && ( startsWith( github.ref, 'refs/tags/' ) || github.ref == 'refs/heads/master' )
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION_LABEL: ${{ steps.target.outputs.version_label }}
+ TARGET_FOLDER: ${{ steps.target.outputs.target_folder }}
+ run: |
+ set -euo pipefail
+ rm -rf _ghp
+ git clone --depth 1 --branch gh-pages "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" _ghp
+ cd _ghp
+ # Create versions.json if it doesn't exist yet
+ if [[ ! -f versions.json ]]; then
+ cat > versions.json <<'JSON'
+ [
+ {"label": "dev", "path": "dev/"},
+ {"label": "next", "path": "next/"}
+ ]
+ JSON
+ fi
+ python - <<'PY'
+ import json, pathlib, os
+ p = pathlib.Path("versions.json")
+ data = json.loads(p.read_text())
+ pinned = [
+ {"label": "dev", "path": "dev/"},
+ {"label": "next", "path": "next/"},
+ ]
+ lbl = os.environ.get("VERSION_LABEL", "")
+ tgt = os.environ.get("TARGET_FOLDER", "")
+ if lbl and lbl not in ("dev", "next") and tgt:
+ pinned.insert(1, {"label": lbl, "path": tgt.rstrip("/") + "/"})
+ p.write_text(json.dumps(pinned, indent=2) + "\n")
+ PY
+ if ! git diff --quiet; then
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add versions.json
+ git commit -m "docs: update versions.json (${GITHUB_REF})"
+ git push origin gh-pages
+ fi
+
diff --git a/docs/source/_extra/versions.json b/docs/source/_extra/versions.json
new file mode 100644
index 00000000..8f6acceb
--- /dev/null
+++ b/docs/source/_extra/versions.json
@@ -0,0 +1,5 @@
+[
+ {"label":"dev","path":"dev/"},
+ {"label":"next","path":"next/"}
+]
+
diff --git a/docs/source/_static/version-switcher.js b/docs/source/_static/version-switcher.js
new file mode 100644
index 00000000..b694f405
--- /dev/null
+++ b/docs/source/_static/version-switcher.js
@@ -0,0 +1,121 @@
+(async function () {
+ function sleep(ms) {
+ return new Promise((r) => setTimeout(r, ms));
+ }
+
+ // Wait for RTD sidebar to exist (theme may build it after our script loads)
+ async function waitForSidebar(maxTries = 50, delayMs = 100) {
+ for (let i = 0; i < maxTries; i++) {
+ const search = document.querySelector(".wy-side-nav-search");
+ const scroll = document.querySelector(".wy-side-scroll");
+ if (search || scroll) return { search, scroll };
+ await sleep(delayMs);
+ }
+ return { search: null, scroll: null };
+ }
+
+ const { search, scroll } = await waitForSidebar();
+ if (!search && !scroll) {
+ console.warn("[version-switcher] RTD sidebar not found.");
+ return;
+ }
+
+ // Avoid duplicates (important if theme re-renders and script runs again)
+ if (document.querySelector(".version-switcher")) return;
+
+ // --- UI ---
+ const container = document.createElement("div");
+ container.className = "version-switcher";
+ container.style.padding = "0.5rem 1rem";
+ container.style.display = "flex";
+ container.style.gap = "0.5rem";
+ container.style.alignItems = "center";
+ container.style.justifyContent = "center";
+
+ const label = document.createElement("span");
+ label.textContent = "Version:";
+ label.style.fontSize = "0.9em";
+ label.className = "vs-label";
+
+ const select = document.createElement("select");
+ select.setAttribute("aria-label", "Select documentation version");
+ select.style.width = "50%";
+ select.style.borderRadius = "20px";
+
+ container.appendChild(label);
+ container.appendChild(select);
+
+ // Insert under the search box if possible
+ if (search && search.parentNode) {
+ search.insertAdjacentElement("afterend", container);
+ } else {
+ scroll.prepend(container);
+ }
+
+ // --- Load versions.json ---
+ select.disabled = true;
+ select.innerHTML = ``;
+
+ async function fetchJson(url) {
+ const resp = await fetch(url, { cache: "no-store" });
+ if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
+ return resp.json();
+ }
+
+ // For local http://localhost:8000/ this will work.
+ // For GitHub project pages it will also work if versions.json is at the root of the published site.
+ let versions;
+ try {
+ versions = await fetchJson(`${window.location.origin}/versions.json`);
+ } catch (e) {
+ // GitHub project page fallback: /QCElemental/versions.json
+ const parts = window.location.pathname.split("/").filter(Boolean);
+ if (parts.length) {
+ try {
+ versions = await fetchJson(`${window.location.origin}/${parts[0]}/versions.json`);
+ } catch (e2) {
+ console.warn("[version-switcher] Could not load versions.json", e, e2);
+ select.innerHTML = ``;
+ return;
+ }
+ } else {
+ console.warn("[version-switcher] Could not load versions.json", e);
+ select.innerHTML = ``;
+ return;
+ }
+ }
+
+ // Populate
+ select.innerHTML = "";
+ for (const v of versions) {
+ const opt = document.createElement("option");
+ opt.value = v.path; // "dev/" etc.
+ opt.textContent = v.label; // "dev"
+ select.appendChild(opt);
+ }
+
+ // Determine current "version folder" (dev / v0.30.1 / etc.)
+ // If you're at /dev/index.html => currentVersion = "dev"
+ // If you're at /index.html => currentVersion = ""
+ const pathParts = window.location.pathname.split("/").filter(Boolean);
+ const currentVersion = pathParts.length >= 2 ? pathParts[1] : pathParts[0] || "";
+
+ for (const opt of select.options) {
+ if (opt.value.replace(/\/+$/, "") === currentVersion) {
+ opt.selected = true;
+ break;
+ }
+ }
+
+ select.disabled = false;
+
+ // Navigate on change; for local dev it’s simplest to go to version root
+ select.addEventListener("change", () => {
+ const target = select.value; // e.g. "dev/"
+ // If on GitHub project site, keep the project prefix automatically.
+ const parts = window.location.pathname.split("/").filter(Boolean);
+ const prefix = parts.length ? `/${parts[0]}/` : "/";
+ window.location.href = `${window.location.origin}${prefix}${target}`;
+ });
+})();
+
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 738593fc..8204ccca 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -28,8 +28,23 @@ Changelog
.. - UNSOLVED (:issue:`397`) extras failed
-v0.35.0 / 2026-MM-DD (Unreleased)
---------------------
+.. _`sec:cl0341`:
+
+0.34.1 / 2026-02-15
+-------------------
+
+`Docs `_
+
+Enhancements
+++++++++++++
+- (:pr:`492`) Docs - Set up documentation build to store versions of documentation for each tag plus dev.
+ Note that this changes intersphinx links. Below is current.
+
+.. code:: python
+
+ "qcelemental": ("https://molssi.github.io/QCElemental/dev/", None),
+ "qcengine": ("https://molssi.github.io/QCEngine/dev/", None),
+ "qcfractal": ("https://molssi.github.io/QCFractal/", None),
Misc.
+++++
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 1d210f1f..c395219c 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -112,12 +112,22 @@
# further. For a list of options available for each theme, see the
# documentation.
#
-# html_theme_options = {}
+html_theme_options = {
+ # keep these off unless you're actually on RTD
+ "version_selector": False,
+ "language_selector": False,
+}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
+html_extra_path = ["_extra"]
+
+
+def setup(app):
+ app.add_js_file("version-switcher.js")
+
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
@@ -201,9 +211,8 @@
"numpy": ("https://numpy.org/doc/stable/", None),
'scipy': ('https://docs.scipy.org/doc/scipy/', None),
'matplotlib': ('https://matplotlib.org/stable/', None),
- "qcelemental": ("http://docs.qcarchive.molssi.org/projects/QCElemental/en/latest/", None),
- "qcportal": ("http://docs.qcarchive.molssi.org/projects/QCPortal/en/latest/", None),
- "qcfractal": ("http://docs.qcarchive.molssi.org/projects/QCFractal/en/latest/", None),
+ "qcelemental": ("https://molssi.github.io/QCElemental/dev/", None),
+ "qcfractal": ("https://molssi.github.io/QCFractal/", None),
}
# -- Options for todo extension ----------------------------------------------