Conventional commits: feat, fix, chore и автоматизация
Открой git log любого крупного open-source проекта — Airflow, dbt, Kubernetes, React, Vue. Заметишь pattern: commit messages выглядят как feat: add support for X, fix: handle null case in Y, chore(deps): bump pandas to 2.2.0. Это не случайность — это Conventional Commits, формальный стандарт сообщений, который превращает git log в structured database.
В этом уроке: что такое conventional commits, зачем DE команды переходят на этот стандарт, какие типы существуют, как валидировать через commitlint, и как automated tools (semantic-release) превращают такие commits в CHANGELOG и автоматические релизы.
Зачем нужен стандарт
Стандартный git log без conventional commits:
abc123 fixed bug
def456 wip
789012 changes
345678 more changes
9abcde merge
Через месяц никто не помнит, что именно “fixed bug” или “changes”. CHANGELOG генерировать вручную — времязатратно. Понимать impact релиза — невозможно. Версии bumping — ad hoc.
Conventional commits превращают git log в:
feat: add transactions aggregation pipeline
fix: handle DST edge case in date_to_partition
chore(deps): bump dbt-core to 1.8.0
docs: update README with new env vars
refactor: extract common config to utils/
test: add integration tests for s3_to_warehouse
Теперь:
- Любой commit при беглом взгляде понятен:
feat:— новый функционал,fix:— bugfix,chore:— обслуживание - CHANGELOG генерится автоматически: “v1.5.0 — feat: add…, feat: support… ; fix: …”
- Версия определяется по типам: feat -> minor bump, fix -> patch, BREAKING CHANGE -> major
- Code review проще: PR с only
chore:— низкий риск, сfeat:— нужно внимательно
В Airflow, dbt, и других DE-flagship проектах conventional commits — обязательны. PR без правильного формата заголовка не мерджится. Это стандарт индустрии.
Формат: type, scope, description
Базовая структура:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Минимальный пример:
feat: add aggregation function
С scope-ом и body:
feat(transactions): aggregate by category daily
Adds a new aggregation step in the pipeline that groups
transactions by category and computes daily sums.
Refs: JIRA-1234
С breaking change:
feat(api)!: change response format for /aggregations
BREAKING CHANGE: response now returns array instead of dict.
Clients need to update parsing logic.
! после type/scope = signal breaking change. Major version bump.
Канонические типы
Полный канонический список типов: feat, fix, chore, docs, refactor, test, style, perf, build, ci, revert.
Каждая команда может расширять (например, добавить data: для data updates, migration: для DB-миграций), но базовые 11 типов — стандарт.
Scope: добавляет контекст
scope (в скобках после type) указывает на компонент:
feat(api): add /aggregations endpoint
fix(parser): handle null timestamps
chore(deps): bump airflow to 2.9
docs(readme): update onboarding section
test(s3): add multi-region tests
В DE-проекте scopes обычно:
- Названия modules:
parser,api,transformers - Названия pipelines:
transactions,users,events - Layer:
dbt,airflow,etl - Группы:
depsдля зависимостей
Scope опционален. Если коммит затрагивает несколько scope-ов или непонятный — можно опустить.
Body и footer
Длинный коммит:
feat(transactions): add daily aggregation pipeline
Implements aggregation step that groups transactions by
category and computes daily sums. Uses pandas instead of
SQL for cross-source flexibility.
Closes JIRA-1234
Co-authored-by: Alice <[email protected]>
Body — multi-line description, объясняет почему изменения, не что (что — видно в diff).
Footer — служебная информация:
Closes #123илиRefs JIRA-1234— связь с тикетамиCo-authored-by:— для нескольких авторовBREAKING CHANGE: <description>— обязательно для major bump
Валидация через commitlint
Чтобы commit messages в команде следовали стандарту, используется commitlint — Node.js утилита, которая проверяет format. Через pre-commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.16.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ['@commitlint/config-conventional']
Этот hook запускается на commit-msg stage (после написания message, перед commit), и валидирует формат. Если не conventional — commit отменён.
Конфиг commitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'build', 'ci', 'chore', 'revert',
'data', // custom — для DE
]],
'subject-case': [2, 'never', ['upper-case']],
'header-max-length': [2, 'always', 72],
},
};
extends: '@commitlint/config-conventional' — стандартные правила (allowed types, format). rules — переопределения.
После pre-commit install:
$ git commit -m "fixed bug"
[INFO] Initializing environment for commitlint
- subject may not be empty [subject-empty]
- type may not be empty [type-empty]
# Commit отменён
$ git commit -m "fix: handle null timestamp in parser"
# Hooks passed
commitizen: guided commit
Если коллеги не привыкли к format, есть commitizen — CLI, который задаёт вопросы и формирует правильный commit:
pip install commitizen
# или
npm install -g commitizen
# Вместо git commit
$ cz commit
? Select the type of change you are committing:
> fix: A bug fix
feat: A new feature
docs: Documentation only changes
style: Code style change
...
? What is the scope of this change? (e.g., parser, api): parser
? Write a short description: handle null timestamp
? Provide a longer description: (optional)
The parser was failing when timestamp was None.
Now it falls back to '1970-01-01' for null cases.
? Are there any breaking changes? No
? Does this change affect any open issues? Yes
? If issues are closed, the commit requires a body. Please enter a longer description: ...
? Add issue references: Closes JIRA-1234
# commitizen формирует:
fix(parser): handle null timestamp
The parser was failing when timestamp was None.
Now it falls back to '1970-01-01' for null cases.
Closes JIRA-1234
Guided commit полезен для onboarding-периода. Через месяц коллеги привыкают и пишут вручную.
CHANGELOG generation
Главная польза conventional commits — автоматический CHANGELOG. Tool — conventional-changelog или git-cliff (Rust, быстрый):
# Установка git-cliff
brew install git-cliff
# или
cargo install git-cliff
# Сгенерировать CHANGELOG из commits
git cliff -o CHANGELOG.md
# Сгенерировать для конкретного релиза (между tags)
git cliff v1.0.0..v1.1.0 -o CHANGELOG-v1.1.0.md
Результат:
# Changelog
## [1.1.0] - 2026-05-13
### Features
- **(transactions)** add daily aggregation pipeline (#123)
- **(api)** add /aggregations endpoint (#125)
### Bug Fixes
- **(parser)** handle null timestamp (#127)
- **(s3)** retry on transient errors (#128)
### Chores
- **(deps)** bump dbt-core to 1.8.0 (#130)
- **(deps)** bump airflow to 2.9.1 (#131)
Все commit messages автоматически собрались в structured CHANGELOG. Никакой ручной работы.
semantic-release: auto-versioning
Дальше — automatic version bumping на основе commits. semantic-release — JS-tool, который читает commits с последнего тега, определяет тип релиза, bumps version, генерит CHANGELOG, делает Git tag, релиз на GitHub:
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: {fetch-depth: 0}
- uses: actions/setup-node@v4
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
После merge PR в main:
- semantic-release анализирует commits с прошлого release
- Если есть
feat:— bumps MINOR (1.0.0 -> 1.1.0) - Если только
fix:— bumps PATCH (1.0.0 -> 1.0.1) - Если есть
BREAKING CHANGE:— bumps MAJOR (1.0.0 -> 2.0.0) - Генерит CHANGELOG, commit-ит
- Создаёт Git tag (
v1.1.0) - Создаёт GitHub Release с release notes
Это автоматизация end-to-end: написал conventional commit -> merge -> новый релиз.
Для DE это особенно полезно в shared libraries — общие helpers, schemas, dbt macros packaged как pip/npm. Версии bumping автоматически, юзеры просто pip install новую версию.
Для airflow DAG проекта без релизов automatic versioning не критичен. Но conventional commits полезны просто для читаемого git log. Минимальный setup — commitlint через pre-commit.
Real-world examples
Реальные commits из крупных DE проектов:
Apache Airflow:
feat(api): Add update_mask to PATCH connections endpoint (#37874)
fix(scheduler): Properly handle datasets after dag run finishes (#37879)
chore(deps): bump cryptography from 41.0.7 to 42.0.4 (#37876)
docs(providers): Update Azure provider docs structure (#37862)
dbt-core:
feat: Add new Snowflake auth methods (#5879)
fix: Resolve table comparison error in incremental models (#5884)
chore(deps): bump httpx to 0.27.0 (#5891)
test: Add integration tests for Redshift (#5887)
Видно, что format строгий, scopes informative. PR title часто синхронизирован с commit message (или с первой строкой).
Junior DE rules of thumb
Самое практичное для DE-junior:
- Тип всегда указывай —
feat:,fix:,chore:,docs:минимум. - Lowercase description —
feat: add X, неfeat: Add X(не критично, но canonical). - Imperative mood — “add X”, не “added X” или “adds X” (как в git log официальном).
- Header до 72 символов — fit в один экран git log.
- Не используй capslock или эмодзи — это шум в structured tooling.
Антипатаerns (так не надо):
Updated stuff(no type, vague)WIP(тип не указан, не информативно)Fix(no description)feat: ADD AMAZING NEW FEATURE!!!!1!(capslock, exclamations)feat: implemented the new pipeline that aggregates by category daily with support for many edge cases and timezone handling, also fixed some bugs(header слишком длинный)
Хорошие примеры:
feat(transactions): add daily aggregation by categoryfix(parser): handle DST edge case in to_partition()chore(deps): bump dbt-core to 1.8.0docs: clarify env vars in README
Hands-on: настроить commitlint
# В тестовом репо
mkdir cc-demo && cd cc-demo
git init
pip install pre-commit
# Создать .pre-commit-config.yaml с commitlint
cat > .pre-commit-config.yaml <<'EOF'
repos:
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.16.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ['@commitlint/config-conventional']
EOF
# Создать commitlint.config.js
cat > commitlint.config.js <<'EOF'
module.exports = {
extends: ['@commitlint/config-conventional'],
};
EOF
# Install hooks
pre-commit install --hook-type commit-msg
# Test 1: bad message
echo "x=1" > main.py
git add main.py
git commit -m "fixed bug"
# commitlint Failed
# - subject may not be empty
# - type may not be empty
# Test 2: good message
git commit -m "fix: handle x edge case"
# Passed
Дальше команда привыкает писать в правильном формате. Через месяц станет автоматическим.
Cross-link
- Урок 02 — pre-commit framework (база для commitlint hook)
- Урок 04 — alternative frameworks (husky тоже умеет commitlint)
- Модуль 11 — Pull Requests; PR title часто использует тот же формат
- Модуль 18 — CI/CD; semantic-release запускается в GitHub Actions
CI/CD для DE: автоматизация пайплайнов и деплоя