Skip to content

Commit ddf3dea

Browse files
committed
Add pipeline to quickly validate Jupyter notebooks
1 parent 9e10fdd commit ddf3dea

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: Check Jupyter Notebooks
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
# Run when notebooks, Python dependencies, or this workflow change
8+
paths:
9+
- 'domains/**/explore/*.ipynb'
10+
- 'pyproject.toml'
11+
- 'uv.lock'
12+
- 'scripts/activateUvEnvironment.sh'
13+
- '.github/workflows/internal-check-notebooks.yml'
14+
15+
jobs:
16+
smoke-test:
17+
runs-on: ubuntu-22.04
18+
19+
steps:
20+
- name: Checkout GIT Repository
21+
uses: actions/checkout@v6
22+
23+
- name: (uv Setup) Install uv
24+
uses: astral-sh/setup-uv@v6
25+
with:
26+
python-version: '3.12'
27+
28+
- name: (uv Setup) Sync dependencies from lockfile
29+
run: uv sync --frozen
30+
31+
- name: Execute and check all notebooks
32+
# NEO4J_INITIAL_PASSWORD is required by notebooks but Neo4j is not available in CI.
33+
# --allow-errors lets all cells run even if Neo4j connection or query cells fail.
34+
# Only ModuleNotFoundError, ImportError, and SyntaxError are treated as fatal —
35+
# all Neo4j connection errors and cascading errors from missing data are ignored.
36+
env:
37+
NEO4J_INITIAL_PASSWORD: smoke-test-no-neo4j
38+
run: |
39+
uv run --with nbconvert python3 - <<'PYEOF'
40+
import json, subprocess, sys
41+
from pathlib import Path
42+
43+
fatal_error_types = {"ModuleNotFoundError", "ImportError", "SyntaxError"}
44+
notebooks = sorted(Path("domains").glob("**/explore/*.ipynb"))
45+
failures = []
46+
47+
for notebook in notebooks:
48+
print(f"\n--- {notebook} ---", flush=True)
49+
executed = Path("executed.ipynb")
50+
51+
result = subprocess.run(
52+
[
53+
sys.executable, "-m", "nbconvert",
54+
"--to", "notebook",
55+
"--execute",
56+
"--allow-errors",
57+
"--ExecutePreprocessor.timeout=300",
58+
"--output", "executed",
59+
"--output-dir", ".",
60+
str(notebook),
61+
],
62+
capture_output=True,
63+
text=True,
64+
)
65+
print(result.stdout, end="")
66+
print(result.stderr, end="", file=sys.stderr)
67+
68+
if result.returncode != 0:
69+
failures.append(f"{notebook}: nbconvert exited with code {result.returncode}")
70+
continue
71+
72+
nb = json.loads(executed.read_text())
73+
for cell_idx, cell in enumerate(nb["cells"], 1):
74+
for output in cell.get("outputs", []):
75+
if output.get("output_type") != "error":
76+
continue
77+
ename = output.get("ename", "")
78+
evalue = output.get("evalue", "")
79+
if ename in fatal_error_types:
80+
failures.append(f"{notebook} cell {cell_idx}: {ename}: {evalue}")
81+
82+
cells_total = len(nb["cells"])
83+
print(f"OK: {cells_total} cells executed.")
84+
85+
print(f"\n{'='*60}")
86+
if failures:
87+
print(f"FAILED ({len(failures)} error(s)):", file=sys.stderr)
88+
for f in failures:
89+
print(f" {f}", file=sys.stderr)
90+
sys.exit(1)
91+
92+
print(f"All {len(notebooks)} notebooks passed.")
93+
PYEOF

0 commit comments

Comments
 (0)