Перейти к содержанию

srekit changelog

Скаффолд CHANGELOG.md в формате Keep a Changelog. Автодетектит GitHub репо из git config remote.origin.url.

Голый вызов генерирует. Две подкоманды обслуживают уже существующий changelog: release режет версию, validate линтит документ. Собственными записями каталога они не являются — srekit changelog ведёт себя ровно так же, как вёл всегда.

Синопсис

srekit changelog [flags]
srekit changelog release --version X.Y.Z [FILE] [flags]
srekit changelog validate [FILE]

Флаги

Флаг Обязательный Описание
--repo нет <owner>/<name> slug. Если не передан — srekit берёт meta.repo из payload'а --from, а если и его нет — читает git config remote.origin.url и парсит GitHub SSH или HTTPS URL'ы.
--version нет Начальный version anchor (например 0.1.0). Default: 0.1.0.
--from нет Читать тела секций из JSON-файла; - читает stdin.
--lang нет Язык генерируемых change type: en или ru. Default: en, либо changelog_lang из конфига. См. Русский вариант.

Плюс общие output-флаги. Default имя файла: CHANGELOG.md.

--lang персистентный на всю группу команд, так что release и validate его тоже принимают. Ни та, ни другая на него не смотрят: словарь change type обе читают из документа, который перед ними лежит.

Примеры

Внутри git-репо с origin-remote на GitHub:

srekit changelog --out CHANGELOG.md
# репо детектится из git remote, версия default 0.1.0

Явно:

srekit changelog --repo jtprogru/srekit --version 0.1.0 --out CHANGELOG.md

Вне git-репо без --repo — ошибка (никакого silent OWNER/REPO плейсхолдера, который кусал юзеров в v0.2):

srekit changelog --stdout
# Error: could not detect repo from git remote — pass --repo OWNER/NAME

Структурированный ввод

--json отдаёт документ в виде {meta, sections}, а --from принимает эту же форму обратно — тела секций можно заполнить скриптом или агентом, а не руками:

srekit changelog --repo acme/api --json > cl.json
# ...заменить тело секции "unreleased"...
srekit changelog --from cl.json

Переданные тела секций вставляются дословно, без вычисления шаблонов, поэтому markdown с {{ }} проходит round-trip без изменений. Пропущенные секции берут дефолты из артефакта. Неизвестный артефакту section id — ошибка с именем нарушителя, а не молчаливый пропуск.

meta в payload'е задаёт repo, initialVersion и today. Флаг выигрывает у файла, файл — у git remote, поэтому payload с meta.repo рендерится и вне git-репозитория.

В отличие от srekit postmortem, у changelog нет --schema и --validate: его артефакт не объявляет обязательных секций, поэтому валидация payload'а не может завершиться неудачей, а схема из двух строковых полей говорит меньше, чем сам --json.

Вывод

Скаффолд включает скелет [Unreleased] / [<version>] с шестью подсекциями Keep a Changelog и заканчивается блоком link reference definitions на github.com/<repo>/compare/v<version>...HEAD.

Этот блок ссылок — футер уровня документа, а не часть тела последней секции. Поэтому он переживает payload --from, заменяющий initial_release, и именно его переписывает changelog release.

Русский вариант

Все остальные артефакты srekit двуязычные — заголовки Русский (English) над русской прозой. changelog — исключение, и по умолчанию остаётся английским, потому что его парсит тулинг вокруг Keep a Changelog. Русскоязычной команде, чей changelog не читает ничей CI, можно переключиться:

srekit changelog --repo acme/api --lang ru --stdout
# Changelog

Все заметные изменения в этом проекте документируются в этом файле.

Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
и этот проект придерживается [Семантического версионирования](https://semver.org/lang/ru/spec/v2.0.0.html).

## [Unreleased]

### Добавлено

-

### Изменено

-

...

## [0.1.0] - 2026-03-04

### Добавлено

- Первый релиз.

[Unreleased]: https://github.com/acme/api/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/acme/api/releases/tag/v0.1.0

Чтобы не набирать флаг каждый раз — changelog_lang: ru в конфиге или SREKIT_CHANGELOG_LANG=ru в окружении, см. Конфигурация. Флаг выигрывает у обоих. Нераспознанное значение падает с ошибкой, называющей en и ru, до того как что-либо записано.

Это ломает тулинг, который читает твой changelog

Всё, что грепает ### Added, группирует записи по типу изменения или собирает release notes из файла, перестанет находить то, что ожидает. Релизная автоматизация, changelog-агрегаторы и линтеры keep-a-changelog написаны под английскую шестёрку. srekit changelog validate и release понимают оба словаря; за пределами srekit этого никто не обещает. Default остаётся английским ровно для того, чтобы это было решением, которое принимают, а не наследуют.

Что остаётся английским

Переводятся только заголовки change type и окружающая проза. ## [Unreleased], заголовки версий и метки ссылочных определений в русском варианте сохраняют английскую форму.

В Markdown ## [Unreleased] и [Unreleased]: <url> — две половины одной reference link, текст заголовка и есть метка. Перевести заголовок — значит перевести метку, а метка это та часть документа, которая смотрит наружу: на compare-URL, собранный из тегов. У русского проекта теги всё равно v1.2.0, а compare-ссылки всё равно живут на англоязычной форже. Плюс [Unreleased] — единственный якорь, который вообще делает changelog машинно-находимым: по нему changelog release находит точку вставки, по нему же её находит любой другой инструмент. Перевод заканчивается ровно там, где документ перестаёт быть прозой.

Как переопределить вариант

changelog.ru.yaml — такой же шипящийся артефакт, как остальные: srekit templates init его скаффолдит, templates list показывает отдельной записью, templates upgrade мёржит против его собственного снапшота. Правка своей копии одного языка не трогает другой. См. Кастомные шаблоны.

Как резать релиз

srekit changelog release --version X.Y.Z переносит всё из-под ## [Unreleased] в новый заголовок ## [X.Y.Z] - YYYY-MM-DD прямо под ним, оставляет [Unreleased] пустым и обновляет блок ссылок так, чтобы [Unreleased] сравнивал новый тег с HEAD.

srekit changelog release --version 1.2.0

Подсекции change type, где нет ничего кроме голого - из скаффолда, по дороге выкидываются — выпущенная версия никогда не шипит пустой ### Deprecated. [Unreleased] остаётся действительно пустым, а не перезаполняется скелетом из шести типов: в примере самого Keep a Changelog он тоже пустой, а перезаполнение клало бы в каждый релизный diff шесть заголовков и шесть плейсхолдеров, которые следующий релиз всё равно вычистит.

Флаги

Флаг Обязательный Описание
--version да Выпускаемая версия, без tag prefix (например 1.2.0).
--date нет Дата релиза в форме YYYY-MM-DD. Default: сегодня.
--yanked нет Пометить релиз отозванным: ## [X.Y.Z] - YYYY-MM-DD [YANKED].
--dry-run нет Напечатать результат, не писать.
--stdout нет Напечатать результат, не писать.
--json нет Отдать разобранный документ (версии, даты, yanked-состояние, change types, определения ссылок) и не писать.

Цель — CHANGELOG.md в рабочей директории либо путь, переданный единственным позиционным аргументом:

srekit changelog release --version 1.2.0 docs/CHANGELOG.md

Ни --out, ни --force

release — не генератор, и общий набор output-флагов он несёт не целиком. Он переписывает тот файл, на который его навели: вторая точка назначения не имеет смысла, а guard от перезаписи защищал бы от собственной цели команды. Любой из этих флагов — ошибка unknown flag, а не молча проигнорированная опция.

Последовательность релизного дня

Команда правит текст. Она не коммитит, не тегает и не пушит — это остаётся под твоей рукой:

srekit changelog release --version 1.2.0 --dry-run   # 1. сначала посмотреть
srekit changelog release --version 1.2.0             # 2. отрезать
git diff CHANGELOG.md                                # 3. отревьюить
git commit -am "release: 1.2.0"                      # 4. закоммитить
git tag -a v1.2.0 -m "1.2.0" && git push origin v1.2.0   # 5. затегать

Отозванные релизы

Релиз, отозванный после публикации, помечается, а не удаляется — номер версии сожжён в любом случае, и читателю нужно понимать, откуда взялась дыра:

srekit changelog release --version 0.0.5 --date 2014-12-13 --yanked
# ## [0.0.5] - 2014-12-13 [YANKED]

--date существует ровно для случаев, когда «сегодня» — неправильный ответ: бэкфилл версии, вышедшей до того, как ты начал пользоваться этим инструментом, или релиз через границу таймзоны, где дата тега и твоя локальная дата расходятся.

Конвенции ссылок берутся из документа

Новые определения строятся из собственной строки [Unreleased] документа, а не из git remote. В этой строке уже закодированы хост, путь репозитория, форма URL сравнения и наличие префикса v у тегов. Поэтому проект на self-hosted GitLab или проект с голыми тегами 1.2.0 сохраняет свою конвенцию:

[Unreleased]: https://git.example.com/group/proj/-/compare/1.1.0...HEAD

после релиза становится

[Unreleased]: https://git.example.com/group/proj/-/compare/1.2.0...HEAD
[1.2.0]: https://git.example.com/group/proj/-/compare/1.1.0...1.2.0

Определение новой версии сравнивает её с предыдущей самой свежей, а если она первая — указывает на её release tag. Slug репозитория резолвится из git только тогда, когда блока ссылок в документе нет вообще, — тем же способом, что и в srekit changelog.

От чего команда отказывается

Каждый из этих случаев завершается ненулевым кодом и оставляет файл байт-в-байт прежним:

Условие Почему
--date не в форме YYYY-MM-DD Проверяется до чтения файла, чтобы опечатка не добралась до документа.
Целевого файла не существует Сообщается с путём и указанием на srekit changelog. Ничего не создаётся.
Нет заголовка ## [Unreleased] Некуда вставлять, а угадывать место — ровно тот способ, которым переписыватель уничтожает историю.
В [Unreleased] нет записей Нечего релизить. Подсекции из одних плейсхолдеров не считаются.
У версии уже есть заголовок Повторный запуск — ошибка, а не идемпотентный no-op: записи, которые он бы перенёс, уже не те, что вышли в релиз. Правь файл руками.

Всё остальное сохраняется дословно

Меняются только три региона: [Unreleased], вставленная версия и блок ссылок. Написанная руками преамбула, стиль пустых строк, стиль маркеров списка, ранее выпущенные версии и хвост документа выходят байт-в-байт такими же, какими вошли. Это свойство дизайна: переписыватель делает splice по байтовым офсетам, а не сериализует заново разобранную модель. Именно поэтому релизный diff остаётся читаемым на ревью.

Валидация существующего changelog

srekit changelog validate [FILE] по каждой проверке сообщает, где документ расходится с Keep a Changelog. Файл не переписывается никогда.

srekit changelog validate
OK    heading-shape
OK    unreleased-section
FAIL  version-order: versions must appear in descending order: 1.1.0 (line 12) is listed above 1.2.0 (line 24)
OK    no-duplicate-versions
FAIL  change-types: unrecognized change type line 31: Improvements; allowed: Added, Changed, Deprecated, Removed, Fixed, Security, Добавлено, Изменено, Устарело, Удалено, Исправлено, Безопасность
OK    change-type-language: English (en) change types
OK    link-definitions
Проверка Что требует
heading-shape Каждый заголовок версии — ## [X.Y.Z] - YYYY-MM-DD, опционально с [YANKED]. Именно она ловит региональную дату вроде 04/03/2026.
unreleased-section Секция [Unreleased] присутствует и стоит выше всех выпущенных версий.
version-order Выпущенные версии идут по убыванию. Сравнение посегментно-числовое, поэтому 1.10.0 корректно оказывается выше 1.9.0.
no-duplicate-versions Ни одна версия не встречается дважды.
change-types Каждая подсекция ### — одна из Added, Changed, Deprecated, Removed, Fixed, Security либо их русских эквивалентов.
change-type-language Документ использует один словарь change type, а не два. Сообщает, какой именно распознан.
link-definitions У каждого заголовка версии есть определение в блоке link reference definitions.

Сообщается каждая проверка — и прошедшая, и упавшая: человеку, который чинит разъехавшийся changelog, нужен весь список за один проход, а не первая ошибка. Ненулевой код возврата — если упала хотя бы одна.

Словарь change type — это шесть типов из спецификации на каждый язык, а не то, что написано в твоём кастомном changelog.yaml. Переименованный заголовок здесь падает, и это правильный ответ: формат называет именно эти шесть. Русский документ, переведённый руками до появления варианта, упадёт, если в нём синоним — Исправления вместо Исправлено, — и в ошибке будет перечислен допустимый набор.

На каком языке документ

Оба словаря распознаются всегда, а какой из них в силе — читается из самого документа, по первому распознанному заголовку change type. --lang на это не влияет: генерация и парсинг идут в противоположные стороны, и у команды, которая переключилась на русский, всё ещё лежит английский CHANGELOG.md с прошлых времён, который release не должен испортить.

validate сообщает распознанный словарь прямо в прошедшей проверке change-type-language — чтобы тот, кто ждал одного языка и получил другой, увидел расхождение, а не список зелёных проверок, измеривших не то.

Документ, в котором словари смешаны, падает с указанием заголовков-нарушителей, а release от такого отказывается и оставляет файл байт-в-байт прежним:

FAIL  change-type-language: document mixes change-type vocabularies: English "Added" (line 5), Russian "Исправлено" (line 12); a changelog must use one language throughout

Наполовину переведённый changelog — это файл, где ### Added и ### Добавлено означают одно и то же, и ни читатель, ни инструмент не могут сгруппировать записи. Документ, в котором нет ни одного распознанного change type, тоже считается ошибкой, а не поводом угадать язык.

validate сообщает, но не чинит. Правка разъехавшегося документа — это изменение, которое ты должен увидеть.

Структура данных для шаблона

changelog шипится двумя v1 YAML-артефактами — internal/tmpl/templates/changelog.yaml и его русским вариантом changelog.ru.yaml, структурно идентичными вплоть до id секций. Каждый — H1 + header_body (intro-параграф) + две секции (unreleased и initial_release) + footer_body (блок link reference definitions). Заголовок секции initial_release динамический ([{{ .Meta.InitialVersion }}] - {{ .Meta.Today }}); section titles template-evaluated с v0.20.0. Template-выражения обращаются к .Meta.<Field> для Today (дата 2006-01-02), Repo (<owner>/<name>), InitialVersion. См. srekit postmortem для полной схемы.

См. также