1+ name : Deploy Notebooks to GH Pages
2+ on :
3+ push :
4+ branches : [ main ]
5+
6+ permissions :
7+ contents : read
8+ pages : write
9+ id-token : write
10+
11+ jobs :
12+ build-and-deploy :
13+ runs-on : ubuntu-latest
14+ steps :
15+ - name : Checkout
16+ uses : actions/checkout@v4
17+
18+ - name : Set up Python
19+ uses : actions/setup-python@v5
20+ with :
21+ python-version : ' 3.10'
22+ cache : ' pip'
23+
24+ - name : Install dependencies
25+ run : |
26+ # Install jupyter and nbconvert (always needed)
27+ pip install jupyter nbconvert
28+
29+ # Install requirements.txt if it exists
30+ if [ -f requirements.txt ]; then
31+ pip install -r requirements.txt
32+ fi
33+
34+ - name : Convert Notebooks to HTML
35+ run : |
36+ mkdir -p public
37+
38+ # Find ALL .ipynb files recursively, preserving directory structure
39+ find . -name "*.ipynb" -not -path "./public/*" -not -path "./.git/*" | while read notebook; do
40+ # Remove leading "./" from path
41+ clean_path="${notebook#./}"
42+
43+ # Get the directory of the notebook
44+ notebook_dir=$(dirname "$clean_path")
45+
46+ # Create the corresponding directory in public/
47+ mkdir -p "public/$notebook_dir"
48+
49+ echo "Converting $clean_path to public/$notebook_dir/"
50+ jupyter nbconvert --to html --template lab --output-dir="public/$notebook_dir" "$clean_path" || echo "Warning: Failed to convert $clean_path"
51+ done
52+
53+ # Copy ALL supporting files (images, data, etc.) - find all non-.ipynb files
54+ find . -type f -not -name "*.ipynb" \
55+ -not -path "./public/*" \
56+ -not -path "./.git/*" \
57+ -not -path "./.github/*" \
58+ -not -name "requirements.txt" \
59+ -not -name "README.md" \
60+ -not -name "LICENSE" \
61+ -not -name ".gitignore" \
62+ -not -name "*.yml" \
63+ -not -name "*.py" | while read file; do
64+ clean_path="${file#./}"
65+ target_dir="public/$(dirname "$clean_path")"
66+ mkdir -p "$target_dir"
67+ cp "$file" "$target_dir/" 2>/dev/null || true
68+ done
69+
70+ # Clean up notebook headers and inject blog header loader
71+ find public -name "*.html" -not -name "index.html" -exec sed -i '/<header class="site-header">/,/<\/header>/d' {} +
72+ find public -name "*.html" -not -name "index.html" -exec sed -i 's|<body>|<body><script src="https://chizkidd.github.io/header-loader.js"></script>|' {} +
73+
74+ # Get repo name and current date
75+ REPO_NAME="${GITHUB_REPOSITORY#*/}"
76+ LAST_UPDATED=$(date +"%B %d, %Y")
77+
78+ # Get repo title (from README or use repo name)
79+ REPO_TITLE=$(grep -m1 "^# " README.md 2>/dev/null | sed 's/^# //' || echo "$REPO_NAME")
80+
81+ echo "=== Building index for: $REPO_TITLE ==="
82+
83+ # Generate the index.html landing page
84+ cat > public/index.html << HTMLEOF
85+ <!DOCTYPE html>
86+ <html>
87+ <head>
88+ <meta charset="UTF-8">
89+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
90+ <title>$REPO_TITLE</title>
91+ <style>
92+ * { margin: 0; padding: 0; }
93+ html, body { height: 100%; }
94+ body {
95+ font-family: Helvetica, Arial, sans-serif;
96+ font-size: 16px;
97+ line-height: 1.5;
98+ font-weight: 300;
99+ background-color: #fdfdfd;
100+ display: grid;
101+ grid-template-rows: auto 1fr auto;
102+ min-height: 100vh;
103+ }
104+ h1, h2, h3, h4, h5, h6 { font-size: 100%; font-weight: 400; }
105+ a { color: #2a7ae2; text-decoration: none; }
106+ a:hover { color: #000; text-decoration: underline; }
107+ a:visited { color: #205caa; }
108+ .wrap { max-width: 800px; padding: 0 30px; margin: 0 auto; }
109+ .page-content { padding: 30px 0; background-color: #fff; }
110+ .header-matched-heading {
111+ color: #333;
112+ font-size: 32px;
113+ letter-spacing: -1.2px;
114+ font-weight: 400;
115+ text-align: left;
116+ line-height: 1.1;
117+ margin-top: 30px;
118+ margin-bottom: 25px;
119+ }
120+ .section-heading {
121+ font-size: 20px;
122+ font-weight: 500;
123+ color: #555;
124+ border-bottom: 1px solid #f0f0f0;
125+ padding-bottom: 5px;
126+ margin-top: 40px;
127+ margin-bottom: 15px;
128+ }
129+ .home-nav-list {
130+ padding-left: 20px;
131+ margin-bottom: 30px;
132+ list-style-type: disc;
133+ }
134+ .home-nav-list li { margin-bottom: 8px; }
135+ .home-nav-list a { font-size: 1.0em; font-weight: 300; color: #2a7ae2; }
136+ .home-nav-list a:hover { color: #000; text-decoration: underline; }
137+ .site-footer {
138+ border-top: 1px solid #e8e8e8;
139+ padding: 30px 0;
140+ }
141+ .site-footer p {
142+ font-size: 15px;
143+ letter-spacing: -.3px;
144+ color: #828282;
145+ text-align: center;
146+ }
147+ .site-footer a { color: #2a7ae2; }
148+ .last-updated { margin-top: 10px; font-size: 13px; color: #999; }
149+ @media screen and (max-width: 600px) {
150+ .wrap { padding: 0 12px; }
151+ }
152+ </style>
153+ </head>
154+ <body>
155+ <script src="https://chizkidd.github.io/header-loader.js"></script>
156+
157+ <div class="page-content">
158+ <div class="wrap">
159+ <h1 class="header-matched-heading">$REPO_TITLE</h1>
160+ <div id="notebook-sections"></div>
161+ </div>
162+ </div>
163+
164+ <footer class="site-footer">
165+ <div class="wrap">
166+ <p>
167+ <a href="https://github.com/${GITHUB_REPOSITORY}" target="_blank">View Repository</a>
168+ </p>
169+ <p class="last-updated">Last updated: $LAST_UPDATED</p>
170+ </div>
171+ </footer>
172+
173+ <script>
174+ const sections = document.getElementById('notebook-sections');
175+ HTMLEOF
176+
177+ # Build JavaScript array of all notebooks
178+ echo " const notebooks = [" >> public/index.html
179+ find public -name "*.html" -not -name "index.html" | while read html_file; do
180+ # Get relative path from public/
181+ rel_path="${html_file#public/}"
182+ # Get display name (remove .html, replace underscores/dashes with spaces)
183+ display_name=$(basename "$rel_path" .html | sed 's/[_-]/ /g')
184+ # Get folder for grouping
185+ folder=$(dirname "$rel_path")
186+ echo " {file: '$rel_path', name: '$display_name', folder: '$folder'}," >> public/index.html
187+ done
188+ echo " ];" >> public/index.html
189+
190+ cat >> public/index.html << 'JSEOF'
191+
192+ // Group notebooks by folder
193+ const grouped = {};
194+ notebooks.forEach(nb => {
195+ const folder = nb.folder === '.' ? 'Root' : nb.folder;
196+ if (!grouped[folder]) grouped[folder] = [];
197+ grouped[folder].push(nb);
198+ });
199+
200+ // Sort folders and notebooks within folders
201+ Object.keys(grouped).sort().forEach(folder => {
202+ const section = document.createElement('div');
203+ section.className = 'section';
204+
205+ const heading = document.createElement('h2');
206+ heading.className = 'section-heading';
207+ heading.textContent = folder.replace(/[_-]/g, ' ').replace(/\//g, ' / ');
208+ section.appendChild(heading);
209+
210+ const list = document.createElement('ul');
211+ list.className = 'home-nav-list';
212+
213+ // Sort notebooks alphabetically
214+ grouped[folder].sort((a, b) => a.name.localeCompare(b.name));
215+
216+ grouped[folder].forEach(nb => {
217+ const li = document.createElement('li');
218+ const a = document.createElement('a');
219+ a.href = nb.file;
220+ a.textContent = nb.name;
221+ li.appendChild(a);
222+ list.appendChild(li);
223+ });
224+
225+ section.appendChild(list);
226+ sections.appendChild(section);
227+ });
228+ </script>
229+ </body>
230+ </html>
231+ JSEOF
232+
233+ - name : Upload artifact
234+ uses : actions/upload-pages-artifact@v3
235+ with :
236+ path : ' public'
237+
238+ - name : Deploy to GitHub Pages
239+ id : deployment
240+ uses : actions/deploy-pages@v4
0 commit comments