Git-хуки, которые отсекают плохой код ещё до коммита
----------------------------------------------------
Здравствуйте, коллеги-разработчики.
Большая часть падений пайплайна в CI — это вовсе не сложные баги, а банальные промахи: оставленный `console.log`, неформатированный файл, сработавший линтер, битый импорт, модуль без тестов. Такие мелочи не должны даже доходить до CI и, тем более, до код-ревью. Их удобно отлавливать ещё в момент `git commit`.
Именно для этого существуют Git-хуки: они позволяют повесить на определённые события (pre-commit, pre-push и т.д.) запуск скриптов-проверок. Если проверка не проходит, хук возвращает ненулевой код выхода — и Git просто не даёт сделать коммит или пуш.
В этой статье — рабочий набор таких хуков для JavaScript/TypeScript и Python: форматирование, линтинг, статический анализ типов, тесты, проверка наличия тестов, а также Docker-варианты этих скриптов.
---
Общее устройство скриптов и хуков
Каждая проверка реализована в виде обычного bash-скрипта (`.sh`), и почти для каждого сценария есть две модификации:
1. Скрипт, принимающий список файлов
Например:
```bash
bash python/check_flake8.sh $FILES
```
Такой формат удобен для `pre-commit`, где можно передать только изменённые файлы из индекса.
2. Скрипт, проверяющий весь проект
Например:
```bash
bash python/check_flake8_all.sh
```
Эту версию логично запускать в `pre-push` или в CI, когда нужно убедиться, что весь репозиторий в порядке.
Логика завершения у всех скриптов одинаковая:
- код выхода `0` — проверка прошла, можно продолжать (коммит или пуш выполняется);
- код выхода `1` — найдены ошибки, операция блокируется.
Пример самого простого `pre-commit`-хука может выглядеть так:
```bash
#!/usr/bin/env bash
FILES=$(git diff --cached --name-only --diff-filter=ACM | tr 'n' ' ')
bash javascript/check_prettier.sh $FILES || exit 1
bash javascript/check_eslint.sh $FILES || exit 1
bash python/check_flake8.sh $FILES || exit 1
bash python/check_mypy.sh $FILES || exit 1
```
Такой хук последовательно запускает сразу несколько проверок для изменённых файлов и блокирует коммит при любой найденной проблеме.
---
Базовая защита: единый стиль и очевидные ошибки
Первый уровень защиты кода — принудительно привести его к общему стилю и отлавливать самые простые, но раздражающие косяки ещё до попадания в репозиторий. Это:
- снижает шум на код-ревью;
- уменьшает количество бессмысленных правок «поменяй кавычки/отступ»;
- ускоряет работу команды — спорить можно о логике, а не о стиле.
Для JavaScript/TypeScript эту роль выполняют связка ESLint + Prettier + TSC.
---
JavaScript/TypeScript: форматирование и линтинг
ESLint: ловим «пахнущий» код
Скрипт `check_eslint_all.sh` — это обёртка над ESLint, которая:
- запускает `npx eslint --fix` по директориям, перечисленным в переменной `LINT_DIRS`;
- автоматически исправляет все проблемы, которые ESLint умеет чинить сам;
- если остаются невыправимые ошибки (например, неиспользуемые переменные, неверные импорты, нарушения правил), завершает работу с `exit 1`.
Таким образом, линтер служит фильтром:
- всё, что можно поправить без участия человека, приводится к норме автоматически;
- всё, что требует внимания разработчика, останавливает коммит.
Обычно используют две версии:
- `check_eslint.sh` — проверка только конкретных файлов (для `pre-commit`);
- `check_eslint_all.sh` — полная проверка проекта (для `pre-push` и CI).
Такой подход не тормозит локальную разработку и при этом гарантирует, что весь проект будет в консистентном состоянии перед пушем.
---
Prettier: единый стиль форматирования «по умолчанию»
За форматирование отвечает Prettier. Для него, как и для ESLint, есть две вариации:
- `check_prettier_all.sh` — прогоняет `prettier --write` по всему проекту;
- `check_prettier.sh` — форматирует только переданные файлы.
В `pre-commit` обычно достаточно второй версии: форматируются только изменённые файлы — это:
- ускоряет проверку;
- не засоряет историю массовыми правками форматирования без логических изменений.
Суть проста: скрипт вызывает `prettier --write` для нужных путей, и разработчику не нужно думать о пробелах, переносах строк, кавычках и прочих деталях стиля. Код автоматически «причесывается» по единым правилам.
---
TSC: быстрая проверка типизации без сборки
Одного линтера для TypeScript мало: он может не заметить поломку типов в другом модуле, особенно в большом монорепозитории. Для этого используется отдельная команда проверки типов — TSC без сборки, которую запускает скрипт `check_tsc_all.sh`.
Особенности:
- запускается `tsc` в режиме проверки типов (без эмита сборки);
- используется отдельный конфиг `tsconfig.check.json`, который создаётся в корне проекта;
- можно тонко настроить, какие директории и файлы проверять, какие исключить.
Примерный сценарий:
1. В корне создаётся `tsconfig.check.json`, ориентированный именно на проверку типов (иногда он отличается от боевого `tsconfig.json`).
2. `check_tsc_all.sh` запускается в `pre-push` или в CI.
3. Если типизация сломана где-то «в стороне» от изменённых файлов, скрипт всё равно это поймает и заблокирует пуш.
Это особенно полезно в больших проектах, где не всегда очевидно, какие модули завязаны друг на друга.
---
Python: линтинг и статический анализ
В мире Python Git-хуки решают те же задачи, что и в JS/TS, но с поправкой на динамическую типизацию и разнородные стили кодирования. Хорошая практика — разделить проверки на два ключевых вида:
1. Линтинг и стиль (Flake8).
2. Статический анализ типов (Mypy).
Оба инструмента можно запускать как по отдельным файлам, так и по всему проекту.
---
Flake8: PEP8 и единый код-стайл
Скрипт `check_flake8.sh` проверяет `.py`-файлы на соответствие PEP8 и заданному код-стайлу. Flake8 помогает поймать:
- лишние пробелы и неверные отступы;
- слишком длинные строки;
- несоглашённые имена функций и переменных;
- простые логические ошибки, которые видны на уровне синтаксиса.
Преимущества:
- проблемы выявляются до запуска тестов и деплоя;
- код выглядит одинаково во всех модулях;
- снижается порог входа для новых участников команды — читать проект проще.
Ошибки выводятся прямо в терминал в привычном формате: путь к файлу, номер строки, тип и описание нарушения. Разработчик сразу понимает, что нужно исправить до коммита.
---
Mypy: статический анализ типов в динамическом мире
Скрипт `check_mypy.sh` отвечает за статический анализ типов с помощью Mypy. Обычно он настроен так, чтобы:
- игнорировать тесты и вспомогательный код;
- концентрироваться на продакшн-части проекта;
- использовать строгий режим проверки там, где это оправдано.
Mypy позволяет:
- находить несоответствия типов аргументов и возвращаемых значений;
- ловить неверно указанные типы в аннотациях;
- обнаруживать потенциальные баги, которые в динамическом Python проявятся только в рантайме.
---
Почему связка Flake8 + Mypy эффективнее по отдельности
Отдельно по себе каждый инструмент полезен, но по-настоящему сильный фильтр качества получается в связке:
- Flake8:
- приводит код к единому виду;
- отлавливает базовые ошибки и «грязный» стиль;
- экономит время на код-ревью — не нужно обсуждать форматирование.
- Mypy:
- добавляет в Python немного «статической строгости»;
- ловит ошибки типов ещё до запуска приложения;
- дисциплинирует разработчиков писать аннотации и думать о контракте функций.
Вместе они создают первый серьёзный барьер: код не попадёт в репозиторий, пока:
- не соответствует принятым правилам стиля;
- не проходит проверку типов.
Разработчик сразу получает обратную связь локально, а команда — более стабильную и предсказуемую кодовую базу, на которую уже можно надёжно накручивать тесты и деплой.
---
Следующий уровень защиты: тесты и покрытие
Один только линтинг не гарантирует, что код работает корректно. Следующий логичный шаг — интегрировать в Git-хуки запуск тестов и проверку покрытия.
Здесь можно использовать два подхода:
1. Быстрые проверки в pre-commit
Лёгкие тесты или smoke-тесты, которые выполняются за секунды. Идея — быстро поймать очевидную поломку.
2. Полноценный прогон тестов в pre-push или CI
Длительные интеграционные и e2e-тесты, замер покрытия, проверка обязательного наличия тестов для новых модулей.
---
JavaScript: проверки наличия тестов и быстрый прогон
Для JS/TS удобно настроить скрипты примерно так:
- в `pre-commit`:
- опциональный быстрый прогон юнит-тестов только по изменённым модулям (если это поддерживает выбранный тестовый раннер);
- проверка, что для новых файлов в `src/` есть соответствующие файлы в `__tests__/` или `*.test.ts/.js`.
- в `pre-push`:
- полный прогон тестов (например, через Jest, Vitest, Mocha и т.п.);
- проверка минимального уровня покрытия (coverage threshold).
Скрипт, проверяющий наличие тестов, обычно:
1. Берёт список изменённых файлов.
2. Фильтрует только те, что относятся к «боевому» коду.
3. Для каждого такого файла пытается найти тестовый файл по заданному паттерну.
4. Если теста нет — возвращает `exit 1` и объяснение в терминале, какого теста не хватает.
Так команда постепенно приучается: новый код = новые тесты.
---
Python: обязательные тесты для новых модулей
В Python можно применить ровно ту же логику:
- в `pre-commit`:
- базовая проверка существования тестов для новых или сильно изменённых модулей;
- быстрый запуск самого критичного набора тестов (например, unit-слой).
- в `pre-push` или CI:
- полный прогон `pytest`;
- контроль покрытия (например, `pytest --cov` с требованием определённого процента).
Скрипт проверки наличия тестов может:
- искать соответствие между `package/module.py` и `tests/test_module.py`;
- ругаться, если модуль из `app/` или `src/` не имеет теста в `tests/`.
Так легко избежать ситуации, когда новый функционал «забыли» покрыть тестами, а потом это всплывает уже на продакшене.
---
Docker-варианты скриптов
Во многих командах окружения у разработчиков заметно отличаются: версии Node, Python, набор глобально установленных утилит, системные библиотеки. Из-за этого локальные проверки могут проходить, а в CI — падать (или наоборот).
Чтобы минимизировать такие расхождения, удобно держать альтернативные Docker-версии тех же скриптов. Логика простая:
- каждый проверочный скрипт имеет Docker-обёртку, которая:
- запускает нужный образ (например, с заранее установленными Node, Python, линтерами, тестовыми фреймворками);
- монтирует кодовую базу внутрь контейнера;
- внутри контейнера выполняет те же команды линтера/форматера/тестов.
Таким образом:
- разработчик может не устанавливать локально весь тяжёлый стек зависимостей;
- результат проверки стабилен — он зависит только от образа, а не от состояния локальной машины;
- CI использует тот же Docker-образ, что и локальная разработка — нет расхождений между «у меня работает» и «в пайплайне всё упало».
---
Преимущества Docker-скриптов
Использование Docker-обёрток даёт несколько ощутимых плюсов:
1. Единое окружение для команды
Все проверки выполняются в идентичном контейнере. Никаких сюрпризов из-за разных версий Node, Python, npm, pip-пакетов и т.п.
2. Простота онбординга
Новому разработчику не нужно тратить полдня на настройку линтеров и тестовых раннеров. Достаточно Docker и Git — всё остальное приходит «в комплекте» со скриптами.
3. Упрощённая поддержка
Обновление версий инструментов происходит централизованно: меняется Dockerfile, пересобирается образ — и вся команда начинает пользоваться обновлённым стеком.
4. Тестируемость сборки
Docker-контейнер, в котором прогоняются проверки, близок к продакшн-среде. Это снижает риск того, что что-то сломается только «на бою» из-за отличий в окружении.
---
Гибкость и расширяемость пайплайна
Хорошо организованный набор Git-хуков — это не статичная конструкция, а живой инструмент, который растёт вместе с проектом. Его легко адаптировать под будь какую технологическую связку.
Несколько примеров, как можно развивать систему:
- Добавить проверку миграций БД (например, убеждаться, что после изменения моделей есть соответствующие миграции).
- Включить проверку лицензий зависимостей (чтобы не тащить в проект запрещённые библиотеки).
- Встроить безопасностные сканеры (поиск секретов в коде, статический анализ на уязвимости).
- Ввести разные профили проверок:
- «быстрый» — для ежедневных коммитов;
- «полный» — для релизных веток или тегов.
Все эти вещи добавляются по той же схеме: отдельный `.sh`-скрипт + нужный Git-хук + (опционально) Docker-обёртка.
---
Практические советы по внедрению
Чтобы Git-хуки не превратились в боль и саботаж со стороны команды, можно придерживаться нескольких правил:
1. Начинайте с малого
Сначала включите только самые быстрые и полезные проверки:
- форматирование (Prettier, Black для Python, и т.п.);
- базовый линтинг (ESLint, Flake8).
2. Избегайте долгих проверок в pre-commit
То, что занимает минуты, лучше отдать `pre-push` или CI. В `pre-commit` должны оставаться проверки, проходящие за секунды.
3. Сделайте удобные сообщения об ошибках
Лог скриптов должен быть понятен: что за ошибка, в каком файле, как её исправить. Тогда разработчик не будет воспринимать хук как «чёрный ящик».
4. Договоритесь о правилах в команде
Набор правил линтера и форматтера должен быть согласован заранее. Лучше один раз потратить время на настройки, чем постоянно спорить о стиле.
5. Позвольте временно отключать тяжёлые проверки локально
Иногда нужно сделать быстрый черновой коммит. Можно предусмотреть флаг для пропуска части проверок локально, но оставить их обязательными в CI.
---
Итоги
Грамотно настроенные Git-хуки превращают процесс коммита из «слепого» сохранения изменений в осознанный чекпоинт качества. Они:
- автоматически форматируют код и приводят его к единым стандартам;
- не пропускают линтерные и типовые ошибки в репозиторий;
- заставляют не забывать о тестах и покрытии;
- выравнивают окружение разработки и CI за счёт Docker-обёрток;
- снимают с ревьюверов заботу о мелочах, оставляя им фокус на архитектуре и логике.
В результате команда получает более чистый, предсказуемый и устойчивый код, а фейлы в CI по банальным причинам практически исчезают. Всё это достигается относительно простыми скриптами и правильной конфигурацией Git-хуков — без тяжёлых и дорогих решений.



