Как мы автоматизировали процесс генерации Release Notes

Всем привет! Меня зовут Семен. Я Java-разработчик и руководитель группы Java-разработки в Центре Big Data компании MTS Digital. В этом посте я хочу поговорить о Release Notes. Что это такое, почему не стоит писать их вручную и какие есть способы автоматизации. Покажу и реальный пример того, как организована работа с Release Notes в нашем проекте.

image-loader.svg

А что это?

Release Notes — это документация, которая характеризует последний инкремент продукта. Новые фичи, баг фиксы и так далее. Она представляет ценность как для разработчиков, так и для менеджеров, которые хотят знать, как развивается проект. Если вы хотя бы раз проводили еженедельные демо, то понимаете, как сложно порой вспомнить все мелкие детали, которые были затронуты в текущем спринте. Как же составлять Release Notes? Самый очевидный способ — руками. Его и рассмотрим.

Делаем Release Notes вручную

Самый главный плюс — для этого не требуется никаких специальных знаний, а также изменения существующих CI/CD процессов. Нанимаем технического писателя и поручаем это все ему. Что может быть проще? Однако тут «есть пара моментов».

  1. Технический писатель может быть не погружен глубоко в продукт. Соответственно, он не сможет самостоятельно определить, что конкретно изменилось в новой версии. Понадобится разработчик, который будет доносить эту информацию. Фактически здесь получается двойная работа. Разработчик объясняет техническому писателю, что добавилось и убавилось. А технический писатель в свою очередь рассказывает всем остальным.

  2. Процесс по документированию новой версии будет очень трудоемким. Здесь оказывает влияние много факторов. Например, как вы описываете задачи в таск-трекинговых системах (полно и развернуто, или кратко и на скорую руку). Как вы оформляете пул-реквесты. Как в целом архитектурно построена ваша система, насколько сложно в ней ориентироваться.

  3. Велика вероятность допустить ошибку. Забыть упомянуть об исправлении важного бага, или случайно написать про фичу, которая будет сделана только в следующем спринте. Чем больше изменений в релизе, тем больше вероятность промаха.

Как видим, формировать Release Notes с помощью человеческих усилий не рационально. Значит, нужно автоматизировать процесс, верно? Да, но и тут возникает ряд вопросов.

  1. Как определять, что изменилось, а что — нет?

  2. Если установить определенные правила контрибьютинга, по которым изменения можно будет отслеживать, как гарантировать их соблюдение?

  3. Мы поддерживаем несколько версий одновременно. Как автоматизировать процесс единообразно?

Что есть «изменение»?

Инкремент продукта можно мерить большими и маленькими шагами. Большой шаг — это задача в таск-трекере (Jira, Trello, Github/Gitlab Tickets и так далее). Маленький — это коммит в репозитории. Первый я бы назвал technical oriented, тогда как второй — business oriented стратегией. Они не являются взаимоисключающими и их вполне можно использовать совместно, ведь у каждого способа есть свои плюсы и минусы.

Technical Oriented Release Notes

Давайте начнем с более простого варианта. Предположим, что мы используем классический git-flow: develop и master ветка. Из develop раскатываются nightly билды, а при каждом мерже в master деплоится новая версия на production. Иначе говоря, осуществляется релиз. Вот на эти релизы нам и нужны Release Notes.

Найти коммиты нового релиза не составляет труда.

git rev-list $PREV_HASH..HEAD

Где $PREV_HASH — это коммит предыдущего релиза. Получив список хэшей, можно узнать сообщения коммитов. Ну, а далее вставляем эту информацию в письмо и рассылаем всем подписчикам. Profit! Более того, многие CI системы умеют формировать changelog подобных изменений автоматически.

На словах все звучит красиво. Но на деле есть много подводных камней.

  1. Зачастую разработчики пренебрегают написанием информативных сообщений коммитов. Из-за этого вместо понятного списка изменений вы можете получить полотно из fix, update, add, delete и.т.д.

  2. В Release Notes попадут также инфраструктурные сообщения от вашего вендора git-сервера (Merge branch x to y, Merge pull request from z).

  3. Во многих командах есть практика squash«ить коммиты перед мержем пул-реквеста, из-за чего большая часть информации о релизе будет безвозвратно утеряна.

Однако проблемы на этом не заканчиваются. Дело в том, что коммиты, (к сожалению), не представляют большой ценности для бизнеса. Даже если каждое сообщение будет максимально продумано и грамотно написано, скорее всего, это не даст продакт-оунеру никакой ценной информации, ведь разрозненный список коммитов тяжело дифференцировать в отдельные юниты работы.

Есть вариант хранить «бизнес-описания» в файле CHANGELOG.md. Однако это тоже ручная работа, автоматизации здесь нет. Тем не менее, у этого подхода есть и плюс — файл CHANGELOG.md тоже хранится в репозитории и, соответственно, его можно выносить на код-ревью.

Business Oriented Release Notes

Вот если бы вы вставили названия и описания задач, которые сделали, это было бы другое дело. © Типичный продакт-оунер.

Ну что же, давайте попробуем.

Список новых коммитов — это единственное, что мы можем узнать при формировании релизов. Значит, нужно каким-то образом связать коммиты и задачи в доске между собой. Решение довольно очевидно: в каждом сообщении о коммите указываем id соответствующей задачи. После этого можно распарсить айдишники из отфильтрованных коммитов и с помощью API получить необходимую нам информацию.

Рисунок 1. Общая схема доставки Business Oriented Release NotesРисунок 1. Общая схема доставки Business Oriented Release Notes

Такой подход позволяет объединить изменения кода с конкретными задачами бизнеса. Более того, коммиты (хэши и сообщения) можно вставлять в письма как дополнительную техническую информацию.

Все в этом подходе хорошо, кроме одной детали. Откуда мы знаем, что разработчики будут писать сообщения в коммитах так, как мы этого хотим? Хороший вопрос. Конечно, можно установить правила, описать их в CONTRIBUTING.md и надеяться, что люди станут их выполнять.

1ca45217f60323256214a9d80d63d793.jpg

Но хотелось бы автоматизировать эту проверку. Причем желательно, чтобы она носила превентивный характер. То есть не позволяла бы написать некорректное сообщение коммита. К счастью, такой инструмент есть.

Есть еще один момент. Откуда мы знаем, что задачи в таск-трекинговой системе тоже будут описаны грамотно? На самом деле, это проблема. Часто разработчики решают вопросы голосом, а потом описывают задачу двумя-тремя словами. К сожалению, нет универсального способа проверить название и описание задач на полноту и ясность. Единственное решение — это внедрение культуры. Объяснять и рассказывать, почему это важно. Например, если вы завели двусмысленную задачу в пятницу вечером, есть вероятность, что в понедельник утром вы уже не вспомните, что конкретно надо было сделать.

Git Hooks

Когда люди упоминают Git хуки, чаще всего на ум приходят webhooks: триггеры на пул-реквесты, комментарии, аппрувы и так далее. Однако Git нативно поддерживает локальные хуки, которые слушают события, происходящие в репозитории на вашей машине. Одно из таких событий — попытка создания коммита. И вот это уже интересно.

image-loader.svg

Git hook представляет собой обычный скрипт, который выполняется перед или после какой-то операции. В данном случае мы будем говорить о хуке commit-msg. Если код возврата хука отличен от нуля, дальнейшая операция отменяется.

Все хуки по умолчанию должны быть расположены в директории .git/hooks. Чтобы хук начал работать, нужно создать файл с соответствующим названием (commit-msg) в директории. Например, вот простой хук на Python, проверяющий наличии ID задачи в сообщении коммита.

#!/usr/bin/env python3
 
import sys
import re
 
with open(sys.argv[1]) as file:
    content = file.read()
    if (not re.match(r'\[MTS-\d+\] - .+', content)):
        raise NameError("The commit is not validated: {}".format(content))

В качестве аргумента командной строки передается путь до файла .git/COMMIT_EDITMSG. Он содержит в себе сообщение коммита. Прочитав его, мы легко можем проверить строку на соответствие регулярному выражению.

Есть пара нюансов с хуками, которые нужно понимать.

Первый нюанс — хуки не сохраняются в системе контроля версий, так как директория .git не индексируется. То есть мы не можем просто запушить их в репозиторий и тем самым автоматически применить для всех будущих контрибьюторов.

Однако у этой проблемы есть решение. Например, можно создать отдельную директорию в проекте, которая будет индексироваться и где мы будем хранить хуки. После этого человеку требуется лишь сделать symlink между нашей кастомной директорией и .git/hooks. А лучше даже написать скрипт, который выполнит это автоматически.

Второй нюанс — хуки сложно написать совместимыми сразу с несколькими операционными системами. Например, если вы написали хук на bash и у вас на Linux все работает отлично, то у вашего коллеги на Windows или MacOS могут возникнуть проблемы. Поэтому для хуков используют высокоуровневые языки, но с ними мы должны быть уверены, что у всех разработчиков будет установлен язык, на котором написан хук (да еще и нужной версии). Лишние зависимости — лишние проблемы.

К счастью, многие инструменты сборки проектов предоставляют плагины/библиотеки для кросс-платформенной реализации git hooks. Моим личным фаворитом является npm пакет Husky. С помощью пары конфигурационных файлов можно добавить необходимый нам хук.

Далее просто указываем команду обновления хуков в package.json «preinstall». Теперь перед каждой установкой зависимостей проекта вместе с тем будут обновляться и хуки. Также хорошим автономным решением является пакет pre-commit.

Semantic Release

Если мы хотим автоматизировать процесс доставки Release Notes, встает проблема версионирования ПО. Можно создать свой набор правил. Например, поднимать нужную цифру в номере версии в зависимости от названия ветки, анализировать коммиты, задачи, теги и так далее. Или можно взглянуть на существующие готовые решения, которые подойдут для вашего проекта.

Semantic Release — это npm пакет, который полностью автоматизирует процесс деплоя, релизов, а также инкрементов версий.

Библиотека следует нескольким концепциям.

  1. Есть одна главная ветка в репозитории (master). Все остальные — короткоживущие

  2. Новый Pull Request — новый релиз

  3. Начальная версия продукта — 1.0.0

По умолчанию Semantic Release следует правилам коммитирования по гайдлайну Angular Commit Messages. Однако их можно переопределять, что поможет в решении проблемы с культурой написания commit-message, которую я описывал выше.

Когда очередной пул-реквест попадает в master ветку, semantic release производит скан коммитов от текущего состояния до последнего релизного тега. fix поднимает patch версию, feat — минорную, а ! — мажорную (парсер при необходимости можно переопределить).

Была версия 1.0.1
feat: …
feat: …
fix: …
feat: …
Стала: 1.1.0

Была версия 2.0.3
build: ...
refactor: …
fix: …
Стала: 2.0.4

Была версия 12.12.4
docs: …
feat!: …
fix: …
Стала:  13.0.0

После успешного релиза (команда определяется конфигом) в Git создается новый тег, в описании которого добавляется отформатированная информация из полученных коммитов.

Вот пример конечного Release Notes из репозитория eslint-plugin-unicorn.

Рисунок 2. Пример конечных Release Notes, сгенерированных с помощью Semantic ReleaseРисунок 2. Пример конечных Release Notes, сгенерированных с помощью Semantic Release

Также отмечу, что библиотека не привязана строго к среде Javascript/Typescript. Благодаря наличию гибкой конфигурации ее можно интегрировать с любой системой сборки проектов, которую вы предпочитаете. Помимо этого, есть уже готовые решения. Например, плагин для Maven и Gradle.

Как вы заметили, Semantic Release исповедует подход Technical Oriented Release Notes. На мой взгляд, он отлично подходит для разработки библиотек и фреймворков (особенно open-source), но является спорным решением для сервисов.

Во-первых, Semantic Release не подходит для поддержки нескольких версий одновременно. Строго говоря, эта фича поддерживается, но на мой взгляд сложность конфигурации и дополнительные специфические правила коммитирования не оправдают итоговый результат.

Во-вторых, релизы как правило привязываются к milestone или story, которые определяются потребностью бизнеса, а не признаком того, в каком формате вы написали сообщение коммита. Ну и в-третьих, релиз/деплой при каждом мерже в определенных ситуациях мера избыточная.

Как мы автоматизировали Release Notes

Вот мы и плавно подобрались к теме того, как автоматизация реализована у нас. Стек следующий: Gitlab, Jenkins, Jira и SonarQube. Схематично процесс формирования отображен на рисунке 3.

Рисунок 3. Процесс автоматической генерации Release Notes после деплояРисунок 3. Процесс автоматической генерации Release Notes после деплоя

Разработчик пушит свои изменения в feature-branch (docs, refactoring, bugfix, и.т.д.) и создает Merge Request. Если все проверки (в том числе code-review) прошли успешно, изменения отправляются в develop ветку. Раз в день develop ветка разворачивается на dev-стенд (nightly builds).

После деплоя происходит процесс парсинга коммитов с целью определить ID задач, которые были затронуты в текущем релизе.

[MTS-2312] - PUT /rest/safe/rule/{ruleId} может вызывать только админ

…

[MTS-2326] - добавил сортировку по полю stage

В данном примере будет получен список из задач MTS-2312 и MTS-2326.

Теперь по JIRA REST API запрашиваем основную информацию по каждой задаче (название и описание).

GET /rest/api/2/issue/{issueIdOrKey}?fields=summary,description&expand=renderedFields

Обратите внимание на параметр expand=renderedFields. Дело в том, что summary и description возвращаются в формате JIRA разметки. При добавлении таких строк в письма их становится тяжело читать и воспринимать. Ранее описанный параметр добавляет в ответ эти же поля в виде HTML, который гораздо проще сверстать в письме.

После проставляем на задачи лейблы конечных стендов, где были развернуты изменения. Например, dev, staging или prod.

PUT /rest/api/2/issue/{issueIdOrKey}

{ "update": { "labels": [ { "add": "staging" } ] } }

В конце информация о выполненных задачах рассылается всем заинтересованным людям. Процесс для master ветки выполняется аналогичным образом.

К плюсам такого подхода можно отнести то, что он легко расширяем. Например, если мы поддерживаем несколько версий в production, то можно без проблем выполнять вышеописанный процесс для каждой релизной ветки.

В итоге мы получили автоматизированный процесс по рассылке писем со всеми задачи, которые были выполнены в рамках текущего релиза, а также их описаниями. Более того, он не требует значительных изменений в процессе разработки. Разработчикам достаточно лишь указывать ID задачи в Jira в каждом коммите. Но для того чтобы реализовать это на практике, требуется внедрение соответствующей культуры. Нужно объяснять сотрудникам, почему это важно и насколько позволяет повысить качество конечного продукта, ведь Release Notes — это тоже часть итогового результата.

Пожалуй, это все, что я вам хотел рассказать про автоматизацию Release Notes. Делитесь своими историями в комментариях. Расскажите, как этот процесс реализован у вас в проекте и спасибо за чтение!

© Habrahabr.ru