From 234cb0bea29d8c1a19f8c66b0813bff8fe08cf2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 00:25:57 +0000 Subject: [PATCH 1/2] Initial plan From 137837eebae33cd710538fc4d7dc7c7f5fb68e21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 00:42:05 +0000 Subject: [PATCH 2/2] Add CI workflows, notebook build scripts, and documentation Co-authored-by: solveforceapp <98552991+solveforceapp@users.noreply.github.com> --- .github/workflows/ci.yml | 51 +++++++++++ .github/workflows/deploy-notebooks-pages.yml | 91 ++++++++++++++++++++ .gitignore | 7 ++ .pre-commit-config.yaml | 11 +++ Dockerfile | 9 ++ README.md | 19 +++- requirements.txt | 7 ++ scripts/build_notebooks.py | 71 +++++++++++++++ 8 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy-notebooks-pages.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 Dockerfile create mode 100644 requirements.txt create mode 100644 scripts/build_notebooks.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c0a77f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + 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 diff --git a/.github/workflows/deploy-notebooks-pages.yml b/.github/workflows/deploy-notebooks-pages.yml new file mode 100644 index 0000000..34795c2 --- /dev/null +++ b/.github/workflows/deploy-notebooks-pages.yml @@ -0,0 +1,91 @@ +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: | + python -m pip install --upgrade pip + 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 || true + 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("Branches") + f.write("

Published branches

") +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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5edaeb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.env +.venv +__pycache__/ +site/ +gh-pages/ +*.pyc +.ipynb_checkpoints/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b7e3bc4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/psf/black + rev: 24.1.0 + hooks: + - id: black + language_version: python3.11 + + - repo: https://gitlab.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f4b574 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim + +ENV DEBIAN_FRONTEND=noninteractive +WORKDIR /app +COPY requirements.txt /app/requirements.txt +RUN python -m pip install --upgrade pip && \ + pip install --no-cache-dir -r /app/requirements.txt +COPY . /app +CMD ["bash"] diff --git a/README.md b/README.md index a161a49..217c7e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# - -~ +# Repository: Notebook pages (branch-per-branch) + +This repository contains many Jupyter notebooks. This change adds CI and an automated build-and-deploy workflow that runs on pushes to any branch and publishes rendered HTML pages for that branch under gh-pages/. + +Files added: +- .github/workflows/ci.yml — runs lint/tests and executes notebooks on pushes/PRs to main. +- .github/workflows/deploy-notebooks-pages.yml — builds and deploys per-branch HTML pages to gh-pages when any branch is pushed. +- scripts/build_notebooks.py — script that executes notebooks and exports them to HTML. +- requirements.txt, Dockerfile, .pre-commit-config.yaml, .gitignore — tooling and environment files. + +How to run locally: +1. python -m venv .venv +2. source .venv/bin/activate +3. pip install -r requirements.txt +4. python scripts/build_notebooks.py site/local-branch + +Enable GitHub Pages to serve the gh-pages branch in repository Settings -> Pages. The per-branch pages will be available at https://.github.io/// . diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed50aa2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +jupyter +nbconvert +nbformat +pytest +flake8 +black +pre-commit diff --git a/scripts/build_notebooks.py b/scripts/build_notebooks.py new file mode 100644 index 0000000..e088ea6 --- /dev/null +++ b/scripts/build_notebooks.py @@ -0,0 +1,71 @@ +#!/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 [--timeout SECONDS] + +By default this will search for all .ipynb files (excluding .ipynb_checkpoints) and: +- execute them with a timeout +- export resulting notebook to HTML and place the HTML in preserving folder structure +""" +import sys +import os +import subprocess +from pathlib import Path + +def find_notebooks(root="."): + nbs = [] + for p in Path(root).rglob("*.ipynb"): + # skip checkpoints and files inside .git or site output + if ".ipynb_checkpoints" in p.parts or "site" in p.parts or "gh-pages" in p.parts: + continue + nbs.append(p) + return nbs + +def main(): + if len(sys.argv) < 2: + print("Usage: build_notebooks.py [--timeout SECONDS]") + sys.exit(1) + outdir = Path(sys.argv[1]) + timeout = 600 + if "--timeout" in sys.argv: + try: + timeout = int(sys.argv[sys.argv.index("--timeout")+1]) + except Exception: + pass + outdir.mkdir(parents=True, exist_ok=True) + + notebooks = find_notebooks(".") + if not notebooks: + print("No notebooks found.") + return + + print(f"Found {len(notebooks)} notebooks. Exporting to {outdir} ...") + for nb in notebooks: + rel = nb.relative_to(Path.cwd()) + target_dir = outdir.joinpath(rel.parent) + target_dir.mkdir(parents=True, exist_ok=True) + print(f"Processing {nb} -> {target_dir}") + + # Execute notebook in place into a temp file and convert to HTML + # Use nbconvert CLI to execute and export; capture exit code + try: + subprocess.check_call([ + sys.executable, "-m", "jupyter", "nbconvert", + "--to", "html", + "--execute", + "--ExecutePreprocessor.timeout={}".format(timeout), + "--output-dir", str(target_dir), + str(nb) + ]) + except subprocess.CalledProcessError as e: + print(f"ERROR executing {nb}: {e}") + # Create a placeholder HTML with the failure message so CI pages report which notebooks failed + fail_html = target_dir.joinpath(nb.stem + ".html") + with open(fail_html, "w", encoding="utf-8") as fh: + fh.write(f"

Execution failed for {nb}

{e}
") + print("Done.") + +if __name__ == "__main__": + main()