CHANGELOG.md: что в него писать и как держать актуальным
5 мин чтения
CHANGELOG.md это Markdown-файл в корне репозитория, который для каждой выпущенной версии перечисляет, что изменилось, в терминах, с которыми пользователь проекта может что-то сделать. Файл читают те, кто решает, обновляться ли, те, кто отлаживает после обновления, и реестры пакетов и инструменты релизов, которые показывают его на странице проекта. Формат, которому следует большинство проектов, описан на keepachangelog.com: новейшая версия сверху, над ней раздел Unreleased, а записи сгруппированы под Added, Changed, Deprecated, Removed, Fixed и Security.
| Раздел | Что в нём | Пример строки |
|---|---|---|
Unreleased | Изменения влиты, но ещё не вошли в версию | - Экспорт теперь потоковый, файлы больше 100 МБ не отваливаются по таймауту. |
Added | Новая возможность, доступная пользователю | - Флаг \—dry-run` у `deploy` печатает план без применения.` |
Changed | Существующее поведение теперь работает иначе | - Таймаут по умолчанию поднят с 30 с до 60 с. |
Deprecated | Ещё работает, запланировано к удалению | - \legacyAuth()` будет удалён в 3.0; используйте `auth()`.` |
Removed | Исчезло в этой версии | - Убрана поддержка Node 16. |
Fixed | Баг, с которым пользователь мог столкнуться | - Поиск больше не падает на запросах с дефисом. |
Security | Исправления, ради которых читатель должен обновиться сегодня | - Исправлен path traversal в обработчике статических файлов (CVE ожидается). |
Что такое файл CHANGELOG.md?
Это читаемая человеком история проекта, по одной версии за раз, которая лежит рядом с кодом и поэтому поставляется вместе с ним. От git log она отличается аудиторией и отбором: git log записывает каждый коммит для тех, кто его писал, а changelog записывает изменения, которые заметит пользователь, и написан для человека, который их не писал. Коммит, переименовывающий внутреннюю переменную, строки в changelog не получает. Коммит, меняющий таймаут по умолчанию, получает, даже если дифф был в один символ.
Файл живёт в корне репозитория под именно этим именем, потому что инструменты ищут его там. GitHub ссылается на него со страницы репозитория, npm и PyPI показывают его, если он входит в пакет, а инструменты релизов вроде release-please и semantic-release читают и переписывают его на месте. Строчное changelog.md и CHANGES.md тоже подходят людям, но некоторые инструменты ищут только каноническое имя.
Как выглядит полный CHANGELOG.md?
Короткий правильный пример стоит больше длинного описания. Этот следует раскладке Keep a Changelog 1.1.0 и семантическому версионированию для номеров.
# Changelog
All notable changes to this project are listed here.
Format: Keep a Changelog. Versions: Semantic Versioning.
## [Unreleased]
### Added
- `--dry-run` flag on `deploy` prints the plan without applying it.
## [2.3.1] - 2026-10-01
### Fixed
- Search no longer fails on queries containing a hyphen. Broken
since 2.3.0.
### Security
- Fixed a path traversal in the static file handler. Upgrade today
if you serve user-uploaded files.
## [2.3.0] - 2026-09-18
### Added
- Streaming export. Files over 100 MB no longer time out.
### Deprecated
- `legacyAuth()` still works but will be removed in 3.0.
Use `auth()`.
[Unreleased]: https://github.com/acme/tool/compare/v2.3.1...HEAD
[2.3.1]: https://github.com/acme/tool/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/acme/tool/compare/v2.2.0...v2.3.0
Четыре детали несут большую часть ценности. У каждой версии есть дата в формате ISO, так что читатель может сопоставить её с тем, когда началась его проблема. Каждый заголовок версии это ссылка на сравнение, так что полный дифф в одном клике. Исправление говорит, какая версия это сломала. А раздел Security стоит отдельно, так что тот, кто ищет ответ на «нужно ли мне обновляться прямо сейчас», находит его за секунды.
Как держать CHANGELOG.md в актуальном состоянии?
Пишите строку в том же pull request, что и изменение, под Unreleased, и переносите раздел под заголовок версии при релизе. Это весь метод. Файл устаревает, когда строку пишет позже кто-то другой, в момент релиза, по списку сообщений коммитов, которые он не писал.
Две вещи закрепляют привычку. Первая это проверка в CI, которая заваливает pull request, трогающий src/, но не трогающий changelog, с меткой, чтобы пропустить её для изменений без видимого пользователю эффекта; проверка changelog как GitHub Action показывает версию на сорок строк. Вторая это шаг релиза, который переименовывает Unreleased в версию и дату одной командой, чтобы никому не нужно было помнить делать это вручную. Как команды решают вопрос, чья это работа, разбирает статья кто пишет changelog.
Нужно ли генерировать changelog из коммитов?
Генерация даёт полный черновик; она не даёт changelog. Инструменты, построенные на conventional commits, и автоматизация релизов вроде release-please читают префиксы feat:, fix: и BREAKING CHANGE: и выдают сгруппированный список со ссылкой на каждый pull request. Список хорош ровно настолько, насколько хороши темы коммитов, а темы коммитов описывают код.
Рабочая середина это сгенерировать, затем отредактировать. Пусть инструмент создаст раздел Unreleased из коммитов, а релизный pull request перепишет строки, которые будет читать пользователь: симптом вместо имени функции, настройка, которая переехала, вместо файла, который изменился. Конвейер по шагам проходит статья от conventional commits к changelog, а браузерного генератора changelog достаточно, чтобы получить черновик из вставленного git log, ничего не устанавливая.
Что не должно попадать в CHANGELOG.md?
Внутренние рефакторинги, обновления зависимостей без изменения поведения, коммиты только с тестами, конфигурация CI и правки форматирования. Их место в истории git, где их найдёт любой, кому они нужны. Их включение хоронит важные строки и приучает читателей пропускать файл.
Второе, что стоит оставить за бортом, это маркетинговая версия изменения. «Молниеносный экспорт» не говорит читателю ничего, что он мог бы проверить. «Экспорт теперь потоковый; файлы больше 100 МБ не отваливаются по таймауту» говорит. Файл это справочный документ, и тон должен соответствовать: короткие строки, конкретные существительные, версия или дата на всём. Части спецификации, которые команды чаще всего опускают, разбирает статья Keep a Changelog на практике, а когда JSON- или YAML-источник рядом с Markdown-файлом себя оправдывает, объясняет статья форматы файла changelog.
Нужен ли файл продукту, у которого есть страница changelog?
Библиотеке нужен, потому что файл это то, что показывают реестры пакетов и что разработчик читает офлайн. SaaS-продукт без устанавливаемого артефакта может обойтись одной страницей, поскольку его пользователи никогда не видят репозиторий. Многие команды держат оба: файл как инженерную запись и публичную страницу changelog, которая пишется по нему для более широкой аудитории, с разделением деталей так, как описывает статья changelog vs release notes. Если страница питается от файла, оставьте файл источником истины и генерируйте страницу, чтобы редактировать было в одном месте.
FAQ
В чём разница между CHANGELOG.md и release notes? Changelog это полная непрерывная запись каждого заметного изменения, хранящаяся в репозитории. Release notes это сообщение об одном релизе, часто короче и переписанное для более широкой аудитории. Многие проекты публикуют раздел changelog для версии как её release notes.
Где должен лежать CHANGELOG.md? В корне репозитория, под именно этим именем, чтобы GitHub, реестры пакетов и инструменты релизов его нашли. В монорепозитории по одному на пакет, рядом с манифестом каждого пакета.
Нужно ли генерировать CHANGELOG.md из коммитов git? Сгенерируйте черновик, затем отредактируйте строки, которые будет читать пользователь. Сообщения коммитов описывают изменение кода; changelog описывает то, что замечает пользователь. Инструменты вроде conventional-changelog и release-please дают хорошую отправную точку.
Нужна ли каждому коммиту запись в changelog? Нет. Только изменениям, которые пользователь проекта мог бы заметить: новая возможность, изменённое поведение, депрекации, удаления, исправления и патчи безопасности. Рефакторинги, изменения тестов и правки CI остаются в истории git.
Технические утверждения в этой статье не проходили независимую проверку. Если здесь что-то не так, сообщите нам, и мы исправим.