Pre-Commit Quality Gates
How to set up, configure, and extend the RouteRTL pre-commit verification pipeline.
Overview
RouteRTL uses a project.yml-driven pre-commit hook — not the generic
.pre-commit-config.yaml framework. This is intentional: the SDK's hook
reads your project-specific configuration and adapts its checks accordingly.
When you commit, the hook executes up to 8 stages:
graph TD C["git commit"] --> MD{"Only .md files?"} MD -->|"yes"| F_DOC["Frozen Directory Guard"] F_DOC --> L_DOC["Markdown Link Validation"] L_DOC --> D_DOC["MkDocs Build (strict)"] D_DOC --> PASS["✅ Commit Accepted"] MD -->|"no"| F["Frozen Directory Guard"] F --> L["Markdown Link Validation"] L --> D["MkDocs Build (strict)"] D --> Y["YAML Check"] Y --> P["Python Linting (ruff)"] P --> H["HDL Linting (GHDL --std=08)"] H --> S["Cocotb Regression Suite"] S --> B["Bridge-gen Regression"] B --> PASS style C fill:#1a1a2e,stroke:#4a4aaa,color:#fff style MD fill:#0d4a4a,stroke:#1a9a8a,color:#fff style F_DOC fill:#2d5016,stroke:#4a8c2a,color:#fff style L_DOC fill:#2d5016,stroke:#4a8c2a,color:#fff style D_DOC fill:#1a3a5c,stroke:#2d6da3,color:#fff style PASS fill:#1a5c3a,stroke:#2da36b,color:#fff style F fill:#2d5016,stroke:#4a8c2a,color:#fff style L fill:#2d5016,stroke:#4a8c2a,color:#fff style D fill:#1a3a5c,stroke:#2d6da3,color:#fff style Y fill:#2d5016,stroke:#4a8c2a,color:#fff style P fill:#4a3a1a,stroke:#a38c2d,color:#fff style H fill:#5c1a3a,stroke:#a32d6d,color:#fff style S fill:#5c1a3a,stroke:#a32d6d,color:#fff style B fill:#3a1a5c,stroke:#6d2da3,color:#fff
Each stage is controlled by flags in your project.yml. Stages with their
flag set to false are skipped silently.
Markdown-only commits are automatically detected. When every staged file is a
.mdfile, the hook skips heavyweight checks (ruff, HDL linting, regression, simulation) and only runs documentation checks (link validation, MkDocs build). See Markdown-Only Fast Path below.
Installation
The SDK ships the hook at tools/githooks/project-pre-commit. Install it
using the Makefile target:
make install-hooks
This copies the hook script to .git/hooks/pre-commit. The hook is a
standalone shell script — it doesn't require any external frameworks.
Configuration via project.yml
All hook behavior is controlled by the hooks section in your project.yml:
hooks:
pre_commit:
enabled: true # Master switch — set false to disable entirely
# ── Stage 1: Frozen directories ────────────────────────
frozen_dirs:
- vendor/routertl # Prevents accidental SDK modifications
# ── Stage 2: Documentation ─────────────────────────────
check_links: true # Validate markdown links in docs/
exclude_doc_dirs: # Skip these directories during link check
- docs/internal
# ── Stage 2.5: MkDocs Build ────────────────────────────
check_docs: true # Run mkdocs build --strict (requires mkdocs.yml)
# ── Stage 3: YAML ──────────────────────────────────────
check_yaml: true # Validate YAML syntax
# ── Stage 3.5: Python Linting ──────────────────────────
lint_python: true # Run ruff check (requires pyproject.toml)
# ── Stage 4: HDL Linting ───────────────────────────────
# Auto-discovers custom VHDL libraries, pre-compiles them,
# then lints each hierarchy. See LINTER_PIPELINE.md for details.
lint: true
exclude_lint: # Glob patterns to exclude from linting
- "*/legacy/*"
- "*/generated/*"
# ── Stage 5: Simulation Tests ──────────────────────────
# Option A: Auto-discover all test_*.py files (recommended)
tests: auto
# Option B: Explicit list of test modules
# tests:
# - tests.units.test_edge_counter
# - tests.units.test_edge_detector
# Tests to skip (used with both auto and explicit modes)
exclude:
tests:
- tests.test_slow_integration
# ── Stage 6: Bridge-gen (SDK only) ─────────────────────
bridge_gen_regression: false # Enable in SDK repo only
Enabling/Disabling Stages
| Stage | project.yml key | Default |
|---|---|---|
| Frozen dirs | hooks.pre_commit.frozen_dirs | [] (disabled) |
| Link validation | hooks.pre_commit.check_links | true |
| MkDocs build | hooks.pre_commit.check_docs | false |
| YAML check | hooks.pre_commit.check_yaml | true |
| Python linting | hooks.pre_commit.lint_python | false |
| HDL linting | hooks.pre_commit.lint | true |
| Simulation | hooks.pre_commit.tests | [] (no tests) or auto |
| Bridge-gen | hooks.pre_commit.bridge_gen_regression | false |
Adding Your Own Tests
To add a new cocotb test to the pre-commit suite:
-
Create the test file in your
simulation.test_dirfollowing thetest_*.pynaming convention (see Cocotb Quickstart). -
If using
tests: auto(recommended): You're done! The file is automatically discovered on the next commit. -
If using an explicit list: Register it in
project.yml:hooks: pre_commit: tests: - tests.ip.test_my_module -
Verify it runs standalone first:
cd sim/cocotb && SIM=nvc python tests/ip/test_my_module.py
With
tests: auto, useexclude.teststo opt out specific tests instead of manually maintaining a growing inclusion list.Git-tracked files only: Auto-discovery filters results through
git ls-files, so untracked WIP files (test_*.pythat haven't been staged or committed) are automatically excluded. This prevents parallel experiments or agent-generated stubs from breaking the pre-commit gate. To include a new test,git addit first.
Extending with Local Hooks
For project-specific checks that don't belong in project.yml, create a
hooks/pre-commit.local file at your project root:
#!/bin/bash
# hooks/pre-commit.local — sourced by the SDK pre-commit hook
echo "🔍 Running project-specific checks..."
# Example: check for debug prints left in RTL
if grep -rn "-- DEBUG" src/ --include="*.vhd"; then
echo "❌ Debug comments found in RTL. Remove before committing."
exit 1
fi
echo "✅ Project-specific checks passed."
The SDK hook sources this file automatically if it exists (stage 0).
Skipping the Hook
For emergency commits (e.g., during a production hotfix):
git commit --no-verify -m "hotfix: critical patch"
This bypasses ALL pre-commit checks. CI will still run the full suite on push. Use sparingly.
Markdown-Only Fast Path
When every staged file has a .md extension, the hook automatically skips
code-oriented checks to save time on documentation-only commits.
Detection logic (runs before any other stage):
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
NON_MD_FILES=$(echo "$STAGED_FILES" | grep -v '\.md$' || true)
If NON_MD_FILES is empty, the MD_ONLY flag is set.
What runs and what doesn't
| Stage | Markdown-only | Normal commit |
|---|---|---|
| Frozen directory guard | ✅ | ✅ |
| Markdown link validation | ✅ | ✅ |
MkDocs build (--strict) | ✅ | ✅ |
| YAML check | ✅ | ✅ |
| Python linting (ruff) | ⏭️ skipped | ✅ |
| HDL linting (GHDL) | ⏭️ skipped | ✅ |
| Full regression | ⏭️ skipped | ✅ |
| Cocotb simulation | ⏭️ skipped | ✅ |
MkDocs and link validation always run on markdown-only commits. These are the precise checks that catch broken docs — skipping them would defeat the purpose.
SDK hook vs consumer hook
Both hooks run docs checks (link validation + MkDocs --strict build)
on markdown-only commits. The difference is in how the code stages
are controlled:
- SDK hook (
pre-commit): runs docs checks inline, thenexit 0before the code stages. - Consumer hook (
project-pre-commit): sets theMD_ONLYflag; each code stage checks the flag and skips individually. Docs stages are driven byproject.ymlflags as usual.
How It Works Internally
The hook script (tools/githooks/project-pre-commit) follows this flow:
- Sources
hooks/pre-commit.localif present - Detects markdown-only commits — sets
MD_ONLY=trueif all staged files are.md - Locates the SDK tools (checks
vendor/routertl/tools,$ROUTERTL_ROOT, ortools/fallback) - Calls
project_manager.py project.yml --gen-hook-configto extract the hook configuration as shell variables - Executes each enabled stage sequentially (skipping code/sim stages
when
MD_ONLY=true) - Exits with non-zero on the first failure (abort commit)
The configuration extraction uses project_manager.py, which parses
project.yml and emits shell-compatible variable assignments. This means
the hook script itself contains zero YAML parsing logic.
CI Mirroring
The CI pipeline should run the same checks as the pre-commit hook, plus heavier tasks. The recommended approach:
# In your CI config
- name: Run pre-commit checks
run: make precommit
The make precommit target mirrors the hook's behavior, ensuring
CI and local development stay in sync.
Further Reading
- Cocotb Quickstart — Writing simulation tests
- Simulator Backends — NVC vs GHDL vs Verilator vs Icarus
- Troubleshooting — Common hook failures