Skip to content

Latest commit

 

History

History
285 lines (231 loc) · 10.2 KB

File metadata and controls

285 lines (231 loc) · 10.2 KB

MkDocs Deploy Action

Deploy MkDocs documentation to GitHub Pages with versioning support using mike.

Overview

This action deploys MkDocs documentation with automatic version management based on the trigger event:

Trigger Version Deployed Alias
pull_request develop -
Push to main main latest (default)
release Tag version (e.g. v1.2.0) Configurable (default: latest)

Usage

- uses: serapeum-org/github-actions/actions/mkdocs-deploy@mkdocs/v1
  with:
    trigger: ${{ github.event_name }}
    deploy-token: ${{ secrets.GITHUB_TOKEN }}

Inputs

Input Description Required Default
trigger Deployment trigger type: pull_request, main, release Yes -
package-manager Package manager: pip, uv, pixi No uv
package-manager-version Version of the uv/pixi package manager (forwarded to its version input); ignored for pip. No latest
python-version Python version to install No 3.12
install-groups Dependency groups to install No groups: docs
deploy-token GitHub token for deployment Yes -
release-tag Release tag version (for release trigger) No -
mike-alias Mike alias for releases No latest
notebooks-path Root directory of Jupyter notebooks to execute and cache. Empty = skip. No `` (skip)
notebooks-exclude Newline-/comma-separated globs (relative to notebooks-path) for notebooks to skip executing. No `` (none)
notebooks-continue-on-error Downgrade a non-excluded notebook execution failure to a warning instead of aborting. No false

Examples

Pull Request Preview

Deploy a develop version on every PR for review:

name: Docs Preview

on: pull_request

permissions:
  contents: write

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - uses: serapeum-org/github-actions/actions/mkdocs-deploy@mkdocs/v1
        with:
          trigger: 'pull_request'
          deploy-token: ${{ secrets.GITHUB_TOKEN }}

Production Deployment

Deploy on push to main and on release:

name: Deploy Docs

on:
  push:
    branches: [main]
  release:
    types: [published]

permissions:
  contents: write

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - uses: serapeum-org/github-actions/actions/mkdocs-deploy@mkdocs/v1
        with:
          trigger: ${{ github.event_name == 'release' && 'release' || 'main' }}
          deploy-token: ${{ secrets.GITHUB_TOKEN }}
          release-tag: ${{ github.event.release.tag_name }}

With pixi

- uses: serapeum-org/github-actions/actions/mkdocs-deploy@mkdocs/v1
  with:
    trigger: 'main'
    deploy-token: ${{ secrets.GITHUB_TOKEN }}
    package-manager: 'pixi'
    install-groups: 'default'

With notebook execution

Deploy docs that include executable Jupyter notebooks. The action runs notebooks once, caches their outputs, and reuses the cache on subsequent runs so unchanged notebooks don't re-execute:

- uses: serapeum-org/github-actions/actions/mkdocs-deploy@mkdocs/v1
  with:
    trigger: ${{ github.event_name }}
    deploy-token: ${{ secrets.GITHUB_TOKEN }}
    notebooks-path: 'docs/notebook'

See Notebook execution below for the consumer contract.

Notebook execution

Setting notebooks-path enables a pre-build step that executes Jupyter notebooks and persists their outputs via actions/cache, so mkdocs-jupyter can render them without re-executing on every CI run. Omitting notebooks-path (the default) skips notebook execution entirely — existing workflows see no change.

Requirements

  • Dependencies: four packages must be in the dependency group passed via install-groups, split by role:

    • jupyter, nbconvert, ipykernel — used by the action's notebook execution step. The action runs a preflight import check against these three and fails fast with an actionable error if any are missing.
    • mkdocs-jupyter — used later by the mkdocs build itself to render notebooks. A missing mkdocs-jupyter passes preflight but fails at build time with an mkdocs plugin error — so add it alongside the other three.

    Concrete example (statista's layout; adapt the group name to whatever you pass via install-groups):

    # pyproject.toml
    [dependency-groups]
    docs = [
        "mkdocs>=1.5",
        "mkdocs-material",
        "mkdocs-jupyter",   # rendering (mkdocs build)
        "mike",
        "jupyter",          # preflighted; drives nbconvert --execute
        "nbconvert",
        "ipykernel",
    ]
  • mkdocs-jupyter config: set execute: false in mkdocs.yml so the plugin reads pre-executed outputs instead of re-running notebooks during the mkdocs build:

    plugins:
      - mkdocs-jupyter:
          execute: false
  • Project layout: pyproject.toml at repo root and library source under src/. The cache-invalidation hash covers notebook contents, src/**/*.py, and pyproject.toml; flat-layout projects (library code at repo root rather than under src/) are not supported out of the box.

Excluding notebooks that can't run in CI

Some notebooks can't execute on a clean runner — they need live network/cloud data, an optional extra that isn't installed for docs, or are intentionally flaky. List them in notebooks-exclude (newline- or comma-separated globs, matched against each notebook's path relative to notebooks-path) to skip their execution. Excluded notebooks are left untouched and rendered with whatever outputs they already carry. This mirrors pytest's --ignore-glob.

- uses: serapeum-org/github-actions/actions/mkdocs-deploy@mkdocs/v1
  with:
    trigger: ${{ github.event_name }}
    deploy-token: ${{ secrets.GITHUB_TOKEN }}
    notebooks-path: docs/examples
    notebooks-exclude: |
      dask/*.ipynb
      **/lazy-*-complete.ipynb
      **/stac-cloud-*.ipynb
      zarr/*.ipynb

Pattern semantics (these mirror pytest's --ignore-glob, which uses fnmatch):

  • * matches across /, so a pattern like dask/*.ipynb matches every notebook under dask/ at any depth, not just its direct children — and a bare *.ipynb matches every notebook in the tree. Anchor the pattern (e.g. dask/, or a distinctive filename stem) to avoid over-excluding.
  • A leading **/ matches at any depth including the root, so **/stac-cloud-*.ipynb also matches a top-level stac-cloud-foo.ipynb.
  • Matching is case-sensitive on every runner OS (so Foo.ipynb is not matched by foo.*).

To keep executing the rest when a non-excluded notebook fails — rather than aborting the whole deploy — set notebooks-continue-on-error: 'true'. The failing notebook is logged as a CI warning and is not cached, so it re-runs on the next CI run; use it for genuinely flaky notebooks, and prefer notebooks-exclude for ones that can never run in CI.

- uses: serapeum-org/github-actions/actions/mkdocs-deploy@mkdocs/v1
  with:
    trigger: ${{ github.event_name }}
    deploy-token: ${{ secrets.GITHUB_TOKEN }}
    notebooks-path: docs/examples
    # Never-runs-in-CI notebooks are excluded outright...
    notebooks-exclude: |
      **/stac-cloud-*.ipynb
    # ...while an occasional flaky failure only warns instead of failing.
    notebooks-continue-on-error: 'true'

Cache invalidation

The cache key is:

jupyter-cache-<hashFiles(notebooks, src/**/*.py, pyproject.toml)>

Any change to a notebook, to library source under src/, or to pyproject.toml invalidates the cache and triggers a full re-execute. There is no restore-keys fallback — primary-key-only, so rendered outputs always reflect the exact code that produced them.

Stripping notebook outputs on commit

Because executed outputs are produced in CI, commit notebooks with their outputs stripped. A pre-commit hook such as nbstripout is the usual way.

How It Works

  1. Sets up Python environment using the selected package manager action
  2. (If notebooks-path is set) Restores .jupyter_cache/ from actions/cache, preflight-checks that jupyter/nbconvert/ipykernel are importable, and runs the vendored prep_notebooks.py script — which copies cached executed notebooks back into the working tree where available, executes (via jupyter nbconvert --execute --inplace) the rest, and skips any notebook matching notebooks-exclude
  3. Configures git with the workflow actor's identity
  4. Runs the appropriate mike command based on trigger:
    • pull_request: mike deploy --push develop
    • main: mike deploy --push main && mike set-default --push main
    • release: mike deploy --push --update-aliases <tag> latest
  5. For non-pip managers, commands are prefixed with <manager> run

Every mike ... --push runs through a fetch-reset-retry wrapper (scripts/mike_push.sh): before each attempt it fetches the current gh-pages tip and repoints the local branch ref to it, so a deploy that loses a push race (a concurrent deploy, or an out-of-band gh-pages push such as a history squash) refetches and retries instead of failing with ! [rejected] gh-pages -> gh-pages (fetch first). mike merges versions.json from the tip it builds on, so retries are idempotent and concurrently-added versions are preserved. A single, uncontended deploy still publishes exactly one new gh-pages commit.

A per-ref concurrency: group in your caller workflow is still worth adding as complementary serialization — it prevents this workflow's own runs from racing, while the retry above also covers out-of-band pushes it can't see.

Prerequisites

  • mkdocs and mike in your project dependencies
  • GitHub Pages enabled on the repository
  • contents: write permission in the workflow
  • (For notebook execution) jupyter, nbconvert, ipykernel, mkdocs-jupyter in the dependency group passed via install-groups; src-layout project structure