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

") +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

") + + 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

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

") + 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