lenec ru

← все посты

Changelog по Keep a Changelog: как писать так, чтобы его читали

10K

Релиз 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

Самый рабочий процесс, который я видела:

  1. Каждый PR, меняющий поведение, добавляет запись в секцию [Unreleased]. Это требование PR-шаблона и проверяется на ревью.
  2. При релизе секция [Unreleased] переименовывается в [X.Y.Z] - YYYY-MM-DD.
  3. Создаётся новая пустая [Unreleased] сверху.
  4. Тег ставится на коммит, в котором это произошло.
  5. 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». Плохой — превращает каждое обновление в отдельный спринт. Это документ небольшой по объёму, но именно тот, к которому пользователь возвращается чаще всего после релиза. Стоит написать его так, как сам бы хотел читать.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.