Здравствуйте, коллеги программисты!
Большинство фейлов в CI — это мелочи: забытый console.log, форматирование, линт, сломанный импорт, файл без теста. Такие ошибки не должны доезжать до сборки или код-ревью.
Git-хуки позволяют запускать проверки прямо во время git commit и блокировать коммит, если были обнаружены нарушения.
В прошлой статье я рассказывал про скрипты, которые я использую для проверки качества кода в PHP/Laravel.
В этой статье я хочу рассказать о скриптах для JavaScript/TypeScript и Python — линтинг, форматирование, тесты, статический анализ и проверка наличия тестов.
Все скрипты описанные в статье находятся здесь - https://github.com/prog-time/git-hooks
Как это работает
Каждый скрипт — обычный .sh файл. Для каждого типа проверки я стараюсь делать 2 версии файла:
Скрипт который принимает список файлов.
Например:bash python/check_flake8.sh $FILESСкрипт который запускает проверку на весь проект.
Например:bash python/check_flake8_all.sh
Каждый скрипт производит описанную проверку и возвращает код выхода:
0— всё хорошо, коммит проходит;1— есть ошибки, коммит блокируется.
Версию скрипта с передачей файлов я использую для проверки через .git/hooks/pre-commit, где передаю список файлов добавленных в Git индекс. Версию с суффиксом *_all.sh я использую для проверки всего проекта в .git/hooks/pre-push или в CI.
Простой пример — вызов из pre-commit:
#!/bin/bash set -e # получаю все измененные файлы ALL_FILE_ARRAY=() while IFS= read -r line; do ALL_FILE_ARRAY+=("$line") done < <(git diff --cached --name-only --diff-filter=ACM || true) bash scripts/python/check_flake8.sh $ALL_FILE_ARRAY bash scripts/js/check_eslint_all.sh
JavaScript: Форматирование и линтинг
Базовый уровень защиты кода заключается в том, чтобы сразу приводить его к единому стилю и ловить очевидные ошибки ещё до того, как они попадут в репозиторий. Это позволяет снизить число банальных проблем, которые обычно тормозят командную работу.
ESLint
Скрипт check_eslint_all.sh — обёртка над ESLint, которая проверяет и автоматически исправляет ошибки по всему проекту.
Скрипт запускает npx eslint --fix по директориям указанным в переменной LINT_DIRS.
Автоисправимые проблемы чинятся автоматически. Если остаются ошибки (например, неиспользуемые переменные или сломанные импорты) — скрипт завершается с exit 1, и коммит блокируется.
#!/bin/bash # ------------------------------------------------------------------------------ # Runs ESLint with --fix on the entire project (app/, components/, lib/, types/). # No file arguments required — always checks all configured directories. # Exits 1 if ESLint fails to fix issues, 0 on success. # ------------------------------------------------------------------------------ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # Directories to check LINT_DIRS=("app/" "components/" "lib/" "types/") echo "=== ESLint (full project) ===" echo "Fixing ESLint issues..." if ! npx eslint "${LINT_DIRS[@]}" --fix; then echo "ERROR: Failed to fix ESLint issues" exit 1 fi echo "ESLint issues fixed!" exit 0
Prettier
Для форматирования есть два варианта:
check_prettier_all.sh— форматирует весь проект;check_prettier.sh— только конкретные файлы.
В pre-commit обычно используется второй вариант — форматируются только изменённые файлы. Это быстрее и не создаёт лишних диффов.
Скрипт просто прогоняет prettier --write, поэтому разработчику не нужно думать о пробелах, переносах и кавычках — стиль применяется автоматически.
Версии этих скриптов находятся здесь - https://github.com/prog-time/git-hooks/tree/main/javascript
TSC
Скрипт check_tsc_all.sh запускает проверку типов без сборки. Для настройки конфигурации используется tsconfig.check.json.
Это полезно, когда проект большой: можно случайно «уронить» типизацию в другом модуле, и обычный линт это не поймает.
#!/bin/bash # ------------------------------------------------------------------------------ # Runs TypeScript type checking on the entire project using tsconfig.check.json. # No file arguments required — checks all files configured in tsconfig. # Exits 1 if type check fails, 0 on success. # ------------------------------------------------------------------------------ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # TypeScript config TS_CONFIG="tsconfig.check.json" echo "=== TypeScript ===" echo "Running type check..." if ! npx tsc --project "$TS_CONFIG"; then echo "ERROR: TypeScript check failed" exit 1 fi echo "TypeScript check passed!" exit 0
Для работы данного скрипта необходимо в корне проекта создать файл tsconfig.check.json.
Пример реализации tsconfig.check.json:
{ "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "noUncheckedIndexedAccess": true, "plugins": [{ "name": "next" }], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }
Python: линтинг и статический анализ
Для Python набор Git-хуков выполняет ту же роль, что и для JavaScript/TypeScript, но с учётом особенностей экосистемы: динамическая типизация и разнообразие стилей кодирования требуют чуть более строгой проверки. Здесь два основных направления: линтинг и статический анализ типов.
Линтинг с Flake8
Скрипт check_flake8.sh проверяет .py файлы на соответствие PEP8 и код-стайлу.
Flake8 ловит мелкие проблемы ещё до запуска тестов или деплоя: лишние пробелы, неверные отступы, слишком длинные строки, нарушения соглашений по именованию функций и переменных. Это снижает риск, что код будет выглядеть по-разному в разных модулях и усложнит чтение для команды.
#!/bin/bash # ---------------------------------------- # Python Code Style Checker # # This script checks Python files for style # issues using flake8. It runs directly on # the host (no Docker required). # ---------------------------------------- if [ $# -eq 0 ]; then echo "No files to check" exit 0 fi PY_FILES=() CHECKED_FILES=0 HAS_ERRORS=0 for file in "$@"; do # Skip non-Python files if [[ ! "$file" =~ \.py$ ]]; then continue fi # Skip if file doesn't exist if [ ! -f "$file" ]; then continue fi PY_FILES+=("$file") done # Check if there are any Python files to check if [ ${#PY_FILES[@]} -eq 0 ]; then echo "No Python files to check" exit 0 fi # Run flake8 linter on each file for file in "${PY_FILES[@]}"; do CHECKED_FILES=$((CHECKED_FILES + 1)) OUTPUT=$(flake8 --max-line-length=120 "$file" 2>&1) EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then HAS_ERRORS=1 echo "Style errors in: $file" echo "$OUTPUT" echo "" fi done # Final result if [ $HAS_ERRORS -ne 0 ]; then echo "----------------------------------------" echo "ERROR: Code style check failed!" echo "Total files checked: $CHECKED_FILES" echo "Fix the errors above before committing." exit 1 fi echo "Code style check passed! ($CHECKED_FILES files checked)" exit 0
Скрипт выводит ошибки стиля прямо в терминал. Например:
Style errors in: app/services/user_service.py app/services/user_service.py:42:1: E302 expected 2 blank lines, found 1 app/services/user_service.py:67:80: E501 line too long (132 > 120 characters)
Статический анализ с Mypy
Скрипт check_mypy.sh выполняет статический анализ типов.
Скрипт игнорирует тесты и вспомогательные файлы, чтобы фокусироваться только на продакшн-коде. Mypy позволяет выявлять потенциальные баги, которые в динамическом Python часто остаются незамеченными до выполнения кода.
#!/bin/bash # ------------------------------------------------------------ # Runs mypy static analysis for changed Python files locally. # Accepts file paths as args and checks only app/*.py files. # ------------------------------------------------------------ if [ $# -eq 0 ]; then echo "No files to check" exit 0 fi PY_FILES=() for file in "$@"; do # Skip non-Python files if [[ ! "$file" =~ \.py$ ]]; then continue fi # Only files inside app/ if [[ ! "$file" =~ ^app/ ]]; then continue fi # Skip if file doesn't exist if [ ! -f "$file" ]; then continue fi PY_FILES+=("$file") done if [ ${#PY_FILES[@]} -eq 0 ]; then echo "No Python files to check" exit 0 fi OUTPUT=$(mypy \ --pretty \ --show-error-codes \ --ignore-missing-imports \ --follow-imports=skip \ "${PY_FILES[@]}" 2>&1) EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then echo "----------------------------------------" echo "ERROR: Static analysis failed!" echo "----------------------------------------" echo "$OUTPUT" echo "----------------------------------------" echo "Total files checked: ${#PY_FILES[@]}" exit 1 fi echo "Static analysis passed! (${#PY_FILES[@]} files checked)" exit 0
Зачем нужна связка Flake8 + Mypy
Flake8 следит за стилем и базовыми ошибками, экономя время на код-ревью.
Mypy ловит ошибки типов, которые не видны при обычном запуске Python.
Вместе они создают первый фильтр качества: код не попадёт в коммит, пока не будет корректен по стилю и типам. Разработчик получает мгновенную обратную связь, а команда — стабильный и читаемый код, готовый к тестированию и деплою.
Тесты и проверка покрытия
Следующий уровень защиты — убедиться, что новый код не только корректен по стилю и типам, но и покрыт тестами. В наборе хуков есть проверки для JavaScript/TypeScript и Python, которые гарантируют, что для изменённых файлов есть соответствующие тесты, а сами тесты проходят.
JavaScript: проверки наличия тестов
Процесс проверки тестирования делится на 2 стадии:
проверка наличия теста
запуск теста для измененного функционала
Скрипт check_tests_exist.sh проверяет наличие тестов для каждого .ts файла и блокирует коммит, если тест отсутствует. Исключения для конфигураций и вспомогательных файлов задаются в переменной SKIP_PATTERNS.
Данный скрипт, проверяет, чтобы для каждого файла существовал файл теста, который должен находиться в директории /tests. Например для файла app/api/v1/roles/route.ts обязательно должен существовать тест tests/app/api/v1/roles/route.test.ts.
#!/bin/bash # ------------------------------------------------------------------------------ # Checks that each staged TypeScript source file has a corresponding test file. # Receives files as arguments or reads from git staged files if none provided. # Exits 1 if any source file is missing its tests/...test.ts counterpart. # ------------------------------------------------------------------------------ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # Patterns to skip (no tests required) SKIP_PATTERNS=("tests/" ".test.ts" ".config.ts" ".config.mjs" "types/" ".d.ts" "layout.tsx" "page.tsx" "loading.tsx" "error.tsx" "globals.css" "providers/" "components/ui/" "prisma/") should_skip() { local file="$1" for pattern in "${SKIP_PATTERNS[@]}"; do [[ "$file" == *"$pattern"* ]] && return 0 done return 1 } # Get test path: source.ts -> tests/source.test.ts get_test_path() { local file="$1" local base="${file%.ts}" base="${base%.tsx}" echo "tests/${base}.test.ts" } # Get files to check FILES=() if [ $# -eq 0 ]; then while IFS= read -r line; do [[ "$line" == *.ts || "$line" == *.tsx ]] && FILES+=("$line") done < <(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true) [ ${#FILES[@]} -eq 0 ] && echo "No staged TypeScript files" && exit 0 else for arg in "$@"; do [[ "$arg" == *.ts || "$arg" == *.tsx ]] && FILES+=("$arg") done fi echo "=== Test Coverage Check ===" echo "" missing=() found=() skipped=() for file in "${FILES[@]}"; do [ ! -f "$file" ] && continue should_skip "$file" && { skipped+=("$file"); continue; } test_path=$(get_test_path "$file") if [ -f "$test_path" ]; then found+=("$file") else missing+=("$file → $test_path") fi done [ ${#found[@]} -gt 0 ] && echo -e "Has tests:" && printf ' %s\n' "${found[@]}" && echo "" [ ${#skipped[@]} -gt 0 ] && echo -e "Skipped:" && printf ' %s\n' "${skipped[@]}" && echo "" if [ ${#missing[@]} -gt 0 ]; then echo -e "Missing tests:" printf ' %s\n' "${missing[@]}" echo "" echo -e "ERROR: ${#missing[@]} file(s) missing tests" exit 1 fi echo -e "All files have tests!"
Также есть check_tests.sh, который находит конкретные тесты для изменённых файлов и запускает их.
Запуск тестов для измененных файлов - https://github.com/prog-time/git-hooks/blob/main/javascript/check_tests.sh
Запуск тестов для всего проекта - https://github.com/prog-time/git-hooks/blob/main/javascript/check_tests_all.sh
Python: проверки наличия тестов
Для работы с Python используется похожий набор скриптов:
find_tests.sh- проверяет наличие тест файлаcheck_pytest.sh- запускает тесты для измененных файлов
Эти проверки делают pre-commit не просто линтером, а локальным гарантом качества кода: даже если разработчик забудет написать тест, коммит не пройдет, а команда получает стабильный и проверенный код.
Docker-варианты скриптов
Для Python-проектов, которые используют специфичные зависимости или системные библиотеки, иногда запуск линтеров и тестов на локальной машине даёт ошибки, которых нет в контейнере. Чтобы избежать расхождений окружений, набор хуков включает Docker-версии скриптов.
Для большинства скриптов, есть вариации скриптов, которые должны запускаться через Docker. Каждый такой скрипт имеет в название суффикс _in_docker.
Каждый инструмент имеет два варианта запуска:
Локальный — обычный скрипт (
check_flake8.sh,check_mypy.sh,check_pytest.sh) работает на хосте.Docker — скрипт запускается внутри контейнера (
check_flake8_in_docker.sh,check_mypy_in_docker.sh,check_pytest_in_docker.sh).
Внутри контейнера запускается инструмент:
docker exec app_dev mypy /app/services/auth.py
Ошибки выводятся обратно с привычными хостовыми путями:
app/services/auth.py:15: error: Incompatible return value type
Если контейнер не запущен, скрипт выдаёт понятное сообщение:
ERROR: Container 'app_dev' is not running Start it with: docker-compose -f docker/dev/docker-compose.yml up -d
Преимущества Docker-скриптов
Консистентность окружения — проверки проходят в том же окружении, что и продакшн.
Без зависимости от локальной машины — версии Python, системных библиотек или расширений не влияют на результат.
Прямые пути в терминале — ошибки отображаются так же, как если бы вы работали локально.
Легкая интеграция с pre-commit — просто замените локальный скрипт на Docker-версию, остальная логика остаётся той же.
Гибкость и тестируемость сборки
Моя сборка уникальна тем, что все скрипты разделены на отдельные подгруппы. Вы можете подключать только те проверки, которые реально нужны вашему проекту, и собирать собственную подборку хуков, без лишнего кода и лишних проверок.
Например, можно использовать только линтинг и Prettier для фронтенда, а для Python оставить только Flake8 и Mypy, либо подключить все проверки сразу — выбор за вами.
Кроме того, в проекте есть автотесты для всех shell-скриптов, которые используются в этой статье. Это значит, что вы можете:
Склонировать репозиторий:
git clone https://github.com/prog-time/git-hooks.gitНастроить конфигурацию под свои директории, инструменты и правила.
Прогнать вашу версию через встроенные тесты, чтобы убедиться, что все скрипты работают корректно и блокируют коммит при ошибках.
Такой подход делает сборку надёжной и предсказуемой: вы точно знаете, что хуки сработают так, как задумано, а команда получает стабильный инструмент контроля качества кода, который можно адаптировать под любые проекты.
Итого
Релиз 2.0.0 превращает pre-commit в полноценный фильтр качества кода, который работает ещё до того, как изменения попадут в репозиторий.
Ключевые возможности:
JavaScript/TypeScript: ESLint, Prettier, проверка типов через TypeScript, запуск тестов (полный и по изменённым файлам), проверка наличия тестов.
Python: Flake8, Mypy, Pytest (локально и в Docker), проверка наличия тестов с контролем дубликатов.
Docker-поддержка: скрипты корректно работают в контейнере, автоматически маппят пути и выводят понятные ошибки.
Гибкость: скрипты разделены на подгруппы, можно подключать только нужные проверки и собирать свою подборку под конкретный проект.
Тестируемость: все скрипты покрыты автотестами, вы можете прогонять свою конфигурацию, чтобы убедиться в её надёжности.
Если вам понравилась эта сборка и она оказалась полезной для вашей команды — не забудьте поставить ⭐ на GitHub. Это помогает проекту развиваться, а мне — видеть, что работа приносит пользу разработчикам.
Если вы используете эту сборку и находите, что что-то можно улучшить, буду рад вашим Pull Request! Любые предложения по новым хукам, исправлениям или оптимизации скриптов помогут сделать проект ещё полезнее для всех.
Репозиторий: https://github.com/prog-time/git-hooks
Комментарии (20)

Tishka17
07.02.2026 12:46Все ещё считаю, что коммитить плохой код - это нормально. Промежуточные результаты работы - тоже результаты и терять их не хочется (например, если это результаты вашего коллеги, который внезапно заболел). Мерджить такой код конечно не надо, но в отдельной ветке пусть лежит, жалко что ли

Prog-Time Автор
07.02.2026 12:46Согласен! Вы же всегда можете временно отключить pre-commit скрипты и пролить ветку в репозитори, а внутри репозитория вас уже не пропустит CI.
Но я всё же против такого подхода, так как считаю, что commit - это точка наиболее стабильной версии кода. По этой причине я при каждом коммите запускаю проверку тестов.
Tishka17
07.02.2026 12:46Я в пет проекте сейчас 2 недели пилю новую сложную фичу. До стабильной версии ещё много, постоянно что-то не так. Мне не коммитить?
Нет, коммит - это не стабильная версия. Это окончание какой-то мысли, какого-то этапа работы. Стабильная версия будет после мерджа

Prog-Time Автор
07.02.2026 12:46Ну да, перефразировали и выдали за правильное определение!
Вы на, так называемое "окончание мысли", не пишите автотесты? Не подгоняете code style под общий стандарт? Не следите за ошибками?
Если вы всё это делаете, то вам не стоит бояться pre-commit проверок. Я в статье написал, что мои pre-commit скрипты проверяют только измененные файлы. А внутри pre-push проверяется уже весь проект целиком, чтобы лишний раз не гонять CI.
Tishka17
07.02.2026 12:46Окончанием мысли могу быть тесты. Сломанные :) потому что как их чинить - это другая мысль

radist2s
07.02.2026 12:46Ненавижу git-хуки, которые не дают мне коммииить любой код, какой хочу.

Prog-Time Автор
07.02.2026 12:46А как ты относишься к git-хукам, которые автоматически исправляют code style? Или те, что корректируют комментарий к коммиту?
Это достаточно универсальная вещь, которая приносит много пользы!
radist2s
07.02.2026 12:46Я честно скажу: я вообще принципиально отключаю все хуки, потому что почти весь код пишет LLM, как и комментарии к коммитам. Сори.

DEgITx
07.02.2026 12:46Одно дело которые корректируют и улучшают без твоего импакта, а другое дело когда вообще коммитеть не дают. Ты задолбешься этот рубильник выключать и включать. Столько ситуаций когда нужно как есть коммитеть что не перечесть.
Поэтому отношу это всё скорее к вредным советам, которые полезны меньшинству чем большинству.

Tishka17
07.02.2026 12:46Пусть улучшает отдельным коммитом. А то так улучшит, что не откатить потом

DEgITx
07.02.2026 12:46ну при мне нормальные матерые стилизаторы тот же astyle для плюсов, Prettier для js и т.п. для каждого из языков никогда ничего не ломали - вполне доверяю и на коммиты ставить. Если что-то более сложное человеку хочется настроить на коммит - то да, можно отдельным обезопасить себя.

Tishka17
07.02.2026 12:46Плюсы и js не закладывают в отступы семантику. Код не меняет из-за отступов, можно форматировать как угодн. В питоне сиутация не такая - изменение форматирования может сломать код, поэтому форматирование закладывается на этапе написания - как вижу так он работать и будет. И вот если вдруг ты решил закоммитить сырой сломанный код (мало ли что случилось, очень надо), некоторые форматтеры могут решить что отформатировать его всё равно надо и что-то куда-то передвинуть

Timur_Sarvarov
07.02.2026 12:46Посмотрите в сторону biome как альтернатива prettier и lint. Быстрее раз в 10. И да слышали ли вы про подход trophy для fe ?

Prog-Time Автор
07.02.2026 12:46Отлично! С biome не работал, но обязательно посмотрю!
"...слышали ли вы про подход trophy для fe ?" - не совсем понял, что вы имеете ввиду. Опишите пожалуйста подробнее.
Timur_Sarvarov
07.02.2026 12:46не все файлы надо покрывать тестами, так ты цементируешь приложение, каждый чих в этом случае будет требовать большого усилия для переписывания тестов, e2e только для критичных flow (авторизация, чекаут, базовый функционал), итеграционные для критически важных кастомных utlilities

Tishka17
07.02.2026 12:46В пет проекте у меня покрытие 93%. Какой же это кайф рефакторить. Я на днях взял и переписал значительный кусок и просто следил что все тесты прошли. Если бы у меня их не было, я не знаю как бы с этой задачей справился - очень много вещей стреляло, о которых я уже даже не помнил
rSedoy
Да ладно, в текущем году, упомянуть python, но не поставить ruff-pre-commit или не указать альтернативы mypy, эт жирный минус.
Prog-Time Автор
Я только начинаю изучать Python, поэтому буду рад если вы предложите свои альтернативы.
Про ruff я слышал, обязательно позже добавлю версию скрипта с проверкой ruff-pre-commit!
Спасибо, что подсветили!