Skip to content

Commit a9ef3cc

Browse files
committed
add github pages deploy action
1 parent ebaf057 commit a9ef3cc

File tree

6 files changed

+614
-0
lines changed

6 files changed

+614
-0
lines changed

.github/scripts/build.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""
2+
Build script for marimo notebooks.
3+
4+
This script exports marimo notebooks to HTML/WebAssembly format and generates
5+
an index.html file that lists all the notebooks. It handles both regular notebooks
6+
(from the notebooks/ directory) and apps (from the apps/ directory).
7+
8+
The script can be run from the command line with optional arguments:
9+
uv run .github/scripts/build.py [--output-dir OUTPUT_DIR]
10+
11+
The exported files will be placed in the specified output directory (default: _site).
12+
"""
13+
14+
# /// script
15+
# requires-python = ">=3.12"
16+
# dependencies = [
17+
# "jinja2==3.1.3",
18+
# "fire==0.7.0",
19+
# "loguru==0.7.0"
20+
# ]
21+
# ///
22+
23+
import subprocess
24+
from pathlib import Path
25+
from typing import List, Union
26+
27+
import fire
28+
import jinja2
29+
from loguru import logger
30+
31+
32+
def _export_html_wasm(
33+
notebook_path: Path, output_dir: Path, as_app: bool = False
34+
) -> bool:
35+
"""Export a single marimo notebook to HTML/WebAssembly format.
36+
37+
This function takes a marimo notebook (.py file) and exports it to HTML/WebAssembly format.
38+
If as_app is True, the notebook is exported in "run" mode with code hidden, suitable for
39+
applications. Otherwise, it's exported in "edit" mode, suitable for interactive notebooks.
40+
41+
Args:
42+
notebook_path (Path): Path to the marimo notebook (.py file) to export
43+
output_dir (Path): Directory where the exported HTML file will be saved
44+
as_app (bool, optional): Whether to export as an app (run mode) or notebook (edit mode).
45+
Defaults to False.
46+
47+
Returns:
48+
bool: True if export succeeded, False otherwise
49+
"""
50+
# Convert .py extension to .html for the output file
51+
output_path: Path = notebook_path.with_suffix(".html")
52+
53+
# Base command for marimo export
54+
cmd: List[str] = ["uvx", "marimo", "export", "html-wasm", "--sandbox"]
55+
56+
# Configure export mode based on whether it's an app or a notebook
57+
if as_app:
58+
logger.info(f"Exporting {notebook_path} to {output_path} as app")
59+
cmd.extend(
60+
["--mode", "run", "--no-show-code"]
61+
) # Apps run in "run" mode with hidden code
62+
else:
63+
logger.info(f"Exporting {notebook_path} to {output_path} as notebook")
64+
cmd.extend(["--mode", "edit"]) # Notebooks run in "edit" mode
65+
66+
try:
67+
# Create full output path and ensure directory exists
68+
output_file: Path = output_dir / notebook_path.with_suffix(".html")
69+
output_file.parent.mkdir(parents=True, exist_ok=True)
70+
71+
# Add notebook path and output file to command
72+
cmd.extend([str(notebook_path), "-o", str(output_file)])
73+
74+
# Run marimo export command
75+
logger.debug(f"Running command: {cmd}")
76+
subprocess.run(cmd, capture_output=True, text=True, check=True)
77+
logger.info(f"Successfully exported {notebook_path}")
78+
return True
79+
except subprocess.CalledProcessError as e:
80+
# Handle marimo export errors
81+
logger.error(f"Error exporting {notebook_path}:")
82+
logger.error(f"Command output: {e.stderr}")
83+
return False
84+
except Exception as e:
85+
# Handle unexpected errors
86+
logger.error(f"Unexpected error exporting {notebook_path}: {e}")
87+
return False
88+
89+
90+
def _generate_index(
91+
output_dir: Path,
92+
template_file: Path,
93+
notebooks_data: List[dict] | None = None,
94+
apps_data: List[dict] | None = None,
95+
) -> None:
96+
"""Generate an index.html file that lists all the notebooks.
97+
98+
This function creates an HTML index page that displays links to all the exported
99+
notebooks. The index page includes the marimo logo and displays each notebook
100+
with a formatted title and a link to open it.
101+
102+
Args:
103+
notebooks_data (List[dict]): List of dictionaries with data for notebooks
104+
apps_data (List[dict]): List of dictionaries with data for apps
105+
output_dir (Path): Directory where the index.html file will be saved
106+
template_file (Path, optional): Path to the template file. If None, uses the default template.
107+
108+
Returns:
109+
None
110+
"""
111+
logger.info("Generating index.html")
112+
113+
# Create the full path for the index.html file
114+
index_path: Path = output_dir / "index.html"
115+
116+
# Ensure the output directory exists
117+
output_dir.mkdir(parents=True, exist_ok=True)
118+
119+
try:
120+
# Set up Jinja2 environment and load template
121+
template_dir = template_file.parent
122+
template_name = template_file.name
123+
env = jinja2.Environment(
124+
loader=jinja2.FileSystemLoader(template_dir),
125+
autoescape=jinja2.select_autoescape(["html", "xml"]),
126+
)
127+
template = env.get_template(template_name)
128+
129+
# Render the template with notebook and app data
130+
rendered_html = template.render(notebooks=notebooks_data, apps=apps_data)
131+
132+
# Write the rendered HTML to the index.html file
133+
with open(index_path, "w") as f:
134+
f.write(rendered_html)
135+
logger.info(f"Successfully generated index.html at {index_path}")
136+
137+
except IOError as e:
138+
# Handle file I/O errors
139+
logger.error(f"Error generating index.html: {e}")
140+
except jinja2.exceptions.TemplateError as e:
141+
# Handle template errors
142+
logger.error(f"Error rendering template: {e}")
143+
144+
145+
def _export(folder: Path, output_dir: Path, as_app: bool = False) -> List[dict]:
146+
"""Export all marimo notebooks in a folder to HTML/WebAssembly format.
147+
148+
This function finds all Python files in the specified folder and exports them
149+
to HTML/WebAssembly format using the export_html_wasm function. It returns a
150+
list of dictionaries containing the data needed for the template.
151+
152+
Args:
153+
folder (Path): Path to the folder containing marimo notebooks
154+
output_dir (Path): Directory where the exported HTML files will be saved
155+
as_app (bool, optional): Whether to export as apps (run mode) or notebooks (edit mode).
156+
157+
Returns:
158+
List[dict]: List of dictionaries with "display_name" and "html_path" for each notebook
159+
"""
160+
# Check if the folder exists
161+
if not folder.exists():
162+
logger.warning(f"Directory not found: {folder}")
163+
return []
164+
165+
# Find all Python files recursively in the folder
166+
notebooks = list(folder.rglob("*.py"))
167+
logger.debug(f"Found {len(notebooks)} Python files in {folder}")
168+
169+
# Exit if no notebooks were found
170+
if not notebooks:
171+
logger.warning(f"No notebooks found in {folder}!")
172+
return []
173+
174+
# For each successfully exported notebook, add its data to the notebook_data list
175+
notebook_data = [
176+
{
177+
"display_name": (nb.stem.replace("_", " ").title()),
178+
"html_path": str(nb.with_suffix(".html")),
179+
}
180+
for nb in notebooks
181+
if _export_html_wasm(nb, output_dir, as_app=as_app)
182+
]
183+
184+
logger.info(
185+
f"Successfully exported {len(notebook_data)} out of {len(notebooks)} files from {folder}"
186+
)
187+
return notebook_data
188+
189+
190+
def main(
191+
output_dir: Union[str, Path] = "_site",
192+
template: Union[str, Path] = "templates/tailwind.html.j2",
193+
) -> None:
194+
"""Main function to export marimo notebooks.
195+
196+
This function:
197+
1. Parses command line arguments
198+
2. Exports all marimo notebooks in the 'notebooks' and 'apps' directories
199+
3. Generates an index.html file that lists all the notebooks
200+
201+
Command line arguments:
202+
--output-dir: Directory where the exported files will be saved (default: _site)
203+
--template: Path to the template file (default: templates/index.html.j2)
204+
205+
Returns:
206+
None
207+
"""
208+
logger.info("Starting marimo build process")
209+
210+
# Convert output_dir explicitly to Path (not done by fire)
211+
output_dir: Path = Path(output_dir)
212+
logger.info(f"Output directory: {output_dir}")
213+
214+
# Make sure the output directory exists
215+
output_dir.mkdir(parents=True, exist_ok=True)
216+
217+
# Convert template to Path if provided
218+
template_file: Path = Path(template)
219+
logger.info(f"Using template file: {template_file}")
220+
221+
# Export notebooks from the notebooks/ directory
222+
notebooks_data = _export(Path("notebooks"), output_dir, as_app=False)
223+
224+
# Export apps from the apps/ directory
225+
apps_data = _export(Path("apps"), output_dir, as_app=True)
226+
227+
# Exit if no notebooks or apps were found
228+
if not notebooks_data and not apps_data:
229+
logger.warning("No notebooks or apps found!")
230+
return
231+
232+
# Generate the index.html file that lists all notebooks and apps
233+
_generate_index(
234+
output_dir=output_dir,
235+
notebooks_data=notebooks_data,
236+
apps_data=apps_data,
237+
template_file=template_file,
238+
)
239+
240+
logger.info(f"Build completed successfully. Output directory: {output_dir}")
241+
242+
243+
if __name__ == "__main__":
244+
fire.Fire(main)

.github/workflows/publish.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# This workflow builds and deploys marimo notebooks to GitHub Pages
2+
# It runs automatically when changes are pushed to the main branch or can be triggered manually
3+
4+
name: Deploy to GitHub Pages
5+
6+
# Defines when the workflow will run
7+
on:
8+
push:
9+
branches: ['main'] # Trigger on pushes to main branch
10+
workflow_dispatch: # Allow manual triggering from the GitHub UI
11+
12+
# Concurrency settings to manage multiple workflow runs
13+
concurrency:
14+
group: 'pages' # Only one workflow in the 'pages' group can run at a time
15+
cancel-in-progress: false # Don't cancel in-progress runs when a new one is triggered
16+
17+
# Environment variables used by the workflow
18+
env:
19+
UV_SYSTEM_PYTHON: 1 # Use system Python with uv package manager
20+
21+
jobs:
22+
# The build job exports marimo notebooks to static HTML/WebAssembly
23+
build:
24+
runs-on: ubuntu-latest # Use the latest Ubuntu runner
25+
steps:
26+
# Check out the repository code
27+
- uses: actions/checkout@v4
28+
29+
# Install uv package manager for faster Python package installation
30+
- name: 🚀 Install uv
31+
uses: astral-sh/setup-uv@v6
32+
33+
# Run the build script to export notebooks to WebAssembly
34+
- name: 🛠️ Export notebooks
35+
run: |
36+
# todo: Ultimately this function should not be part of this repo
37+
# It should very much be an action such that other repos
38+
# can use it without forking or copying it
39+
# No, it should not be an action. As otherwise can't run before push
40+
uv run .github/scripts/build.py # This script exports all notebooks to the _site directory
41+
tree _site # Display the exported files
42+
43+
# Upload the generated site as an artifact for the deploy job
44+
- name: 📤 Upload artifact
45+
uses: actions/upload-pages-artifact@v3
46+
with:
47+
path: _site # Directory containing the built site
48+
49+
# The deploy job publishes the built site to GitHub Pages
50+
deploy:
51+
needs: build # This job depends on the build job completing successfully
52+
53+
# Required permissions for the GitHub Pages deployment
54+
permissions:
55+
pages: write # Permission to deploy to Pages
56+
id-token: write # Permission to verify the deployment
57+
58+
# Configure the deployment environment
59+
environment:
60+
name: github-pages # Deploy to the github-pages environment
61+
url: ${{ steps.deployment.outputs.page_url }} # Use the URL from the deployment step
62+
63+
runs-on: ubuntu-latest # Use the latest Ubuntu runner
64+
steps:
65+
# Deploy the site to GitHub Pages using the official action
66+
- name: 🚀 Deploy to GitHub Pages
67+
id: deployment # ID used to reference this step's outputs
68+
uses: actions/deploy-pages@v4 # GitHub's official Pages deployment action

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ marimo edit --sandbox notebooks/getting_started.py
1919
## Notebooks
2020

2121
- [Getting Started](notebooks/getting_started.py) - simple guide to sending your first probes, same as the [measurements](https://nxthdr.dev/docs/measurements) documentation
22+
23+
## License
24+
25+
This project is licensed under the MIT License. See the [LICENSE](notebooks/LICENSE) file for details.
26+
However, parts of the code in `.github/` and `templates/` are adapted from [marimo-gh-pages-template](https://github.com/marimo-team/marimo-gh-pages-template) repository which is licensed under the Apache License 2.0. See the [LICENSE](https://github.com/marimo-team/marimo-gh-pages-template/blob/main/LICENSE) file for details.

0 commit comments

Comments
 (0)