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