pre-commit framework: первая линия защиты
CI/CD в dbt-проекте — это два слоя. Локальный слой — pre-commit hooks, которые блокируют коммит до того, как код уехал в GitHub. Удалённый слой — GitHub Actions, который запускает полноценный dbt build на PR. Хорошее правило: всё, что можно проверить локально за секунду, не должно доходить до удалённого CI.
Локальная проверка — это pre-commit framework, инструмент, который запускает любые программы (linters, formatters, validators) на staged файлах перед созданием коммита. В этом уроке вы соберёте .pre-commit-config.yaml для dbt-проекта, поймёте механику git hooks, и научитесь обходить блокировки в редких аварийных случаях.
Зачем pre-commit в dbt-проекте
Без pre-commit типичный цикл выглядит так:
С pre-commit:
Принцип shift left: проверки максимально близко к моменту написания кода. Чем раньше — тем дешевле починка.
Что такое pre-commit framework
pre-commit — это Python пакет (pip install pre-commit), который управляет git hooks. У git есть встроенный механизм hooks (.git/hooks/pre-commit), но он:
- Не версионируется (лежит вне репо).
- У каждого разработчика свой.
- Сложно поделиться конфигурацией.
pre-commit framework решает эти проблемы: конфигурация лежит в .pre-commit-config.yaml в корне репо, hooks автоматически устанавливаются командой pre-commit install, и все участники команды получают одинаковую проверку.
Не путайте: pre-commit (framework, Python пакет) — это менеджер хуков. pre-commit (git hook) — это файл .git/hooks/pre-commit, который запускается при git commit. Framework записывает свой shell-скрипт в этот файл при pre-commit install.
Минимальный .pre-commit-config.yaml для dbt
Создайте файл в корне репо:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: check-merge-conflict
- id: check-added-large-files
args: ["--maxkb=1000"]
- repo: https://github.com/sqlfluff/sqlfluff
rev: 3.4.0
hooks:
- id: sqlfluff-lint
args: ["--dialect=duckdb", "--templater=dbt"]
files: \.(sql|sql.jinja)$
- id: sqlfluff-fix
args: ["--dialect=duckdb", "--templater=dbt"]
files: \.(sql|sql.jinja)$
- repo: https://github.com/dbt-checkpoint/dbt-checkpoint
rev: v2.0.6
hooks:
- id: check-model-has-tests
- id: check-model-has-description
- id: check-source-has-freshness
Разберём по блокам:
| Блок | Что делает |
|---|---|
pre-commit-hooks | Общие проверки: висячие пробелы, отсутствующая newline в конце файла, валидный YAML, маркеры merge conflict, защита от случайного коммита больших файлов. |
sqlfluff | sqlfluff-lint — линтер: проверяет, но не правит. sqlfluff-fix — автоправит то, что может (capitalize, indent, trailing commas). |
dbt-checkpoint | dbt-specific: для каждой новой модели должны быть тесты и description. Для источника — freshness. |
Установка и первый запуск
# 1. Установить framework
pip install pre-commit
# 2. Установить hooks в .git/hooks/
pre-commit install
# 3. Запустить вручную (один раз — на всём репо)
pre-commit run --all-files
# 4. Дальше — автоматически при каждом git commit
Что происходит при pre-commit install:
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
В .git/hooks/pre-commit появляется shell-скрипт, который при каждом коммите вызывает pre-commit run. Этот скрипт смотрит на .pre-commit-config.yaml и запускает все hooks.
Что происходит при git commit:
$ git commit -m "add stg_customers model"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
sqlfluff-lint............................................................Failed
- hook id: sqlfluff-lint
- exit code: 1
== [models/staging/stg_customers.sql] FAIL
L: 3 | P: 1 | CV02 | Use 'COALESCE' instead of 'IFNULL'.
L: 5 | P: 10 | LT02 | Expected indent of 4 spaces.
[INFO] Some hook failed.
Коммит не создаётся. Нужно исправить (или прогнать sqlfluff-fix, который автоправит часть проблем), и попробовать снова.
Где взять rev для каждого репо
rev — это git tag или SHA коммита, который pre-commit framework склонит. Он pinит версию hook’а, чтобы у всей команды (и в CI) была одинаковая.
Узнать актуальный rev:
# Auto-update до последней stable версии
pre-commit autoupdate
# Принт diff: какие revs обновятся
pre-commit autoupdate --dry-run
pre-commit autoupdate сходит в каждый указанный repo:, найдёт последний git tag, и обновит rev:. Это безопасно делать раз в месяц или квартал — обычно автоматизируется через Dependabot / Renovate.
Не оставляйте rev: master или rev: main. Это означает «использовать актуальный код из ветки», который завтра может сломать ваш pipeline. Всегда pin’ить теги.
Скоуп: на каких файлах запускать каждый hook
По умолчанию hook запускается на всех staged файлах. Большинство hooks из framework’ов уже имеют сенсибельные defaults — sqlfluff-lint запускается только на .sql, check-yaml — только на .yml/.yaml.
Если нужно ограничить вручную:
- id: sqlfluff-lint
files: ^models/.*\.sql$ # только в models/, исключает analyses/, macros/
exclude: ^models/legacy/ # исключить legacy подпапку
files и exclude — это regex. files оставляет только matched, exclude отсеивает. Если оба заданы, сначала filter по files, потом убираются exclude.
Skip: что делать, если очень нужно закоммитить
В очень редких случаях hook ложно срабатывает, или ты делаешь WIP-commit для собственного backup. Можно skip:
# Способ 1: пропустить ВСЕ hooks
git commit --no-verify -m "WIP: experimenting with new structure"
# Способ 2: пропустить КОНКРЕТНЫЕ hooks
SKIP=sqlfluff-lint,sqlfluff-fix git commit -m "skip lint for this commit"
--no-verify — это аварийный outlet. Если используете часто, значит pre-commit конфиг неверный (слишком строгий или ложноположительный). Не закладывайтесь на скрытое использование --no-verify — настройте hooks так, чтобы они отражали реальные production-стандарты команды.
В CI можно явно проверить, что коммиты не пушились с --no-verify, прогнав pre-commit run --all-files ещё раз на удалённой стороне — об этом следующий модуль.
fail_fast и stages
Иногда нужно остановить серию hooks на первой ошибке:
fail_fast: true
repos:
- repo: ...
Без fail_fast (по умолчанию) prerun запускает все hooks, чтобы дать максимум фидбэка. С fail_fast: true — остановка на первой ошибке, экономит секунды, но ты увидишь только одну проблему.
Stages — это когда запускать. По умолчанию — commit (при git commit). Можно повесить на push:
- id: sqlfluff-lint
stages: [push] # запустится только при git push, не при commit
Это для тяжёлых hooks, которые слишком медленные для каждого коммита, но всё ещё хочется проверить до отправки в удалённый репо.
Скрипт setup.sh для команды
В корне репо положите скрипт, чтобы новый разработчик в команде поднял dev-окружение за секунды:
#!/bin/bash
# scripts/setup-dev.sh
set -e
echo "1. Installing Python deps..."
pip install -r requirements-dev.txt # pre-commit, sqlfluff, dbt-checkpoint
echo "2. Installing dbt packages..."
dbt deps
echo "3. Installing pre-commit hooks..."
pre-commit install
echo "4. Running pre-commit на всех файлах (warm cache)..."
pre-commit run --all-files || echo "Some hooks failed — это нормально для первого запуска, исправьте и commit"
echo "Setup complete. Запускайте 'dbt run' для проверки."
В README репо: «Новый разработчик? Выполни ./scripts/setup-dev.sh».
Попробуй сам
В вашем dbt-проекте:
- Установите pre-commit:
pip install pre-commit
- Создайте минимальный
.pre-commit-config.yamlв корне:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- Установите hooks:
pre-commit install
- Создайте файл с висячими пробелами и без новой строки в конце:
printf "SELECT 1 \nSELECT 2" > test_file.sql
git add test_file.sql
git commit -m "test"
-
Увидите ошибки. Pre-commit автофиксит висячие пробелы и добавит newline. Файл изменился — нужно повторно
git addиgit commit. -
Бонус: добавьте sqlfluff в конфиг и попробуйте закоммитить SQL с неправильным indent. Заметьте разницу между
sqlfluff-lint(только ругается) иsqlfluff-fix(правит).
Ключевые выводы
- pre-commit framework — локальный слой CI. Проверки на staged файлах перед созданием коммита. Shift left: дешевле починить за секунды локально, чем за 10 минут в GitHub Actions.
- Конфигурация —
.pre-commit-config.yamlв корне репо. Версионируется, шарится между всеми разработчиками. - Установка:
pip install pre-commit && pre-commit install. Хуки записываются в.git/hooks/pre-commit. - Стандартные hooks для dbt: общие (trailing-whitespace, check-yaml),
sqlfluff(lint + fix),dbt-checkpoint(dbt-specific проверки тестов и descriptions). rev:всегда пинить к тегу.pre-commit autoupdateдля регулярных апдейтов.- Аварийный skip:
git commit --no-verifyилиSKIP=hook_id git commit. Не злоупотреблять — это сигнал к перенастройке. stages: [push]— для тяжёлых hooks, которые слишком медленные на каждый commit.