Конвенция коммитов и семантическое версионирование
Урок про то, как превратить сообщения коммитов в машиночитаемый формат и автоматически считать из них версию и changelog.
Conventional Commits — соглашение о структуре сообщения коммита (
тип(область): описание), которое позволяет инструментам понимать смысл изменения и автоматизировать версии и журнал изменений.
Зачем формализовать сообщения коммитов
Сообщение «fix stuff» бесполезно: по нему нельзя ни понять, что менялось, ни решить, как поднимать версию, ни собрать changelog. Через полгода даже сам автор не вспомнит, что скрывается за «правки» или «обновил». А когда таких коммитов в истории тысячи, проект теряет память о себе: расследование инцидента превращается в чтение диффов вслепую. Conventional Commits задаёт жёсткий, но простой формат, который читается и человеком, и скриптом. Главная выгода — автоматизация: инструмент сам определяет следующий номер версии и генерирует журнал изменений, а люди перестают спорить «это minor или patch?». Дополнительный бонус — дисциплина мышления: чтобы выбрать тип, разработчик вынужден честно ответить себе, что именно он делает — добавляет возможность, чинит баг или просто переставляет код. Часто на этом этапе и обнаруживается, что в один коммит свалено три разных по смыслу изменения.
Формат сообщения
Структура такая:
<тип>(<область>): <краткое описание>
<тело: что и зачем>
<футер: BREAKING CHANGE / ссылки на задачи>
Заголовок обязателен, область и тело — нет. Область в скобках уточняет, какой части системы касается изменение (auth, cart, api), и помогает группировать записи в changelog. Тело раскрывает мотивацию — почему понадобилось изменение, а не пересказывает дифф. Футер хранит метаданные: пометку о ломающем изменении и ссылки на задачи (Closes #42). Заголовок принято держать коротким (около 50 символов) и в повелительном наклонении — «добавить», «исправить», как будто вы продолжаете фразу «этот коммит, если применить, …». Основные типы:
| Тип | Смысл | Влияние на версию |
feat | новая функциональность | MINOR |
fix | исправление бага | PATCH |
docs | только документация | — |
refactor | правка без смены поведения | — |
test | тесты | — |
chore | сборка, зависимости, рутина | — |
perf | оптимизация | PATCH |
Примеры реальных сообщений:
git commit -m "feat(auth): добавить вход через Google"
git commit -m "fix(cart): не терять скидку при смене валюты"
git commit -m "docs(readme): описать переменные окружения"
git commit -m "refactor(api): вынести валидацию в отдельный модуль"
Связь с semantic versioning
Semver — это формат версии MAJOR.MINOR.PATCH (например, 2.4.1) со строгим смыслом каждого числа:
- MAJOR — несовместимое изменение, ломающее существующий код пользователей.
- MINOR — новая возможность, обратно совместимая.
- PATCH — исправление, не меняющее API.
Главный практический смысл semver — предсказуемость обновлений. Увидев, что у зависимости вышла версия 2.4.2 вместо 2.4.1, вы понимаете: это только багфикс, обновляться безопасно. А скачок до 3.0.0 — повод насторожиться: что-то сломали, нужно читать changelog. Менеджеры пакетов опираются на это в диапазонах версий: запись ^2.4.0 означает «бери любые minor и patch, но не перескакивай на новый major», то есть «давай новое, но не ломай меня». Conventional Commits отображается на semver почти один-в-один: fix → бамп PATCH, feat → бамп MINOR, а ломающее изменение → бамп MAJOR. Ломающее изменение помечают либо восклицательным знаком после типа, либо футером BREAKING CHANGE:.
git commit -m "feat(api)!: переименовать поле userId в accountId"
# или развёрнуто:
git commit -m "feat(api): переименовать userId в accountId
BREAKING CHANGE: клиенты, читавшие userId, должны перейти на accountId"
Покажем правило бампа кодом — функция берёт текущую версию и тип изменения и возвращает следующую:
def bump(version, kind):
major, minor, patch = (int(x) for x in version.split("."))
if kind == "breaking":
return f"{major + 1}.0.0"
if kind == "feat":
return f"{major}.{minor + 1}.0"
if kind == "fix":
return f"{major}.{minor}.{patch + 1}"
return version
v = "1.4.2"
for k in ["fix", "feat", "breaking"]:
v = bump(v, k)
print(f"{k:9} -> {v}")
Вывод:
fix -> 1.4.3 feat -> 1.5.0 breaking -> 2.0.0
Обратите внимание: feat обнуляет PATCH, а breaking обнуляет и MINOR, и PATCH — это требование semver.
Автогенерация changelog
Раз каждый коммит размечен типом, журнал изменений собирается автоматически — и это, пожалуй, самая ощутимая выгода всей конвенции. Раньше changelog писали вручную, забывали, привирали; теперь он становится точным побочным продуктом нормальной работы. Инструменты вроде standard-version, semantic-release или commitizen читают коммиты с прошлого тега, группируют их по типам и пишут CHANGELOG.md, ставят новый git-тег и (в CI) публикуют релиз. Связка с feat/fix здесь ключевая: разделы «Features» и «Bug Fixes» в журнале — это ровно ваши коммиты соответствующих типов. Многие команды добавляют ещё и хук commit-msg (через commitlint), который проверяет формат сообщения прямо в момент коммита и не даёт записать что-то вне конвенции — так формат соблюдается не на честном слове, а технически.
# standard-version: посчитать версию из коммитов, обновить CHANGELOG, поставить тег
npx standard-version
# semantic-release в CI: полностью автоматический релиз
npx semantic-release
Готовый раздел changelog выглядит так:
## 1.5.0 (2026-06-27)
### Features
* **auth:** добавить вход через Google
### Bug Fixes
* **cart:** не терять скидку при смене валюты
Как это работает под капотом
Инструмент берёт диапазон коммитов последний-тег..HEAD, парсит каждое сообщение регуляркой по шаблону тип(область): описание и определяет максимальный «уровень» изменения среди всех коммитов: есть хоть один breaking → MAJOR, иначе есть feat → MINOR, иначе есть fix → PATCH. Затем считает новый номер, дописывает в CHANGELOG.md сгруппированные записи и создаёт аннотированный тег vX.Y.Z. Никакой магии — просто строгий формат делает сообщения парсимыми.
Частые ошибки
- Свободный текст в типе.
fixed:илиFeature:не распознаются — типы пишутся строчными и из фиксированного списка. - Забыли пометить breaking change. Сломали API, но закоммитили как
feat— инструмент поднимет MINOR вместо MAJOR, и пользователи получат сломанное обновление как «безопасное». - Свалка в одном коммите. Коммит «feat + fix + рефакторинг» нельзя корректно отнести к одному типу; делайте атомарные коммиты.
- Описание с большой буквы или с точкой. Конвенция предписывает повелительное наклонение в нижнем регистре без точки: «добавить», а не «Добавил.».
- Ручная правка версии. Если вы вручную правите номер в обход инструмента, автоматический расчёт рассинхронизируется с тегами.
Итоги
- Conventional Commits — формат
тип(область): описание, понятный и людям, и скриптам. - Маппинг на semver:
fix→ PATCH,feat→ MINOR, breaking change → MAJOR. - Breaking помечают
!после типа или футеромBREAKING CHANGE:. - Размеченные коммиты позволяют автоматически считать версию, ставить тег и генерировать changelog.
- Атомарные коммиты и точные типы — обязательное условие, иначе автоматика врёт.