Git-хуки для javascript, typescript и python: как отсекать плохой код до коммита

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-хуков — без тяжёлых и дорогих решений.

Прокрутить вверх