Putting Jupyter notebooks under source control with Quarto
Posted on Tue 13 January 2026 in Software, last modified Thu 05 February 2026
Jupyter notebooks are an excellent way to provide interactivity.
Extremely helpful when building tutorials and demos. However, putting
Jupyter notebooks under source control is messy: the .ipynb format is
a JSON-based format that combines content (code and Markdown text) with
state information (outputs, version numbers, etc.), and you need to
clean up this information before committing or else Git gets very
confused.
Also, the fact that text has to live in separate cells from code tends to break down the flow when reading.
This is where Quarto is very useful. Quarto lets
you write code and text together in a single .qmd document, a Markdown
file. Code is interpolated using a variant on standard Markdown code
blocks. The file therefore consists solely of content, no metadata or
execution outputs.
Quarto offers a lot of features for publishing notebooks as a website,
as a PDF/ePub book, and other formats, but the most useful is its
feature to convert easily between .qmd and .ipynb. Here's a
Makefile for doing this:
SHELL := /bin/bash
.ONESHELL:
QMD := $(wildcard *.qmd)
IPYNB := $(QMD:%.qmd=build/%.ipynb)
VENV := .venv
notebooks: $(IPYNB)
build/%.ipynb: %.qmd
. $(VENV)/bin/activate
mkdir -p build
quarto convert $< -o $@
clean:
rm -rf build
.PHONY: all clean
Note that this is a Makefile so the indents need to be tab characters, not spaces.
To "compile" your .qmd files into Jupyter notebooks, simply run
make notebooks
The build/ directory will now contain the notebooks, which you can
distribute. It's a good idea to put build/ under .gitignore so that
only your .qmd files go under source control.
Adding a GitHub Action (update 2026-02-05)
The following action should be saved to .github/workflows/build.yml
(or some other name in that directory), and will cause GitHub to create
a branch called notebooks that contain the .ipynb files converted
from the source .qmd files. This is very useful if you wish to share
the repository's notebooks branch such that users can clone them
directly or load them into Binder or Colab, without themselves
installing Quarto.
name: Makefile CI
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip list
- name: "Download Quarto"
shell: bash
run: |
# Hardcoding versions for now to make sure new versions don't break things
quarto_version="1.2.269"
quarto_file_name="quarto-${quarto_version}-linux-amd64.deb"
quarto_url="https://github.com/quarto-dev/quarto-cli/releases/download/v${quarto_version}/${quarto_file_name}"
wget -q "${quarto_url}"
sudo dpkg -i "${quarto_file_name}"
- name: Build notebooks
run: |
make notebooks
cp -a build /tmp/notebooks
cp requirements.txt /tmp/notebooks/
cp README.md /tmp/notebooks/
- name: Deploy notebooks to branch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin notebooks || true
git checkout -B notebooks
git reset --hard
rm -rf ./*
cp -a /tmp/notebooks/. .
git add -A
git commit -m "Deploy updated notebooks" || echo "No changes to commit"
git push -f origin notebooks