Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Lint (flake8)
run: |
flake8 .

- name: Run unit tests (pytest)
run: |
pytest -q --maxfail=1

- name: Execute all notebooks (nbconvert)
run: |
python - <<'PY'
import glob, subprocess, sys
notebooks = glob.glob('**/*.ipynb', recursive=True)
if not notebooks:
print("No notebooks found.")
sys.exit(0)
for n in notebooks:
print("Executing", n)
subprocess.check_call([
sys.executable, "-m", "jupyter", "nbconvert",
"--to", "notebook", "--execute", "--inplace",
"--ExecutePreprocessor.timeout=600", n
])
PY
90 changes: 90 additions & 0 deletions .github/workflows/deploy-notebooks-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
on:
push:
branches:
- '**'

name: Build and deploy notebooks to GitHub Pages (per-branch)

jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: write # needed to push gh-pages
steps:
- name: Checkout repository (current branch)
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Determine branch name
id: branch
run: |
echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" > $GITHUB_ENV
echo "branch=${GITHUB_REF#refs/heads/}" > $GITHUB_OUTPUT

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; else pip install jupyter nbconvert nbformat; fi

- name: Build notebooks to HTML for this branch
run: |
mkdir -p site/${{ env.BRANCH_NAME }}
python scripts/build_notebooks.py site/${{ env.BRANCH_NAME }}

- name: Prepare gh-pages branch workspace
run: |
# If gh-pages exists, clone it, otherwise init an empty gh-pages branch
REPO_URL="https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
if git ls-remote --exit-code origin gh-pages; then
git clone --depth 1 --branch gh-pages "$REPO_URL" gh-pages || git clone "$REPO_URL" gh-pages
else
# create temporary folder, init gh-pages and push
mkdir gh-pages
cd gh-pages
git init
git remote add origin "$REPO_URL"
git checkout -b gh-pages 2>/dev/null || git checkout gh-pages
touch .nojekyll
git add .nojekyll
git commit -m "Initialize gh-pages"
git push origin gh-pages
cd ..
git clone --depth 1 --branch gh-pages "$REPO_URL" gh-pages
fi

- name: Copy built site into gh-pages under branch folder
run: |
set -e
rsync -a --delete site/${{ env.BRANCH_NAME }}/ gh-pages/${{ env.BRANCH_NAME }}/
# ensure a top-level index exists listing branches (optional)
python3 - <<'PY'
import os, json
root='gh-pages'
items = sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root,d))])
index_path = os.path.join(root, 'index.html')
with open(index_path, 'w') as f:
f.write("<html><head><title>Published branches</title></head><body>")
f.write("<h1>Published branches</h1><ul>")
for d in items:
f.write(f'<li><a href="{d}/">{d}</a></li>')
f.write("</ul></body></html>")
PY

- name: Commit and push changes to gh-pages
working-directory: gh-pages
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add --all
if git diff --quiet --cached; then
echo "No changes to deploy"
else
git commit -m "Deploy notebooks for branch '${{ env.BRANCH_NAME }}' [ci skip]"
git push origin gh-pages
fi
42 changes: 42 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Build artifacts
site/
gh-pages/
_site/

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Jupyter Notebook
.ipynb_checkpoints

# Virtual environments
venv/
ENV/
env/

# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
jupyter
nbconvert
nbformat
pytest
flake8
80 changes: 80 additions & 0 deletions scripts/build_notebooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
Execute all notebooks in the repository (recursive) and export them to HTML into an output folder.

Usage:
python scripts/build_notebooks.py <output_dir>
"""
import sys
import os
import glob
import subprocess

def main():
if len(sys.argv) < 2:
print("Usage: python scripts/build_notebooks.py <output_dir>")
sys.exit(1)

output_dir = sys.argv[1]
os.makedirs(output_dir, exist_ok=True)

# Find all notebooks recursively
notebooks = glob.glob('**/*.ipynb', recursive=True)
# Exclude notebooks in output_dir if it's a subdirectory
notebooks = [nb for nb in notebooks if not nb.startswith(output_dir)]

if not notebooks:
print("No notebooks found.")
# Create a placeholder index
index_path = os.path.join(output_dir, 'index.html')
with open(index_path, 'w') as f:
f.write("<html><body><h1>No notebooks found</h1></body></html>")
sys.exit(0)

print(f"Found {len(notebooks)} notebook(s)")

results = []
for nb in notebooks:
print(f"Processing {nb}...")
# Determine output HTML path, preserving directory structure
# Replace path separators with underscores to avoid conflicts
html_name = nb.replace('/', '_').replace('.ipynb', '.html')
html_path = os.path.join(output_dir, html_name)

try:
# Execute and convert to HTML
subprocess.check_call([
sys.executable, "-m", "jupyter", "nbconvert",
"--to", "html",
"--execute",
"--ExecutePreprocessor.timeout=600",
"--output", html_path,
nb
])
print(f" -> {html_path}")
results.append((nb, html_name, True, None))
except subprocess.CalledProcessError as e:
print(f" FAILED: {e}")
# Create an error page
with open(html_path, 'w') as f:
f.write(f"<html><body><h1>Execution failed for {nb}</h1><pre>{e}</pre></body></html>")
results.append((nb, html_name, False, str(e)))

# Create index.html listing all notebooks
index_path = os.path.join(output_dir, 'index.html')
with open(index_path, 'w') as f:
f.write("<html><head><title>Notebooks</title></head><body>")
f.write("<h1>Notebooks</h1><ul>")
for nb, html_name, success, error in results:
status = "✓" if success else "✗"
f.write(f'<li>{status} <a href="{html_name}">{nb}</a>')
if not success:
f.write(f' <span style="color:red">(failed)</span>')
f.write('</li>')
f.write("</ul></body></html>")

print(f"\nIndex created at {index_path}")
print(f"Successfully built {sum(1 for _, _, s, _ in results if s)}/{len(results)} notebooks")

if __name__ == "__main__":
main()
Loading