From c547ec94cdd68f2cb9d61a7345351f0efe139530 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 01:00:39 +0000
Subject: [PATCH 1/4] Initial plan
From 141a22a70c70c745ef58048619cf8c7b084cb6d3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 01:06:10 +0000
Subject: [PATCH 2/4] Add CI and notebook deployment workflows with supporting
scripts
Co-authored-by: solveforceapp <98552991+solveforceapp@users.noreply.github.com>
---
.github/workflows/ci.yml | 51 +++++++++++
.github/workflows/deploy-notebooks-pages.yml | 91 ++++++++++++++++++++
.gitignore | 42 +++++++++
requirements.txt | 5 ++
scripts/build_notebooks.py | 79 +++++++++++++++++
5 files changed, 268 insertions(+)
create mode 100644 .github/workflows/ci.yml
create mode 100644 .github/workflows/deploy-notebooks-pages.yml
create mode 100644 .gitignore
create mode 100644 requirements.txt
create mode 100755 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..be6cc29
--- /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("")
+ f.write("
Published branches
")
+ for d in items:
+ f.write(f'- {d}
')
+ f.write("
")
+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..561aee3
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..9962e38
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+jupyter
+nbconvert
+nbformat
+pytest
+flake8
diff --git a/scripts/build_notebooks.py b/scripts/build_notebooks.py
new file mode 100755
index 0000000..338c9e4
--- /dev/null
+++ b/scripts/build_notebooks.py
@@ -0,0 +1,79 @@
+#!/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
+"""
+import sys
+import os
+import glob
+import subprocess
+
+def main():
+ if len(sys.argv) < 2:
+ print("Usage: python scripts/build_notebooks.py ")
+ 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("No notebooks found
")
+ sys.exit(0)
+
+ print(f"Found {len(notebooks)} notebook(s)")
+
+ results = []
+ for nb in notebooks:
+ print(f"Processing {nb}...")
+ # Determine output HTML path
+ html_name = os.path.basename(nb).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"Execution failed for {nb}
{e}")
+ 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("Notebooks")
+ f.write("Notebooks
")
+ for nb, html_name, success, error in results:
+ status = "✓" if success else "✗"
+ f.write(f'- {status} {nb}')
+ if not success:
+ f.write(f' (failed)')
+ f.write('
')
+ f.write("
")
+
+ 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()
From a8a485637b3d9cd35c245fd5143ff36cd5ceef91 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 01:09:13 +0000
Subject: [PATCH 3/4] Fix code review issues: remove duplicates, improve error
handling, preserve directory structure
Co-authored-by: solveforceapp <98552991+solveforceapp@users.noreply.github.com>
---
.github/workflows/deploy-notebooks-pages.yml | 25 ++++++++++----------
scripts/build_notebooks.py | 5 ++--
2 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/deploy-notebooks-pages.yml b/.github/workflows/deploy-notebooks-pages.yml
index be6cc29..6e10c24 100644
--- a/.github/workflows/deploy-notebooks-pages.yml
+++ b/.github/workflows/deploy-notebooks-pages.yml
@@ -34,7 +34,6 @@ jobs:
- 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 }}
@@ -50,7 +49,7 @@ jobs:
cd gh-pages
git init
git remote add origin "$REPO_URL"
- git checkout -b gh-pages || true
+ git checkout -b gh-pages 2>/dev/null || git checkout gh-pages
touch .nojekyll
git add .nojekyll
git commit -m "Initialize gh-pages"
@@ -65,17 +64,17 @@ jobs:
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("")
- f.write("Published branches
")
- for d in items:
- f.write(f'- {d}
')
- f.write("
")
-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("Published branches")
+ f.write("Published branches
")
+ for d in items:
+ f.write(f'- {d}
')
+ f.write("
")
+ PY
- name: Commit and push changes to gh-pages
working-directory: gh-pages
diff --git a/scripts/build_notebooks.py b/scripts/build_notebooks.py
index 338c9e4..7767e93 100755
--- a/scripts/build_notebooks.py
+++ b/scripts/build_notebooks.py
@@ -36,8 +36,9 @@ def main():
results = []
for nb in notebooks:
print(f"Processing {nb}...")
- # Determine output HTML path
- html_name = os.path.basename(nb).replace('.ipynb', '.html')
+ # 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:
From 83f581dc53474fa13c44ba5a59fc2e5555487d23 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 01:11:22 +0000
Subject: [PATCH 4/4] Add explicit permissions to CI workflow for security
Co-authored-by: solveforceapp <98552991+solveforceapp@users.noreply.github.com>
---
.github/workflows/ci.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c0a77f5..f02c0a7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,8 @@ on:
jobs:
test:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
steps:
- name: Checkout
uses: actions/checkout@v4