Автоматизация процессов разработки и тестирования программного обеспечения (ПО) — лучший способ уйти от рутины и заняться действительно интересными задачами. Азарт от реализации новой функции может быть погребен под рутиной сборки, подготовки и выпуска нового релиза. Как избежать этой неизбежной скучной задачи, но выпустить релиз ПО, не упустив при этом ни одной мелочи при его подготовке?
Выпуск релиза ПО — это не только сборка ПО в определённого формата пакет и отправка пакета на место его установки. Зачастую выпуск релиза включает в себя множество других задач, таких как:
оформление сопроводительной документации;
указание номера версии релиза у задач в Bug-трекинге;
проверка, что нужные задачи попали в нужный релиз;
оформление и рассылка уведомлений о выходе релиза.
Количество таких задач может меняться от проекта к проекту в зависимости от требований и рабочего процесса, принятого на проекте.
Но независимо от количества и типа задач, практически для любого продукта выпуск релиза происходит с определённой периодичностью. И такой процесс — это отличный кандидат для автоматизации рутинных действий.
Логично будет использовать уже существующие инструменты. И часть процесса выпуска релиза на нашем проекте уже была автоматизирована. Но оставалось ещё много работы, которую приходилось выполнять вручную. И желание автоматизировать этот процесс привело к тому, что у нас появился сервис позволяющий выпустить релиз в два клика. И сегодня я расскажу, как мы пришли к созданию такого инструмента и каких результатов достигли.
С небольшого ручейка начинается река
Изначально не было цели создать инструмент автоматизации выпуска релизов. У нас был процесс выпуска релизов, он хорошо и пошагово расписан на Wiki. Но один из шагов был настоящей мукой для выпускающего релиз разработчика. А именно: добавление информации о выпуске релиза на страницу Wiki.
Этот нехитрый шаг требовал усидчивости, внимания и кропотливой ручной работы при написании статьи о выпуске релиза. Большая часть работы сводилась к копированию информации из задач в Bug-трекинге в страницу релиза на Wiki. На это несложное, но жутко скучное и однообразное действие требовались не только моральные силы разработчика, но и время.
Поэтому создание инструмента автоматизации выпуска релизов начинался с довольно простой консольной программы, которая через API Bug-трекинга вытаскивала необходимый список задач и формировала страницу в Wiki-разметке. И по завершении работы программы готовая страничка быстро и заботливо переносилась разработчиком уже на Wiki.
Сэкономив время на одном шаге выпуска релиза, сразу возникло желание автоматизировать и другие. Результатом проделанной работы был набор скриптов, объединённый одной кодовой базой.
Муки выбора
У получившегося набора скриптов автоматизации было несколько недостатков:
скрипт надо было запускать на локальном компьютере разработчика;
-
сложно было выполнить проверки:
можно ли выпускать релиз?
корректно ли выпустился релиз?
Первая трудность хорошо решалась при помощи инструмента continuous integration (CI). Нужно было просто добавить шаг в сборку проекта. Но CI, к сожалению, не мог решить вторую проблему.
Зачем нужны эти проверки?
К сожалению, в активном ритме проекта бывают различные ситуации. Например, забыли перевести задачу в нужное состояние, хотя код уже попал в релизную ветку. Возможна и обратная ситуация, когда задача в нужном состоянии, а коммит забыли перенести в релизную ветку.
Нахождение таких несоответствий можно легко запрограммировать, а вот научить программу принимать решения исходя из сложившийся ситуации очень сложно. И такое решение должен принять человек, который выпускает релиз.
И по этой причине было принято решение разработать собственный инструмент, заточенный под наши нужды.
Выпуск релиза в два клика
Во время выпуска релиза необходимо выполнить две проверки:
можно ли выпускать релиз?
корректно ли выпущен релиз?
По этой причине весь процесс автоматизированного выпуска релиза был разделён на две большие части:
подготовка предрелизной информации;
выпуск релиза и отображение результатов выпуска.
1. Подготовка предрелизной информации
Всё взаимодействие с Release Manager осуществляется через веб-интерфейс. Поэтому сотруднику выпускающий релиз необходимо перейти на главную страницу Release Manager в браузере и первым кликом выбрать нужный проект. Через пару секунд Release Manager отобразит информацию о предстоящем релизе:
В таблице “Correct state” отображаются задачи, которые прошли под все необходимые критерии для включения их в указанный релиз. Иными словами, задачи в этом столбце гарантированно пойдут в релиз.
В таблице “Incorrect state” отображаются задачи, которые по каким-то критериям не подошли для включения в релиз. Все критерии разделены в две большие группы:
Resolved/Unresolved (Завершённые по статусу в Bug-трекинге/ Не завершённые по статусу в Bug-трекинге);
Merged/Unmerged (Слиты изменения по задаче в source branch/ Не слиты изменения по задаче в source branch).
Виджет “Release Options” отображает версию выпускаемого релиза, которую RM высчитывает по определённому алгоритму, и дополнительные опции, которые можно указать при выпуске релиза:
source branch — это имя ветки, на основе которой выполнялись необходимые вычисления;
create new branch - активация алгоритма создания отдельной релизной ветки при выпуске релиза;
RC mode - активация алгоритма выпуска релиз кандидата (RC).
Кнопка “Release!” запускает следующий шаг выпуск релиза или релиз кандидата, если активирован переключатель “RC mode”.
Имея всю информацию перед глазами, сотруднику нужно лишь проверить:
версия, указанная в задаче на выпуск релиза, совпадает с версией, подсчитанной Release Manager;
что все необходимые задачи включены в табличку “Correct state”.
Далее выставить переключатели в нужные состояния (зависит от того, что сотрудник выпускает релиз кандидат или релиз) и затем нажать кнопку “Release!”
2. Выпуск релиза и отображение результатов выпуска
В среднем Release Manager требуется несколько секунд, чтобы проделать весь объём работы, который у сотрудника при ручном выпуске релиза занимал до нескольких часов нудной и кропотливой работы. По окончанию выпуска релиза Release Manager отобразит страницу с результатами выпуска:
Всё что останется сделать — это скопировать информацию на wiki и отправить письмо с шаблоном.
Релиз Release Manager’а
Изначально Release Manager был реализован для одного проекта. И сразу стал незаменимым инструментом в работе. Но оказалось, что у ребят из других групп разработки тоже есть желание автоматизировать выпуск релизов своих проектов. И демонстрация Release Manager’а коллегам показала, что он способен помочь им в процессе автоматизации выпуска релиза.
Ребята из других групп с энтузиазмом взялись за работу и в скором времени количество проектов, поддерживаемых Release Manager возросло с 1 до 9. И для других групп разработки данный инструмент стал незаменимым помощником при выпуске релизов.
Время перемен
Концепция Release Manager, выпуск релиза в два клика, прекрасно подходила для всех проектов, включённых в него. И Release Manager исправно и активно помогал нам в работе. Но со временем всё меняется. И алгоритм выпуска релиза тоже. Какие-то шаги добавлялись, какие изменялись, а какие-то и вовсе убирались. Всё это приводило к тому, код Release Manager тоже менялся. Концепция Release Manager прекрасно продолжала работать несмотря на все изменения. Но кодовая база, к сожалению, нет.
Почему такое произошло?
Изначально Release Manager разрабатывался для автоматизации процессов на одном проекте. Поэтому дизайн приложения был заточен для работы только с одним проектом. Потом добавили ещё 8. Кодовая база изменялась со временем и в какой-то момент поддерживать код стало сложно. Это стало приводить к тому, что новые доработки в один проект стали ломать существующие функции в других. Также это привело к другой трудности, а именно по коду стало сложно понять для какого проекта он используется.
Стало ясно, что необходимо обновить Release Manager, чтобы он соответствовал текущим нуждам.
Рефакторинг
Люди, делая ремонт в доме, обновляют мебель и делают перестановку. В большинстве случаев они делают не потому, что дом перестал выполнять свои функции. Просто он перестал соответствовать их нуждам. А нужды всегда разные.
Аналог такого ремонта есть и у разработчиков. Он называется рефакторинг. Release Manager продолжал исправно выполнять свои функции, но дорабатывать и поддерживать его в исправном состоянии становилось всё сложнее и сложнее. Исходя из возникших трудностей мною были составлены требования к разработке:
изменения кодовой базы для одного проекта не влияли на функциональную работоспособность другого;
глядя в код, можно было визуально определить для какого проекта он используется, не вникая в логику его работы.
Говорят, что всё новое — это хорошо забытое старое. Отчасти так произошло с обновлением Release Manager. Вдохновение пришло из опыта работы с системами CI. В таких системах вся сборка проекта сводится к набору шагов.
Такой набор шагов ещё называют пайплайн (pipeline). Выпуск релиза тоже представляет собой набор шагов. По этой причине я принял решение, научить Release Manager собирать для каждого проекта свой собственный пайплайн, а шаги для каждого проекта хранить отдельно друг от друга.
Для этой цели я создал аннотацию @Pipeline, внутри которой можно определить шаг. Шаг хранит в себе информацию о проекте и о порядке выполнения шага в наборе шагов. Шаг представляет собой аннотацию @Step. Вот пример использования:
@Component
@Pipeline({
@Step(project = ES, order = 1),
@Step(project = DT, order = 3)
})
public class IssuesByFiltersStep implements PreReleaseStep {
private final IssueService issueService;
@Autowired
public IssuesByFiltersStep(IssueService issueService){ this.issueService = issueService; }
@Override
public PreReleasePipelineContext execute(PreReleasePipelineContext context){...}
}
Один и тот же шаг может быть переиспользован на разных проектах. Поэтому для аннотации @Pipeline можно указать несколько аннотаций @Step. Также один и тот же шаг в разных проектах может выполняться разным по счёту. Это уже зависит от алгоритма выпуска релиза конкретного проекта.
Благодаря аннотациям, разработчику не требуется вникать в логику работы кода, чтобы понять на каких проектах он используется. Но аннотации здесь поставлены не только для наглядности. Они используются Release Manager’ом для сборки шагов по каждому проекту в пайплайн выпуска релиза этого проекта.
Однако возникла трудность, так как проекты все разные и сами шаги тоже могут отличаться. Другими словами, реализация конкретных шагов может сильно отличаться друг от друга на разных проектах. И Release Manager должен уметь склеивать любые шаги для любого проекта. Этого можно добиться, если все шаги будут использовать единый интерфейс:
public interface PipeLineStep<T> {
T execute(T t);
}
У интерфейса всего один метод, который запустит выполнения шага. По концепции Release Manager, выпуск релиза делится на два этапа:
сбор предрелизной информации;
выпуск релиза и отображение результатов.
Поэтому Release Manager’у необходимо уметь понимать какие шаги к какому этапу относятся. По этой причине было добавлено ещё два интерфейса для каждого этапа процесса выпуска релиза:
public interface PreReleaseStep extends PipelineStep<PreReleasePipelineContext> { > {
}
public interface ReleaseStep extends PipelineStep<ReleasePipelineContext> {
}
В итоге для создания одного шага выпуска релиза требуется выполнить два действия:
Создать класс, реализующий либо интерфейс PreReleaseStep, либо интерфейс ReleaseStep;
-
Над классом указать аннотацию @Pipeline, а в ней аннотацию @Step, содержащую имя проекта и порядок выполнения шагов.
@Component
@Pipeline({
@Step(project = ES, order = 1),
@Step(project = DT, order = 3)
})
public class IssuesByFiltersStep implements PreReleaseStep {
private final IssueService issueService;
@Autowired
public IssuesByFiltersStep(IssueService issueService) { this.issueService = issueService; }
@Override
public PreReleasePipelineContext execute(PreReleasePipelineContext context) {...}
}
При запуске Release Manager создаст два списка. В одном списке будут лежать все экземпляры классов, реализирующие интерфейс PreReleaseStep, в другом - все экземпляры классов, реализирующие интерфейс ReleaseStep. И после этого Release Manager начнёт создавать наборы шагов выпуска релиза для каждого проекта.
Для начала он их отфильтрует и упорядочит для каждого проекта:
public static <T> List<T> filterAndSort(List<T> steps, Project project) {
return steps
.stream()
.filter(step -> hasProject(step, project))
.sorted(byOrder(project))
.collect(Collectors.toList());
}
Метод filterAndSort() принимает два аргумента. В первый аргумент будет передан один из ранее собранных списков. Во второй аргумент - проект, по которому нужно отфильтровать и отсортировать шаги.
После того, как каждый шаг прошёл через фильтр, все шаги сортируются по порядку, указанному в поле order в аннотации @Step. Результат операции снова собирает в список (List).
После это Release Manager начнёт склеивать шаги в пайплайн:
public static <T> Function<T,T> createPipeline(List<? extends PipelineStep<T>> pipelineSteps, String projectName) {
if (pipelineSteps == null || pipelineSteps.isEmpty()) {
throw new IllegalStateException("error to create pipeline for project '" + projectName + "' due to there is no pipeline steps");
}
return pipelineSteps
.stream()
.map(step -> (Function<T,T>) LogableFunction
.of(step::execute)
.logBefore("start step : " + step.getClass().getSimpleName())
.logAfter("finish step : " + step.getClass().getSimpleName())
)
.reduce(Function::andThen)
.map(pipeline -> LogableFunction
.of(pipeline)
.logBefore("start '" + projectName + "' pipeline")
.logAfter("finish '" + projectName + "' pipeline")
)
.orElseThrow(() -> new IllegalStateException("error to create pipeline for project '" + projectName + "'"));
}
Метод createPipeline() принимает два аргумента. Первый отфильтрованный и отсортированный список шагов для определённого проекта. И второй - имя проекта.
В первом методе map() Release Manager создаёт LogableFunction из каждого шага и добавляет логирование перед и после выполнения шага. (О LogableFunction можно почитать здесь). В данном случае логируется, что шаг запущен и шаг завершён. Это было сделано по нескольким причинам:
разработчикам не нужно писать эти логи в каждом шаге;
единообразный стиль логирования облегчает чтение логов.
В качестве имени шага было выбрано имя класса. Но при желании можно добавить в интерфейс метод, возвращающий имя шага.
Метод reduce() склеивает шаги в одну цепочку последовательного вызова метода execute() у каждого шага. Результат выполнения метода вернёт одну функцию. На втором методе map() полученную функцию снова оборачиваем в LogableFunction и добавляем логирование о том, что выполнение пайплайна для определённого проекта началось и закончилось.
Метод также бросает исключения в определённых случаях. Если исключение будет брошено, то запуск Release Manager остановится, так как Web-сервис не может корректно предоставлять услуги по выпуску релиза, если не удалось собрать его пайплайн.
После того как все пайплайны собраны, Release Manager запущен и ожидает, когда пользователь зайдёт на главную страницу Web-сервиса и выберет проект. В этом случае Release Manager найдёт нужный сервис, содержащий необходимый пайплайн и выполнит его. В коде Release Manager’а этот процесс выглядит так:
public PreReleaselnfо getPreReleaselnfo(ReleaseOptions releaseOptions) {
var pipelineContext = new PreReleasePipelineContext();
pipelineContext.projectProperties = properties;
pipelineContext.releaseOptions = releaseOptions;
return SupplierFunctor
.оf(pipelineContext)
.andThen(preReleasePipeline)
.andThen(YoutrackPreReleaseInfо::create)
.get();
}
Метод getPreReleaseInfo() принимает один аргумент. В нём содержится базовая информация о доступных для пользователя опциях при выпуске релиза. Изначально она заполнена значениями по умолчанию.
Далее создаётся объект pipelineContext, где будет хранится вся информация о ходе работы в рамках каждого шага при сборе предрелизной информации. Далее контекст кладётся в специальный объект SupplierFunctor к которому можно последовательно применять различные функции. И результат выполнения PreReleaseInfo возвращается пользователю после вызова метода get().
Код выполнения выпуска релиза очень сильно похож на код, выполняющий сбор предрелизной информации:
public Releaselnfo performRelease(PreReleaseInfo preReleaselnfo) {
var pipelineContext = new ReleasePipelineContext();
pipelineContext.preReleaselnfo = preReleaselnfo;
pipelineContext.projectProperties = properties;
return SupplierFunctor
.of(pipelineContext)
.andThen(releasePipeline)
.andThen(YoutrackReleaselnfо::create)
.get();
}
После проведённого рефакторинга вся бизнес-логика заключается в отдельных шагах, которые потом Release Manager собирает в наборы шагов для каждого проекта. Таким образом, можно создать отдельный шаг специфичный для нужного проекта, и он никак не будет влиять на работу шагов других проектов.
Заключение
Получившийся Release Manager оказался очень полезным внутренним продуктом для многих команд разработчиков в компании. Он помог существенно сократить время выпуска очередного релиза, избавил от ненужной рутины, нейтрализовал ошибки ручной сборки и стал основой для дальнейшей кастомизации.
Теперь стало проще соблюдать график выпуска релизов, был организован согласованный конвейер для новых версий, учитывающий потребности всех участников на проекте.
Выбор инструмента остается за вами, но при всем обилии существующих решений создание собственного Release Manager оказалось посильной задачей. Наш алгоритм прошел проверку на работоспособность и может быть взят за основу для создания новых уникальных программных продуктов. Дерзайте!