Skip to content

Pre-push mkdocs strict-mode gate

A pre-push hook runs mkdocs build --strict locally whenever a push touches docs/ or mkdocs.yml, so documentation breakage fails on your machine instead of in CI.

CI runs the same build (the docs.yml lane) on every such push. Before this hook landed, audit slice G found that 38 of 50 master pushes failed that lane for trivial breakage: broken anchors, orphan pages, missing nav entries.

See ADR-0466 for the decision record.

Quick start

# Install all pre-push hooks (idempotent):
make hooks-install

# Run the mkdocs check manually (without pushing):
scripts/git-hooks/pre-push-mkdocs-strict.sh  # runs directly

# Install the docs toolchain if mkdocs is not on PATH:
pip install -r docs/requirements.txt

How the hook works

  1. Scope check — if the push does not touch docs/ or mkdocs.yml, the hook exits 0 immediately (no latency on non-docs pushes). If the framework selects no documentation paths, the check does not run. A direct script invocation also skips a confirmed non-doc change before checking for MkDocs. It consumes the complete changed-path list, including large diffs, and runs the build if Git cannot determine that list. Documentation changes must pass the build.
  2. Toolchain check — once documentation is selected, missing mkdocs blocks the push with an installation hint. Install via pip install -r docs/requirements.txt in the active environment.
  3. Build — runs mkdocs build --strict against a temporary --site-dir. The temporary directory is cleaned up on exit regardless of outcome. The build is never --quiet: that flag raises the log level to ERROR, so the WARNINGs --strict counts are never emitted and a site with broken anchors builds with exit code 0. The hook also blocks when the output holds a WARNING or ERROR line, and scripts/ci/tests/test_pre_push_mkdocs_strict.py (run by the docs.yml workflow) fails on the quiet form.
  4. Result — on success, a single confirmation line is printed. On failure, the mkdocs WARNING and ERROR lines are printed to stderr and the push is blocked with exit code 1.

Bypassing the hook

Situation Command
Skip this hook only SKIP=mkdocs-strict git push
Skip all pre-push hooks git push --no-verify

The targeted bypass (SKIP=mkdocs-strict) is preferred: it still runs the PR-body deliverables gate and other pre-push hooks.

Warning

Bypassing is for the maintainer only. Agents never bypass hooks (operational rule 6 of the agent harness forbids the skip-hooks push flag and LEFTHOOK=0).

Troubleshooting

"mkdocs not found on PATH"

Install the docs toolchain:

pip install -r docs/requirements.txt

The exact anchor, page, or nav entry that broke is printed. Fix the broken reference in the doc file and push again. Common causes:

  • Renamed a heading → update all links that reference its anchor.
  • Added a new page but forgot to add it to mkdocs.yml nav:.
  • Moved a file → update any cross-links that point at the old path.

"mkdocs build --strict" passes locally but fails in CI

The CI lane uses docs/requirements.txt pinned versions. Ensure your local installation matches:

pip install -r docs/requirements.txt

If CI still fails after a local pass, check the CI log for the exact warning — the validation: block in mkdocs.yml (see ADR-0403) governs which categories are warn vs. info.

Wire-up details

Three parts make up the wiring:

  • Hook stage. make install-hooks installs a regular dispatcher for the framework's pre-push stage. .pre-commit-config.yaml registers mkdocs-strict as an independent check, so a skipped PR-body check cannot skip this build.
  • Selection. The check is selected by changed docs/ or mkdocs.yml paths in Git's pushed-ref range. A direct invocation of pre-push-mkdocs-strict.sh selects documentation changes relative to origin/master, and runs conservatively when that base is unavailable. The build validates the active working tree, as other local source checks do; CI validates the submitted commit in a clean checkout.
  • Migration. Installations predating ADR-1241 must rerun make install-hooks, particularly if an old source symlink points into a removed worktree. See local hooks for migration and custom-hook preservation.