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