Большинство фейлов в CI — это мелочи: забытый console.log, форматирование, линт, сломанный импорт, файл без теста. Такие ошибки не должны доезжать до сборки или код-ревью.
Git-хуки позволяют запускать проверки прямо во время git commit и блокировать коммит, если были обнаружены нарушения.
Все скрипты находятся здесь: https://github.com/prog-time/git-hooks
Как это работает
Структура скриптов
Каждый скрипт — обычный .sh файл. Для каждого типа проверки существует 2 версии:
-
Скрипт для конкретных файлов
Например:bash python/check_flake8.sh $FILES -
Скрипт для всего проекта
Например:bash python/check_flake8_all.sh
Коды возврата
0— всё хорошо, коммит проходит1— есть ошибки, коммит блокируется
Применение
- Версия с передачей файлов →
pre-commit(проверка файлов в Git индексе) - Версия
*_all.sh→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/TypeScript: Форматирование и линтинг
Базовый уровень защиты кода заключается в том, чтобы сразу приводить его к единому стилю и ловить очевидные ошибки ещё до того, как они попадут в репозиторий.
ESLint
Скрипт check_eslint_all.sh — обёртка над ESLint, которая проверяет и автоматически исправляет ошибки по всему проекту.
Возможности:
- Запускает
npx eslint --fixпо директориям из переменнойLINT_DIRS - Автоисправимые проблемы чинятся автоматически
- При неисправимых ошибках (неиспользуемые переменные, сломанные импорты) — блокирует коммит
Пример скрипта:
#!/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
TypeScript (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:
{
"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 выполняет статический анализ типов.
Особенности:
- Игнорирует тесты и вспомогательные файлы
- Фокусируется только на продакшн-коде
- Выявляет потенциальные баги, которые в динамическом 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: проверка тестов
Процесс проверки тестирования делится на 2 стадии:
- Проверка наличия теста
- Запуск теста для изменённого функционала
Проверка наличия тестов
Скрипт check_tests_exist.sh проверяет наличие тестов для каждого .ts файла и блокирует коммит, если тест отсутствует.
Правила:
- Для файла
app/api/v1/roles/route.tsдолжен существовать тестtests/app/api/v1/roles/route.test.ts - Исключения задаются в переменной
SKIP_PATTERNS
Пример скрипта:
#!/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— находит и запускает тесты для изменённых файлов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
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 для фронтенда
- Только Flake8 и Mypy для Python
- Все проверки сразу
Автотесты для скриптов
В проекте есть автотесты для всех 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-поддержка:
- Скрипты корректно работают в контейнере
- Автоматически маппят пути
- Выводят понятные ошибки
Гибкость:
- Скрипты разделены на подгруппы
- Можно подключать только нужные проверки
- Собирать свою подборку под конкретный проект
Тестируемость:
- Все скрипты покрыты автотестами
- Можно прогонять свою конфигурацию для проверки надёжности
Ссылки
Репозиторий: https://github.com/prog-time/git-hooks
Если вам понравилась эта сборка и она оказалась полезной для вашей команды — не забудьте поставить ⭐ на GitHub!
Если вы используете эту сборку и находите, что что-то можно улучшить, буду рад вашим Pull Request! Любые предложения по новым хукам, исправлениям или оптимизации скриптов помогут сделать проект ещё полезнее для всех.