Changelog по Keep a Changelog: как писать так, чтобы его читали
Релиз 2.4.0. В changelog написано: «bug fixes and improvements». Пользователь обновляется, у него ломается интеграция, он идёт смотреть, что поменялось — и видит ту же строку. В тикете «что вы поменяли в API?» инженер тратит полдня, пролистывая коммиты между двумя тегами, потому что нормального лога изменений нет.
Changelog — это не файл для отчётности перед менеджментом. Это документ, по которому пользователь решает: ставить обновление, ждать, читать миграцию. Если он бесполезен — его не читают, а если его не читают, апдейты накапливаются и боль от перехода на новую major растёт. Разберу формат Keep a Changelog, как он работает на практике, и какие частые ошибки делают changelog бесполезным.
Что такое Keep a Changelog
Это соглашение, описанное на keepachangelog.com. Файл CHANGELOG.md в корне репозитория, обратная хронология (свежее сверху), фиксированный набор секций для каждого релиза.
Шесть секций, ничего не выдумывай:
- Added — новые фичи.
- Changed — изменения в существующем поведении.
- Deprecated — то, что в скором времени удалят.
- Removed — удалённые фичи.
- Fixed — исправления багов.
- Security — фиксы уязвимостей.
Внутри каждого релиза заголовок — версия и дата по ISO. На самом верху — секция [Unreleased], куда коммитят изменения по мере работы, и которую при релизе превращают в очередную версию.
Минимальный пример
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New `--format` flag for the `export` command, supports `json` and `csv`.
## [2.4.0] - 2026-05-15
### Added
- Streaming mode for files larger than 1 GB.
- `--retry` option for the `upload` command, with exponential backoff.
### Changed
- `parse()` now returns `Result<T, ParseError>` instead of throwing.
Callers must handle the `Err` case.
- Default timeout for HTTP client increased from 5s to 30s.
### Fixed
- Race condition in concurrent writes that caused data loss on busy filesystems.
- `--config` flag now correctly overrides values from `~/.config/tool.toml`.
## [2.3.1] - 2026-04-22
### Security
- Updated `libxml2` to 2.12.5 to fix CVE-2024-25062.
Это работает. Пользователь читает четыре строки и понимает, переходить или нет.
Как писать запись об изменении
Самый частый провал — записи в стиле «refactored auth module» или «improved performance». Для пользователя обе эти строки бесполезны.
Было:
- Refactored authentication module.
- Improved performance.
- Various bug fixes.
Стало:
- Auth tokens are now JWT instead of opaque strings. Existing sessions remain valid until expiration.
- Bulk import is 4x faster on files over 100k rows due to streaming JSON parsing.
- Fixed crash when uploading files with non-ASCII names on Windows.
Правило одной строки: запись должна отвечать «что поменялось для меня как пользователя API/инструмента». Если нельзя ответить — это не запись для changelog, это запись для коммита, и она там и должна остаться.
Технические детали реализации (какой класс отрефакторили, какой паттерн применили) — не нужны. Они интересны автору и трём контрибьюторам, но не пятнадцати тысячам пользователей.
Breaking changes — отдельная история
Любое breaking change должно быть видно издалека. В Keep a Changelog оно попадает в секцию Changed или Removed, но я обычно дополнительно помечаю его маркером BREAKING:.
## [3.0.0] - 2026-06-01
### Removed
- **BREAKING:** Removed deprecated `legacyMode` option.
Migration: remove the option, default behavior is now `strict` mode.
### Changed
- **BREAKING:** `connect()` is now async and returns a Promise.
Migration: add `await` before all calls. Synchronous fallback removed.
- Default log level changed from `info` to `warn`. Set `LOG_LEVEL=info` to restore.
Каждое breaking change — с инструкцией, как мигрировать. Не «see migration guide», а конкретный one-liner. Полный гайд — отдельным документом, но в changelog обязательна короткая подсказка.
Если breaking changes больше пяти — release notes выносятся в отдельную страницу, на которую CHANGELOG.md ссылается. Тысячестрочный changelog с подробным разбором миграций становится нечитаемым.
Связь с SemVer
Keep a Changelog хорошо работает только в паре с Semantic Versioning. Иначе пользователь не знает, что ждать от обновления.
- Patch (X.Y.Z): только Fixed и Security. Никаких Added или Changed с поведением.
- Minor (X.Y.0): Added и совместимые Changed. Никаких Removed.
- Major (X.0.0): Removed, Breaking-Changed.
Если в minor-релизе есть Removed — это нарушение SemVer. Changelog в этот момент полезен: рецензент его читает на ревью PR-а в main и видит, что «здесь же breaking, мы не можем выпустить как 2.5».
Где живёт changelog
Стандарт — CHANGELOG.md в корне репозитория. Другие места, которые встречаются:
- GitHub Releases. Удобно для пользователей, которые подписаны на релизы. Но не работает offline и не лежит в репозитории, если репозиторий клонировали из зеркала.
- Отдельный сайт документации. Хорошо для крупных продуктов с богатым контентом. Но требует поддержки и обычно отстаёт от главного
CHANGELOG.md. - Раздел в README. Только для совсем маленьких проектов с парой релизов. Дальше README распухает.
Лучшее решение — CHANGELOG.md как источник правды, GitHub Release с тем же содержимым (можно автоматизировать), и раздел на сайте доков, генерируемый из CHANGELOG.md.
Автогенерация changelog
Соблазн: «давай парсить коммиты и собирать changelog автоматически». Инструменты есть: conventional-changelog, git-cliff, release-please. Они работают, если коммиты следуют Conventional Commits (feat:, fix:, BREAKING CHANGE:).
Что в этом хорошо:
- Не пропустишь изменение — оно автоматически попадает в лог.
- Не нужно отдельно писать запись в changelog при PR.
- Версии бампятся автоматически по правилам Conventional Commits.
Что в этом плохо:
- Сообщения коммитов пишут разработчики для разработчиков. Они не отвечают на вопрос «что поменялось для пользователя».
- На выходе получаешь «список изменений», а не «changelog для людей».
- Один PR = один коммит в логе, даже если в нём пять разных правок.
На моей практике гибрид работает лучше всего: автогенерация делает черновик, человек перед релизом проходит по списку и переписывает записи в человеческие. Сэкономленное время — сборка, а не редактура.
Что должно попадать в changelog
- Изменения публичного API: новые/изменённые/удалённые endpoints, функции, флаги, опции.
- Изменения форматов конфигов и данных.
- Изменения в зависимостях, которые видит пользователь (новый минимум версии Node, Python).
- Изменения поведения по умолчанию (default values, default flags).
- Все security-фиксы, даже если CVE ещё не присвоен.
Что в changelog не нужно
- Внутренние рефакторинги без изменения поведения.
- Изменения в тестах.
- Изменения в CI, релиз-инфраструктуре, скриптах разработки.
- Обновления версий dev-зависимостей.
- Косметические правки в комментариях, форматировании.
Если возникает желание добавить что-то из этого списка — спроси: «изменит ли это что-то для человека, который только использует продукт?» Если нет — в changelog оно не нужно.
Процесс обновления changelog
Самый рабочий процесс, который я видела:
- Каждый PR, меняющий поведение, добавляет запись в секцию
[Unreleased]. Это требование PR-шаблона и проверяется на ревью. - При релизе секция
[Unreleased]переименовывается в[X.Y.Z] - YYYY-MM-DD. - Создаётся новая пустая
[Unreleased]сверху. - Тег ставится на коммит, в котором это произошло.
- CI вытаскивает соответствующий блок из
CHANGELOG.mdи публикует как описание GitHub Release.
Скрипт извлечения блока — десять строк на любом скрипт-языке, ищет ## [X.Y.Z] и берёт всё до следующего ##. Не нужно тащить в проект отдельную тулзу для этого.
Антипаттерны, которые встречаю чаще всего
- «Bug fixes and improvements». Это не запись, это извинение за нежелание писать changelog.
- Прямые ссылки на коммиты вместо описания. «Fixed:
abc123». Пользователь не должен читать diff, чтобы понять, что поменялось. - Слишком технические записи. «Refactored DI container to use Lifetimes<T>». Пользователю всё равно.
- Выборочный changelog. «Major изменения у нас есть, минорные не вписали». Тогда это не changelog, это анонс.
- Отсутствие даты релиза. Без даты сложно сопоставить с issue в трекере.
- Пустая секция вида «### Added» без записей. Лучше удалить заголовок, чем оставлять пустоту.
Чек-лист хорошего changelog
- Файл
CHANGELOG.mdв корне репозитория, обратная хронология. - Секция
[Unreleased]сверху, заполняется по мере коммитов. - Каждый релиз — заголовок с версией по SemVer и датой.
- Записи в фиксированных секциях: Added/Changed/Deprecated/Removed/Fixed/Security.
- Каждая запись — одна-две строки, отвечает «что это значит для пользователя».
- Все breaking changes помечены явно, с миграционной подсказкой.
- Security-фиксы вынесены в отдельную секцию.
- Версии минимальных зависимостей упомянуты, если они менялись.
- Нет внутренних рефакторингов и изменений CI.
- Файл генерируется не из ничего — есть процесс на каждом PR.
Хороший changelog экономит часы на ручном расследовании «что у нас изменилось между 2.3 и 2.7». Плохой — превращает каждое обновление в отдельный спринт. Это документ небольшой по объёму, но именно тот, к которому пользователь возвращается чаще всего после релиза. Стоит написать его так, как сам бы хотел читать.