Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 13.01 · 25 мин
Средний
pre-commitCI/CDLocal quality gatesGit hooks

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, и научитесь обходить блокировки в редких аварийных случаях.

Airflow: CI pipeline для DAG-кода — сравнение подходов

Зачем pre-commit в dbt-проекте

Без pre-commit типичный цикл выглядит так:

Без pre-commit: цикл боли
Локально пишет кодРазработчик пишет SQL. Локально dbt run проходит (потому что SQL компилируется и выполняется). Но он не запускал линтер, не проверял что есть тест на новую модель, не форматировал.
commit + pushДелает git commit и git push. Никаких проверок не было.
CI красныйGitHub Actions запускает CI. Тратит 5-10 минут на сборку проекта, прогон тестов. Падает на линте: missing comma, wrong indentation, или на dbt-checkpoint: 'модель X без тестов'.
Ещё 3 кругаРазработчик чинит, делает новый commit + push. Снова ждёт 5-10 минут. Иногда CI красный из-за другой причины. И снова. И снова.

С pre-commit:

С pre-commit: быстрый feedback
Локально пишет кодРазработчик пишет SQL.
pre-commit ловит проблемуgit commit запускает pre-commit hooks. За 2-5 секунд проверяется: sqlfluff, sqlfmt, проверка YAML, проверка наличия тестов на новые модели. Если что-то не так — commit отклоняется с понятной ошибкой.
Исправляет за 30 секРазработчик исправляет проблему на месте. Многие hooks умеют автофиксить (sqlfmt, sqlfluff fix), просто переcommitит.
CI зелёныйКогда все pre-commit hooks зелёные, делается push. CI с высокой вероятностью пройдёт с первого раза.

Принцип shift left: проверки максимально близко к моменту написания кода. Чем раньше — тем дешевле починка.


Что такое pre-commit framework

pre-commit — это Python пакет (pip install pre-commit), который управляет git hooks. У git есть встроенный механизм hooks (.git/hooks/pre-commit), но он:

  1. Не версионируется (лежит вне репо).
  2. У каждого разработчика свой.
  3. Сложно поделиться конфигурацией.

pre-commit framework решает эти проблемы: конфигурация лежит в .pre-commit-config.yaml в корне репо, hooks автоматически устанавливаются командой pre-commit install, и все участники команды получают одинаковую проверку.

NOTE

Не путайте: 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, защита от случайного коммита больших файлов.
sqlfluffsqlfluff-lint — линтер: проверяет, но не правит. sqlfluff-fix — автоправит то, что может (capitalize, indent, trailing commas).
dbt-checkpointdbt-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.

WARNING

Не оставляйте 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"
DANGER

--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-проекте:

  1. Установите pre-commit:
pip install pre-commit
  1. Создайте минимальный .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
  1. Установите hooks:
pre-commit install
  1. Создайте файл с висячими пробелами и без новой строки в конце:
printf "SELECT 1   \nSELECT 2" > test_file.sql
git add test_file.sql
git commit -m "test"
  1. Увидите ошибки. Pre-commit автофиксит висячие пробелы и добавит newline. Файл изменился — нужно повторно git add и git commit.

  2. Бонус: добавьте sqlfluff в конфиг и попробуйте закоммитить SQL с неправильным indent. Заметьте разницу между sqlfluff-lint (только ругается) и sqlfluff-fix (правит).


Ключевые выводы

  1. pre-commit framework — локальный слой CI. Проверки на staged файлах перед созданием коммита. Shift left: дешевле починить за секунды локально, чем за 10 минут в GitHub Actions.
  2. Конфигурация — .pre-commit-config.yaml в корне репо. Версионируется, шарится между всеми разработчиками.
  3. Установка: pip install pre-commit && pre-commit install. Хуки записываются в .git/hooks/pre-commit.
  4. Стандартные hooks для dbt: общие (trailing-whitespace, check-yaml), sqlfluff (lint + fix), dbt-checkpoint (dbt-specific проверки тестов и descriptions).
  5. rev: всегда пинить к тегу. pre-commit autoupdate для регулярных апдейтов.
  6. Аварийный skip: git commit --no-verify или SKIP=hook_id git commit. Не злоупотреблять — это сигнал к перенастройке.
  7. stages: [push] — для тяжёлых hooks, которые слишком медленные на каждый commit.
Проверка знанийKnowledge check
В команде из 5 человек настроили .pre-commit-config.yaml, но через неделю обнаружили, что у одного разработчика hooks молча игнорируются. Какая самая вероятная причина и как это исправить системно?
ОтветAnswer
Самая вероятная причина: разработчик **не выполнил `pre-commit install`** на своей машине. `.pre-commit-config.yaml` сам по себе ничего не делает — это конфиг. Реальная установка hooks происходит при `pre-commit install`, который записывает shell-скрипт в `.git/hooks/pre-commit`. Этот файл лежит вне репо (в `.git/` который в gitignore по дизайну), поэтому при `git clone` он не появляется автоматически.\n\n**Системные решения:**\n\n1. **README + setup скрипт.** `./scripts/setup-dev.sh` который делает `pre-commit install`. Обязательный шаг для нового разработчика.\n\n2. **`pre-commit install --install-hooks`** при `dbt deps` через hook в dbt_project.yml on-run-start (хак, не очень чисто).\n\n3. **CI-проверка.** В GitHub Actions запускать `pre-commit run --all-files` — если разработчик пушнул с `--no-verify` или без `pre-commit install`, CI словит ошибки. Это сетка безопасности.\n\n4. **Husky-стиль автоустановка.** В `package.json` (если есть) или через post-checkout git hook (`.git/hooks/post-checkout`) запускать `pre-commit install` автоматически. Хрупко, обычно не делают.\n\n**Правильный паттерн** = 1 + 3. README документирует ручную установку, CI ловит тех, кто пропустил.
Проверка знанийKnowledge check
Hook \`sqlfluff-lint\` падает на коммите с ошибкой 'CV02: Use COALESCE instead of IFNULL'. Разработчик считает это false positive, потому что IFNULL более читаемый. Как правильно отреагировать на это в команде?
ОтветAnswer
Это **не баг pre-commit**, а несоответствие правил линтера команде. Нельзя просто `--no-verify` — это создаст inconsistency: один человек пишет IFNULL, другой COALESCE, в codebase бардак.\n\n**Шаги:**\n\n1. **Обсудить с командой.** Это вопрос style guide, не технический. Что мы пишем — IFNULL или COALESCE? Аргументы за COALESCE: ANSI SQL, работает на всех warehouses, поддерживает >2 аргументов. Аргументы за IFNULL: короче, читаемее на 2 аргументах.\n\n2. **Если решили использовать COALESCE** — оставить правило CV02, разработчик чинит свой код.\n\n3. **Если решили разрешить IFNULL** — отключить правило в `.sqlfluff`:\n\n```ini\n[sqlfluff]\nexclude_rules = CV02\n```\n\nИли через inline директиву в конкретном файле:\n\n```sql\n-- noqa: CV02\nSELECT IFNULL(x, 0) FROM t\n```\n\n4. **Документировать решение** в README или style guide: «Мы разрешаем IFNULL, потому что [причина]».\n\n**Анти-паттерн** = делать `git commit --no-verify` и не обсуждать. Pre-commit становится бесполезным, потому что каждый игнорирует то, что не нравится. Linter живёт по принципу «consensus, not individual taste».

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4