Skip to content

ci: let workflow_dispatch actually deploy docs, rename to "Docs Publish" #236

ci: let workflow_dispatch actually deploy docs, rename to "Docs Publish"

ci: let workflow_dispatch actually deploy docs, rename to "Docs Publish" #236

Workflow file for this run

name: Docs Publish
on:
push:
branches: [main]
paths:
- docs/**
- src/**
- packages/mvtb-data/**
- pyproject.toml
- .github/workflows/docs.yml
pull_request:
branches: [main]
paths:
- docs/**
- src/**
- packages/mvtb-data/**
- pyproject.toml
- .github/workflows/docs.yml
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
# Only one deployment at a time; don't cancel an in-progress deploy
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Check notebook bootstrap cells and outputs are in sync
run: |
python docs/notebooks/sync_bootstrap.py --check
python docs/notebooks/clear_outputs.py --check
- name: Install system dependencies
run: sudo apt-get install -y graphviz
- name: Install Python dependencies
run: |
pip install --upgrade pip
pip install -r docs/requirements.txt
cd packages/mvtb-data && pip install .
- name: Build docs
run: sphinx-build -b html docs/source docs/build/html
- name: Build wheel for JupyterLite
run: |
pip install build
python -m build --wheel --outdir docs/lite/pypi
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/lite/wasm_check/package-lock.json
- name: Check the JupyterLite install path actually works (real Pyodide, real WASM)
run: |
npm --prefix docs/lite/wasm_check ci
node docs/lite/wasm_check/check_pyodide_install.mjs docs/lite/pypi/*.whl
- name: Sync notebooks into JupyterLite content
run: |
mkdir -p docs/lite/files
find docs/notebooks \
-type f \
-name '*.ipynb' \
! -path '*/.ipynb_checkpoints/*' \
! -path '*/old/*' \
-print0 | while IFS= read -r -d '' nb; do
rel="${nb#docs/notebooks/}"
mkdir -p "docs/lite/files/$(dirname "$rel")"
cp -p "$nb" "docs/lite/files/$rel"
done
- name: Adapt staged notebooks for JupyterLite
run: |
python - <<'PY'
import json
from pathlib import Path
root = Path("docs/lite/files")
for nb_path in root.rglob("*.ipynb"):
with nb_path.open("r", encoding="utf-8") as f:
nb = json.load(f)
meta = nb.setdefault("metadata", {})
meta["kernelspec"] = {
"name": "python",
"display_name": "Python (Pyodide)",
"language": "python",
}
meta.setdefault("language_info", {})
meta["language_info"]["name"] = "python"
for cell in nb.get("cells", []):
if cell.get("cell_type") != "code":
continue
source = cell.get("source", [])
if isinstance(source, str):
lines = source.splitlines(keepends=True)
else:
lines = source
new_lines = []
changed = False
for line in lines:
if line.strip() in ("%matplotlib ipympl", "%matplotlib widget"):
newline = "\n" if line.endswith("\n") else ""
new_lines.append(f"%matplotlib inline{newline}")
changed = True
else:
new_lines.append(line)
if changed:
cell["source"] = (
new_lines
if not isinstance(source, str)
else "".join(new_lines)
)
with nb_path.open("w", encoding="utf-8") as f:
json.dump(nb, f, indent=1)
f.write("\n")
PY
- name: Build JupyterLite site
# jupyterlite-pyodide-kernel is pinned deliberately -- NOT for wasm-ABI
# matching (MVTB is pure Python, unlike RTB's compiled-extension pin of
# the same version, for a different reason). Newer kernel releases bundle
# Pyodide >=0.27.7, which turned WebAssembly JSPI ("stack switching") on
# by default for run_until_complete(). Browsers without JSPI support
# (Safari has none as of 2026-08; Firefox only behind a flag) either
# crash the kernel outright or, with the enableRunUntilComplete: false
# workaround, turn run_until_complete into a fire-and-forget no-op --
# letting a cell's package/import machinery run before an install has
# actually finished landing (confirmed live on bdsim: intermittent
# ModuleNotFoundError despite a successful-looking install cell).
# jupyterlite-pyodide-kernel==0.6.1 bundles Pyodide 0.27.6, genuinely
# predating this whole mechanism -- matches RTB's and bdsim's existing
# pins. Installing jupyterlite-core directly rather than the jupyterlite
# metapackage: the metapackage requires jupyterlite-core>=0.8.1, which
# conflicts with this pin's jupyterlite-core<0.7.0 requirement.
# Once Safari (and Firefox out-of-flag) ship JSPI, revisit this pin --
# and docs/lite/wasm_check/package.json's matching pyodide npm pin.
run: |
pip install jupyterlite-core "jupyterlite-pyodide-kernel==0.6.1" jupyter-server
cd docs/lite && jupyter lite build --output-dir ../build/html/lite
- name: Package notebooks zip
run: |
rm -f docs/build/html/mvtb_notebooks.zip
zip -r docs/build/html/mvtb_notebooks.zip docs/notebooks
- name: Upload notebooks zip artifact
uses: actions/upload-artifact@v7
with:
name: mvtb_notebooks
path: docs/build/html/mvtb_notebooks.zip
if-no-files-found: error
# push and workflow_dispatch both publish; pull_request only builds
# (to catch breakage in review), it never gets an artifact to deploy.
- uses: actions/upload-pages-artifact@v5
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
with:
path: docs/build/html
deploy:
needs: build
runs-on: ubuntu-latest
# ref check (not just event_name) so a manual dispatch from a
# non-main branch builds but doesn't publish -- push is already
# restricted to main via the `branches: [main]` filter above, but
# workflow_dispatch can be run against any branch.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5