Skip to content

Commit eab561a

Browse files
authored
Add workflow to format Jupyter notebooks for GitHub
This workflow formats Jupyter notebooks for GitHub compatibility by converting VS Code XML format to standard JSON, adding missing widget state metadata, and ensuring compliance with GitHub's requirements.
1 parent 89d478f commit eab561a

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
name: Format Notebook for GitHub
2+
3+
on:
4+
push:
5+
paths:
6+
- '**.ipynb'
7+
pull_request:
8+
branches:
9+
- main
10+
workflow_dispatch: # Allows manual triggering
11+
12+
permissions:
13+
contents: write
14+
pull-requests: write
15+
16+
jobs:
17+
format-notebooks:
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
with:
24+
fetch-depth: 0
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@v4
28+
with:
29+
python-version: '3.10'
30+
31+
- name: Install dependencies
32+
run: |
33+
python -m pip install --upgrade pip
34+
pip install nbformat nbconvert jupyter
35+
36+
- name: Check and fix notebook format
37+
run: |
38+
# Create a script to fix notebooks
39+
cat > fix_notebooks.py << 'EOF'
40+
import os
41+
import json
42+
import nbformat
43+
from nbformat.validator import validate
44+
45+
# Find all notebook files
46+
notebook_files = []
47+
for root, dirs, files in os.walk('.'):
48+
if '.git' in dirs:
49+
dirs.remove('.git')
50+
for file in files:
51+
if file.endswith('.ipynb'):
52+
notebook_files.append(os.path.join(root, file))
53+
54+
print(f"Found {len(notebook_files)} notebooks to process")
55+
56+
# Process each notebook
57+
for nb_path in notebook_files:
58+
print(f"Processing {nb_path}")
59+
try:
60+
# Read the notebook
61+
with open(nb_path, 'r', encoding='utf-8') as f:
62+
content = f.read()
63+
64+
# Handle VS Code XML format
65+
if '<VSCode.Cell' in content:
66+
print(f" Converting from VS Code format...")
67+
# This is a simple conversion - in a real workflow you'd need more sophisticated parsing
68+
cells = []
69+
70+
# Extract markdown and code cells
71+
import re
72+
cell_pattern = re.compile(r'<VSCode.Cell.*?language="(.*?)".*?>(.*?)</VSCode.Cell>', re.DOTALL)
73+
for match in cell_pattern.finditer(content):
74+
cell_type, cell_content = match.groups()
75+
76+
if cell_type == "markdown":
77+
cells.append(nbformat.v4.new_markdown_cell(cell_content.strip()))
78+
elif cell_type in ["python", "javascript", "java", "typescript"]:
79+
cells.append(nbformat.v4.new_code_cell(cell_content.strip()))
80+
81+
# Create a new notebook
82+
nb = nbformat.v4.new_notebook()
83+
nb.cells = cells
84+
85+
# Add metadata
86+
nb.metadata = {
87+
"kernelspec": {
88+
"display_name": "Python 3",
89+
"language": "python",
90+
"name": "python3"
91+
},
92+
"language_info": {
93+
"codemirror_mode": {
94+
"name": "ipython",
95+
"version": 3
96+
},
97+
"file_extension": ".py",
98+
"mimetype": "text/x-python",
99+
"name": "python",
100+
"nbconvert_exporter": "python",
101+
"pygments_lexer": "ipython3",
102+
"version": "3.8.10"
103+
}
104+
}
105+
else:
106+
# Standard JSON format notebook
107+
nb = nbformat.reads(content, as_version=4)
108+
109+
# Ensure widget state exists if needed
110+
if "widgets" in str(content):
111+
print(f" Adding widget state metadata...")
112+
if "metadata" not in nb:
113+
nb["metadata"] = {}
114+
if "widgets" not in nb["metadata"]:
115+
nb.metadata["widgets"] = {
116+
"application/vnd.jupyter.widget-state+json": {
117+
"state": {},
118+
"version_major": 2,
119+
"version_minor": 0
120+
}
121+
}
122+
123+
# Validate the notebook
124+
validate(nb)
125+
126+
# Write the fixed notebook
127+
with open(nb_path, 'w', encoding='utf-8') as f:
128+
nbformat.write(nb, f)
129+
130+
print(f" Successfully processed {nb_path}")
131+
132+
except Exception as e:
133+
print(f" Error processing {nb_path}: {str(e)}")
134+
continue
135+
EOF
136+
137+
# Run the notebook fixing script
138+
python fix_notebooks.py
139+
140+
- name: Configure Git
141+
run: |
142+
git config --global user.name "github-actions[bot]"
143+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
144+
145+
- name: Commit changes (if any)
146+
run: |
147+
git add "*.ipynb"
148+
git commit -m "Fix notebook format for GitHub compatibility" || echo "No changes to commit"
149+
150+
- name: Push changes (PR)
151+
if: github.event_name == 'pull_request'
152+
env:
153+
TOKEN: ${{ secrets.GITHUB_TOKEN }}
154+
run: |
155+
git fetch origin
156+
git checkout -b ${{ github.event.pull_request.head.ref }} origin/${{ github.event.pull_request.head.ref }}
157+
git pull --rebase origin ${{ github.event.pull_request.head.ref }} || echo "No rebase needed"
158+
git push origin HEAD:${{ github.event.pull_request.head.ref }}
159+
160+
- name: Push changes (non-PR)
161+
if: github.event_name != 'pull_request'
162+
env:
163+
TOKEN: ${{ secrets.GITHUB_TOKEN }}
164+
run: |
165+
git remote set-url origin https://x-access-token:${TOKEN}@github.com/${{ github.repository }}
166+
git push || echo "No changes to push"
167+
168+
- name: Create Pull Request (non-PR)
169+
if: github.event_name != 'pull_request'
170+
uses: peter-evans/create-pull-request@v6
171+
with:
172+
token: ${{ secrets.GITHUB_TOKEN }}
173+
branch: fix-notebook-format
174+
title: "Fix notebook format for GitHub compatibility"
175+
body: |
176+
This PR fixes Jupyter notebook formatting issues to ensure proper rendering on GitHub.
177+
178+
The workflow addresses:
179+
- Converting VS Code XML format to standard Jupyter JSON format
180+
- Adding missing widget state metadata
181+
- Ensuring notebook format complies with GitHub's requirements
182+
183+
These changes allow notebooks to render properly in GitHub's notebook viewer.
184+
base: main

0 commit comments

Comments
 (0)