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 ----------------------------------------------