Здравствуйте, коллеги программисты!

Большинство фейлов в CI — это мелочи: забытый console.log, форматирование, линт, сломанный импорт, файл без теста. Такие ошибки не должны доезжать до сборки или код-ревью.

Git-хуки позволяют запускать проверки прямо во время git commit и блокировать коммит, если были обнаружены нарушения.

В прошлой статье я рассказывал про скрипты, которые я использую для проверки качества кода в PHP/Laravel.

В этой статье я хочу рассказать о скриптах для JavaScript/TypeScript и Python — линтинг, форматирование, тесты, статический анализ и проверка наличия тестов.

Все скрипты описанные в статье находятся здесь - https://github.com/prog-time/git-hooks

Как это работает

Каждый скрипт — обычный .sh файл. Для каждого типа проверки я стараюсь делать 2 версии файла:

  1. Скрипт который принимает список файлов.
    Например: bash python/check_flake8.sh $FILES

  2. Скрипт который запускает проверку на весь проект.
    Например: 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-скриптов, которые используются в этой статье. Это значит, что вы можете:

  1. Склонировать репозиторий: git clone https://github.com/prog-time/git-hooks.git

  2. Настроить конфигурацию под свои директории, инструменты и правила.

  3. Прогнать вашу версию через встроенные тесты, чтобы убедиться, что все скрипты работают корректно и блокируют коммит при ошибках.

Такой подход делает сборку надёжной и предсказуемой: вы точно знаете, что хуки сработают так, как задумано, а команда получает стабильный инструмент контроля качества кода, который можно адаптировать под любые проекты.

Итого

Релиз 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)


  1. rSedoy
    07.02.2026 12:46

    Да ладно, в текущем году, упомянуть python, но не поставить ruff-pre-commit или не указать альтернативы mypy, эт жирный минус.


    1. Prog-Time Автор
      07.02.2026 12:46

      Я только начинаю изучать Python, поэтому буду рад если вы предложите свои альтернативы.

      Про ruff я слышал, обязательно позже добавлю версию скрипта с проверкой ruff-pre-commit!

      Спасибо, что подсветили!


  1. past
    07.02.2026 12:46

    Кажется, автор изобрел велосипед.

    https://pre-commit.com/


  1. Tishka17
    07.02.2026 12:46

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


    1. Prog-Time Автор
      07.02.2026 12:46

      Согласен! Вы же всегда можете временно отключить pre-commit скрипты и пролить ветку в репозитори, а внутри репозитория вас уже не пропустит CI.

      Но я всё же против такого подхода, так как считаю, что commit - это точка наиболее стабильной версии кода. По этой причине я при каждом коммите запускаю проверку тестов.


      1. Tishka17
        07.02.2026 12:46

        Я в пет проекте сейчас 2 недели пилю новую сложную фичу. До стабильной версии ещё много, постоянно что-то не так. Мне не коммитить?

        Нет, коммит - это не стабильная версия. Это окончание какой-то мысли, какого-то этапа работы. Стабильная версия будет после мерджа


        1. Prog-Time Автор
          07.02.2026 12:46

          Ну да, перефразировали и выдали за правильное определение!

          Вы на, так называемое "окончание мысли", не пишите автотесты? Не подгоняете code style под общий стандарт? Не следите за ошибками?

          Если вы всё это делаете, то вам не стоит бояться pre-commit проверок. Я в статье написал, что мои pre-commit скрипты проверяют только измененные файлы. А внутри pre-push проверяется уже весь проект целиком, чтобы лишний раз не гонять CI.


          1. Tishka17
            07.02.2026 12:46

            Окончанием мысли могу быть тесты. Сломанные :) потому что как их чинить - это другая мысль


    1. wert_lex
      07.02.2026 12:46

      `git commit --no-verify` ?


  1. radist2s
    07.02.2026 12:46

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


    1. Prog-Time Автор
      07.02.2026 12:46

      А как ты относишься к git-хукам, которые автоматически исправляют code style? Или те, что корректируют комментарий к коммиту?

      Это достаточно универсальная вещь, которая приносит много пользы!


      1. radist2s
        07.02.2026 12:46

        Я честно скажу: я вообще принципиально отключаю все хуки, потому что почти весь код пишет LLM, как и комментарии к коммитам. Сори.


  1. DEgITx
    07.02.2026 12:46

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

    Поэтому отношу это всё скорее к вредным советам, которые полезны меньшинству чем большинству.


    1. Tishka17
      07.02.2026 12:46

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


      1. DEgITx
        07.02.2026 12:46

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


        1. Tishka17
          07.02.2026 12:46

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


  1. Timur_Sarvarov
    07.02.2026 12:46

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


    1. Prog-Time Автор
      07.02.2026 12:46

      Отлично! С biome не работал, но обязательно посмотрю!
      "...слышали ли вы про подход trophy для fe ?" - не совсем понял, что вы имеете ввиду. Опишите пожалуйста подробнее.


      1. Timur_Sarvarov
        07.02.2026 12:46

        не все файлы надо покрывать тестами, так ты цементируешь приложение, каждый чих в этом случае будет требовать большого усилия для переписывания тестов, e2e только для критичных flow (авторизация, чекаут, базовый функционал), итеграционные для критически важных кастомных utlilities


        1. Tishka17
          07.02.2026 12:46

          В пет проекте у меня покрытие 93%. Какой же это кайф рефакторить. Я на днях взял и переписал значительный кусок и просто следил что все тесты прошли. Если бы у меня их не было, я не знаю как бы с этой задачей справился - очень много вещей стреляло, о которых я уже даже не помнил