Пайплайн проверки качества: pre-commit, линтеры и форматирование без «войны стилей»
Соберём практическую схему: что запускать локально, что в CI и как договориться о едином стиле команды с минимумом шума.
Содержание
Пайплайн проверки качества: pre-commit, линтеры и форматирование без «войны стилей»
Когда в команде появляется несколько разработчиков, очень быстро выясняется, что качество кода — это не только про “правильные алгоритмы”, но и про единообразие: одинаковые правила оформления, воспроизводимые проверки, отсутствие случайных отличий в стиле и форматировании, которые создают лишние диффы в PR. Проблема усугубляется тем, что линтеры, форматтеры и проверки часто спорят друг с другом: один ругается на стиль, второй форматирует обратно, третий считает это “изменением”. Итог — ощущение, что инструментов много, а пользы мало.
Ниже — практическая схема пайплайна проверки качества, которая снижает шум в PR, делает поведение инструментов предсказуемым и помогает команде договориться о едином стиле с минимальными “войнами” — как на локальных машинах, так и в CI. Речь пойдёт про Python, но подход применим почти к любому языку: локально — быстрые проверки, в CI — гарантии, а форматирование — отдельный этап без двусмысленностей.
Зачем нужен пайплайн: где именно возникают конфликты
Почти все конфликты в стиле и качестве сводятся к трём источникам:
- Непоследовательные инструменты. Например, форматирование одним инструментом, а линт на другой набор правил. В результате линтер считает “ошибкой” то, что форматтер намеренно сделал.
- Неподходящее место запуска. Если форматтер гоняется в CI, но разработчики не запускают его локально, каждый PR превращается в повторный “раунд правок”.
- Отсутствие “единого источника правды”. Команда не зафиксировала правила (версии, настройки, приоритеты), и каждый применяет “свои” параметры.
Правильная архитектура пайплайна выглядит так:
- pre-commit отвечает за быстрые проверки и авто-исправления до коммита.
- линтеры отвечают за статические проблемы качества (ошибки, потенциальные баги, анти-паттерны).
- форматтер отвечает за внешний вид (стандартизирует код).
- CI подтверждает, что всё прошло и результат воспроизводим.
Ключевой принцип: форматтер должен либо запускаться перед линтером, либо быть частью правила, которое линтер не ломает. Иначе вы получаете вечный цикл “линтер ругается — форматтер правит — линтер снова ругается”.
Инструментальная база: что именно использовать в Python
Для Python сегодня есть “устойчивый” стек, который обычно хорошо работает в командах:
- ruff как линтер и набор правил стиля/антипаттернов.
- ruff format как форматтер, чтобы не плодить несколько движков форматирования.
- pre-commit для управления хукaми на локальной стороне.
- CI (GitHub Actions / GitLab CI / другая система) для проверки каждого PR.
Почему именно ruff:
- Он умеет быть и линтером, и частью форматирования (через
ruff format). - Настройки можно свести в единый
pyproject.toml. - В отличие от “зоопарка” линтеров, проще договориться о приоритетах.
Чтобы читателю было проще стартовать, можно рассматривать курс “ruff – для начинающих!” как дополнительный путь освоения инструмента глубже: /course/ruff-free. Важно, что в рамках команды основная договорённость всё равно делается через конфигурацию и пайплайн, а не через “знания откуда-то”.
Базовая схема: локально быстро, в CI гарантированно
Ниже — пример архитектуры, которая минимизирует шум и гарантирует согласованность.
Что запускать локально (pre-commit)
На локальной стороне важно:
- проверять и исправлять форматирование,
- ловить очевидные проблемы качества,
- не заставлять разработчика ждать 3–10 минут на каждом коммите.
Практически: pre-commit конфигурируется так, чтобы:
- форматтер запускался первым (он может вносить изменения),
- линтер запускался вторым (он проверяет результат форматирования),
- в случае ошибок линтер/форматтер “останавливают” коммит.
Что запускать в CI
В CI уместны более “тяжёлые” проверки, но всё равно с прицелом на воспроизводимость:
- линтер/статические проверки для всего репозитория (а не только изменившихся файлов),
- единообразная версия инструментов (зафиксировать в зависимостях),
- (опционально) прогон тестов, mypy и т.д.
Если линтер и форматтер уже отрабатывают в pre-commit, то в CI мы в основном подтверждаем соответствие “всему репозиторию стандарту”.
Опционально: split “format vs lint” в конфигурации
Хорошая практика — разделять:
- formatting (приведение к стилю),
- linting (семантические и структурные проверки).
Форматирование должно либо автоматически исправлять, либо чётко фиксировать ожидания (“код обязан быть отформатирован”). Линтер — всегда проверяет, не исправляя “стиль” втихаря (или вы явно делаете это частью команды).
Настройка ruff и форматирования без конфликтов
Сначала определим единый набор правил. Всё удобно хранить в pyproject.toml.
Пример pyproject.toml
[tool.ruff]
target-version = "py311"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "W", "I"] # пример: ошибки/flake8-совместимые + imports
ignore = []
# Правила можно настраивать точнее, но главное — договориться команде.
# Например, можно явно отключать то, что вас не волнует.
# [tool.ruff.lint.per-file-ignores]
# "tests/*" = ["S101"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
Что важно:
- Одна конфигурация для команды.
pyproject.tomlстановится “источником правды”. - Один форматтер. Если вы используете
ruff format, не добавляйте альтернативный форматтер, который будет менять то же самое (или делайте это осознанно и не в одной итерации). - Одинаковая версия ruff в команде. Иначе одна и та же конфигурация может давать разные результаты форматирования/линтинга.
Если команда ещё не уверена в наборах правил ruff, лучше начать с “меньшего” набора и накапливать опыт. Принцип такой: сначала устранить шум (format + базовые ошибки), затем расширять правила до “качества, которое реально защищает от багов”.
Настройка pre-commit: делаем хук, который не превращает жизнь в ад
pre-commit управляет цепочкой проверок. Он поддерживает хуки, которые могут исправлять файлы (например, форматтер). В этом случае pre-commit обычно делает так: обнаружил несоответствие — запустил инструмент — применил изменения — повторно проверил.
Базовый .pre-commit-config.yaml
Пример для ruff:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff-format
args: ["--check"]
- id: ruff
args: ["--fix"]
# Важно: decide whether you want auto-fix in linters.
# Если включить --fix, ruff будет править часть проблем.
# Это снижает ручную работу, но может менять код неожиданно.
Разберём нюансы:
ruff-formatс--checkозначает: форматтер проверяет, что файл уже отформатирован. В зависимости от конфигурации pre-commit и конкретного хука может быть режим авто-исправления. Если вам нужно именно “почини сам”, обычно убирают--checkи позволяют хуку форматировать.ruff --fix— полезная опция, если вы доверяете автоматическим исправлениям. Но если команда хочет минимизировать изменения на коммите (и обсуждать правки осмысленно), можно отключить--fixи оставить только диагностику.- Версию
revлучше закреплять. Иначе через месяц другой участник может получить другой формат.
Практический компромисс, который часто хорошо работает в командах:
- форматирование всегда выполняется или проверяется в строгом режиме,
- линтер обычно диагностирует, а авто-фикс включают только для безопасных правок (в ruff это более тонко настраивается, но концепт такой).
Когда форматтер должен быть “check”, а когда “fix”
Выбор влияет на пользовательский опыт:
- Форматтер “check”: разработчик получает ошибку, если формат не соответствует; коммит блокируется. Плюс — меньше неожиданных изменений в коммите. Минус — нужно запускать форматтер вручную или иметь отдельный “шаг” перед коммитом.
- Форматтер “fix”: pre-commit сам меняет файлы и коммит в итоге проходит. Плюс — меньше лишних команд. Минус — разработчик может быть удивлён тем, что его “исходник” изменился.
Для команды, которая хочет минимизировать спор о стиле и уменьшить ручную работу, обычно выигрывает режим, когда форматтер применяет правки (а линтер уже проверяет результат).
Если хотите именно управлять процессом, делайте явную дисциплину:
- “Мы форматируем всегда до линтинга”.
- “После pre-commit всё уже в консистентном виде”.
Автообнаружение файлов и скорость
pre-commit по умолчанию анализирует только затронутые файлы, что делает локальную проверку быстрой. Но в CI вы всё равно хотите прогнать всё репозиторий целиком, иначе вы можете пропустить “древние” несоответствия.
CI: как гарантировать, что в PR нет сюрпризов
CI должен быть “скептиком”: если локальный пайплайн должен привести код к стандарту, CI проверяет, что стандарт действительно соблюдён.
Пример GitHub Actions
name: quality
on:
pull_request:
push:
branches: [ "main" ]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Ruff format check
run: ruff format --check .
- name: Ruff lint
run: ruff check .
Что стоит отметить:
ruff format --check .гарантирует, что в репозитории нет “неотформатированных” файлов.ruff check .выполняет линтинг.
Если вы хотите добавить автоматические исправления в CI, обычно это плохая идея: CI не должен превращаться в “робота, который коммитит обратно”. Лучше заблокировать PR и попросить привести код в соответствие. Это устраняет часть конфликтов в процессе ревью.
Важность кэширования и версий
Чтобы CI не был слишком медленным, обычно добавляют кэш пакетов или используют “замороженные” окружения. Но первично — воспроизводимость. Версия ruff в CI должна совпадать с версией в pre-commit.
Если вы держите ruff как зависимость в проекте, можно закрепить версию в requirements-dev или pyproject (через tool.poetry/uv/etc.) и использовать её в обоих местах.
Договорённость о стиле: как избежать “войны правил”
Самая частая ошибка — “давайте включим всё самое строгое”. В итоге появляются тысячи замечаний, и команда перестаёт доверять инструментам. Лучше строить стиль как систему, которую легко поддерживать.
Практика: принцип “миграции к стандарту”
Есть два сценария:
- Зелёное поле: проект новый или вы начинаете с нуля — включаете проверки сразу.
- Унаследованный код: есть большой объём файлов со старым стилем — мигрируете постепенно.
Для второго сценария типичный подход:
- Сначала включаете форматирование и автоматически приводите репозиторий к единому виду (один раз).
- Затем запускаете линтер с минимальным набором правил, которые не ломают архитектурные решения.
- После того, как шум ушёл, расширяете правила постепенно.
Это особенно важно для ruff: его “select”/“ignore” позволяет контролировать степень строгости.
Командные правила: фиксируйте не “мнения”, а параметры
Ваша цель — превратить обсуждение “как принято оформлять” в чтение конфигурации:
line-lengthи правила переносов,- семейство правил (какие ошибки ловим, какие предупреждения игнорируем),
- параметры форматирования (кавычки, отступы, etc.).
Когда обсуждение сходит на нет, остается только “что написано в pyproject.toml”. Это снимает эмоциональную часть споров.
Противоречия: когда ruff ругается на то, что сам форматтер сделал
Такое бывает, если:
- вы используете разные инструменты форматирования (black/yapf/autopep8),
- в линтере включены правила, которые не совпадают с форматтером,
- версии инструментов расходятся.
Решение:
- используйте один форматтер (например,
ruff format), - линтер настраивайте так, чтобы он не конфликтовал с форматом (обычно это автоматически достигается, когда оба инструмента от ruff),
- закрепляйте версии.
Если всё равно видите конфликт, полезно сделать репродукцию: один файл, команда ruff format и ruff check в чистом окружении. Тогда становится ясно, что именно идёт “в разрез”.
Политика авто-исправлений: сколько “магии” допустимо
Опыт подсказывает: авто-фикс в pre-commit — это хорошо, если он:
- делает небольшие и ожидаемые правки (import, простые замены, приведение к стилю),
- не меняет поведение программы.
Но даже если вы уверены, что авто-фикс безопасен, в команде должно быть правило, как рассматривать изменения:
- Если авто-фикс включён, разработчик может не заметить, что файл изменился.
- Ревьюеру придётся проверять дифф, который кажется “не по делу”.
Компромисс:
- форматтер пусть правит (или пусть строго проверяет),
- линтер пусть чаще диагностирует, а
--fixвключайте только на “безопасные” наборы.
В ruff можно тонко регулировать, что считать фиксируемым, но концептуально команда должна решить: “мы доверяем автоматическим исправлениям, но в пределах разумного”.
Практический чеклист внедрения в команду
Ниже — последовательность, которая помогает избежать типичных “не взлетело”.
Шаг 1. Закрепите версии инструментов
- Выберите версию ruff.
- Используйте её и в
pre-commit(черезrev), и в CI (через зависимость или явныйpip install ruff==...).
Шаг 2. Настройте единый pyproject.toml
- Правила линтинга и форматирования — там.
- Не храните часть настроек в отдельных местах.
Шаг 3. Приведите код к стандарту (если есть legacy)
- Запустите
ruff formatи при необходимостиruff check --fix. - Коммитьте результат отдельным PR/коммитом, чтобы потом не было “трудно понять, что изменилось”.
Шаг 4. Включите pre-commit и договоритесь о политике фиксов
- Форматирование: либо “fix”, либо “check”, но согласованно.
- Линтер: “diagnose” и/или “auto-fix” — осознанно.
Шаг 5. Настройте CI как репликацию локальной логики
- В CI прогоняйте те же шаги: формат check + lint check.
- CI не должен “молчаливо” использовать другой набор команд.
Шаг 6. Настройте коммуникацию
Даже лучший пайплайн не работает, если разработчики не понимают, что делать при красном статусе:
- Введите правило: “если pre-commit не прошёл — запусти форматтер/линтер локально”.
- Дайте короткий документ: команды и порядок.
Как это выглядит для разработчика: поведенческая модель
Представим процесс:
- Разработчик меняет код.
- Делает commit.
- pre-commit:
- форматирует/или проверяет формат,
- запускает ruff check,
- блокирует коммит при несоответствиях.
- В PR CI повторяет:
ruff format --check,ruff check.
Результат:
- Дифф в PR почти всегда “чистый” и связан с реальными изменениями, а не с ручной правкой стиля.
- Команда перестаёт спорить о кавычках и отступах: это вопрос автоматики.
- Линтер концентрируется на том, что действительно может быть проблемой.
Типичные ошибки и как их избежать
Ошибка 1. Два форматтера в проекте
Например, ruff format + black. Итог — циклы правок. Решение: оставьте один форматтер, второй уберите.
Ошибка 2. CI проверяет одно, а pre-commit другое
Например, в pre-commit включены дополнительные правила, а CI использует базовый ruff check . без тех
Комментарии
Пока нет комментариев