Close

Работа с зависимостями Maven при переходе на Git

Фотография Николы Паолуччи
Мэтт Шелтон

Консультант по разработке


Итак, мы переходим на Git и довольны моделью Git-flow. Что дальше? Время тестировать! Моя замечательная команда разработчиков составила список «подопытных» в Confluence: в него вошли рабочие процессы, которые используются сегодня, и те, которые могут понадобиться для реализации всяких странных идей в будущем. После этого мы создали идентичную структуру проекта (но без кода, просто файл pom.xml) и опробовали в ней каждый рабочий процесс.

Зависимости Maven чуть не стали нашей проблемой номер один.

Нумерация сборок в Maven. До релиза Maven присваивает сборке номер типа 1.0.0-SNAPSHOT. После релиза часть -SNAPSHOT отбрасывается, и номер версии превращается в 1.0.0. Процесс сборки должен поддерживать возможность последовательно увеличивать номер вспомогательной версии, чтобы в следующий раз получилась версия под номером 1.1.0-SNAPSHOT. Номер не обязательно должен состоять из трех цифр — можно задать свое количество на старте проекта. Мы остановились на этом варианте. Важно понимать, что означает -SNAPSHOT. Это добавление всегда указывает на то, что перед вами последняя предрелизная версия проекта.

Артефакты


Главным вопросом ко всем рабочим процессам было то, сможем ли мы нормально управлять версиями проекта и зависимостями между проектами.

По умолчанию, когда зависимости Maven извлекаются для сборки, артефакты всегда загружаются из старого доброго Интернета и сохраняются локально, поэтому последующие сборки занимают меньше времени. Чтобы ускорить работу, можно использовать репозиторий артефактов в локальной сети в качестве локального кэша для таких внешних зависимостей. Извлечение из локальной сети почти всегда оперативнее, чем загрузка из самой быстрой сети доставки контента. В качестве репозитория артефактов мы используем Artifactory Pro. Более того, поскольку наша архитектура состоит из нескольких модулей, в Artifactory мы также храним свои артефакты сборки. Когда мы собираем общий пакет, то можем загрузить конкретную версию путем разрешения зависимостей Maven и извлечь нужный объект прямо из репозитория артефактов.

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

базы данных
Связанные материалы

Перемещение полного репозитория Git

Логотип Bitbucket
СМ. РЕШЕНИЕ

Изучите Git с помощью Bitbucket Cloud

Зависимости Maven, функциональные ветки и запросы pull


Все наши сборки попадают в Artifactory. Работая с SVN, мы хранили две последние сборки незавершенных версий в репозитории снимков состояния, а ожидающие одобрения сборки конечных версий — в промежуточном репозитории, и только сборки, достойные отправки в рабочую среду, достигали репозитория релизов.[1] Эти сборки нумеровались, как описано выше, и извлекались по предсказуемому URL-адресу, сформированному на основе репозитория и версии.

Основной рабочий процесс каждого разработчика заключался в том, чтобы создать функциональную ветку от ветки разработки, написать в ней свою часть кода, а потом открыть запрос pull, чтобы объединить результат с веткой разработки. В отдельном проекте такой подход обычно работает без нареканий, но в нашей ситуации мы сразу столкнулись с проблемой, которая заставила нас серьезно пересмотреть всю миграцию.

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

Итак, разработчик А (пусть ее зовут Анжела) начинает работать над функцией в Jira. Ей понадобятся две ветки: одна из общего проекта, а другая из продукта X. Версия для общего проекта получит номер 2.1.0-SNAPSHOT, а версия для продукта X — 2.4.0-SNAPSHOT. Анжела какое-то время работает в локальном репозитории, после чего отправляет код в Bitbucket Server. Bamboo обнаруживает эти изменения, собирает общий пакет и загружает общую сборку за номером common-2.1.0-SNAPSHOT в Artifactory, после чего выполняет сборку для продукта X, используя зависимость из common-2.1.0-SNAPSHOT, и также загружает ее, присвоив номер productX-2.4.0-SNAPSHOT. Сборка проходит модульные тесты.

Разработчик Б (назовем его Брюсом) начинает в Jira работу над другой функцией, на этот раз — для продукта Y. Ему тоже понадобятся две ветки: одна из общего проекта, а другая из продукта Y. Версия для общего проекта, как и в первом случае, получит номер 2.1.0-SNAPSHOT, а версия для продукта Y — 2.7.0-SNAPSHOT. Брюс какое-то время работает в локальном репозитории, после чего отправляет изменения в Bitbucket Server. Bamboo обнаруживает эти изменения, собирает общий пакет и загружает общую сборку за номером common-2.1.0-SNAPSHOT в Artifactory, после чего выполняет сборку для продукта X, используя зависимость из common-2.1.0-SNAPSHOT, и также загружает ее, присвоив номер productX-2.4.0-SNAPSHOT. Сборка проходит модульные тесты.

Тем временем Анжела находит небольшой баг в коде продукта X и пишет модульный тест для проверки исправления. Она запускает тест локально и проходит его. Анжела отправляет изменения в Bitbucket, после чего их обнаруживает Bamboo и выполняет сборку для продукта X. Сборка выполняется успешно, однако не проходит некоторые модульные тесты. Это старые тесты, которые Анжела написала еще для первых изменений функции. Каким-то образом в сборке Bamboo появился регресс, которого не наблюдалось в локальной сборке. Как такое возможно?

Причина кроется в общей зависимости, загруженной Bamboo при выполнении сборки для продукта X. Эта копия изменилась: Брюс перезаписал общую сборку common-2.1.0-SNAPSHOT в Artifactory, когда выполнял сборку своей функции. Конфликта в исходном коде не было, поскольку оба разработчика работали изолированно, каждый в своей ветке. Однако источник, откуда извлекались артефакты Maven, был искажен.

Впору хвататься за голову.

В течение примерно месяца после обнаружения проблемы мы перепробовали все возможное, чтобы ее обойти. Через своего менеджера TAM[2] мы связались с командой Bamboo, которая использует git flow, и поговорили с разработчиком, который поддерживает JGit Flow — реализацию этой методики на Java. Все эти люди нам очень помогли, но, кроме цепочки ручных действий, которые разработчик должен выполнить при работе над своей функцией, приемлемых решений мы не нашли.

Если вам любопытно, что мы пытались предпринять, то вот весь список.

1. Изменить номер версии при создании ветки или сразу после этого:

  • используя mvn jgitflow:feature-start для создания ветки;
  • с помощью хука в Bitbucket Server или локального хука в git;
  • путем ручной настройки с помощью mvn version:set-version после создания ветки;
  • путем автоматизации изменений с помощью плагина [maven-external-version].

2. Изменить номер версии при объединении готовой ветки с веткой разработки:

  • используя mvn jgitflow:feature-finish для завершения работы над веткой;
  • используя драйвер слияний git для разрешения конфликтов между файлами POM;
  • с помощью асинхронного хука post-receive в Bitbucket Server.

3. Сделать все вручную. (Шутка. Мы недолго рассматривали этот вариант.)

Возможные рабочие процессы


В свете этой особенности становится очевидно, что для одних рабочих процессов подмодули подходят хорошо, а для других плохо. Можно предложить как минимум три сценария, в которых подмодули станут разумным выбором:

  • Компонент или подпроект изменяются слишком быстро, или предстоящие изменения могут нарушить работу API. В этом случае можно заблокировать код для конкретного коммита в целях безопасности.

  • У вас есть компонент, который обновляется довольно редко, и вы хотите отслеживать его в качестве зависимости поставщика (например, я поступаю так со своими плагинами vim).

  • Вы делегируете выполнение части проекта другим лицам и хотите интегрировать их работу в определенное время или в конкретном релизе. Это полезно только в том случае, когда обновления происходят не слишком часто.

Спасибо finch за хорошее объяснение этих сценариев.

У всего перечисленного обнаружились свои побочные эффекты. Чаще всего они заключались в том, что разработчикам приходилось выполнять действия вручную всякий раз, когда им нужна была функциональная ветка. А создавать такие ветки приходилось постоянно. Кроме того, в большинстве случаев мы не могли эффективно использовать запросы pull. Именно это стало решающим фактором.

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

Одна версия, чтоб править всеми


Оглядываясь назад, я могу уверенно сказать, что нашей самой большой ошибкой было сосредоточиться на инструментах git-flow, вместо того чтобы внедрить нужный рабочий процесс, используя имеющиеся средства. А именно:

  • Jira Software
  • Bamboo Server
  • Maven
  • Artifactory Pro

Оказалось, что этих инструментов достаточно.

Один из наших инженеров подал блестящую идею: проблема заключается не в управлении сборками как таковом, а в том, что переписываются артефакты, и потому нам нужно доработать Artifactory. Он предложил использовать свойство Maven, чтобы присваивать URL-адресу репозитория снимков состояния пользовательское значение с идентификатором задачи Jira, а затем записывать все артефакты из него в динамически создаваемый репозиторий в Artifactory с помощью пользовательского шаблона. Тогда сопоставитель зависимостей Maven будет находить артефакты в репозитории снимков состояния, относящемся к ветке разработки, если не требуется создавать новую ветку (например, если мы работаем только над продуктом, но не над общим проектом).

Мы настроили эту полезную переменную свойства в файле параметров сборок и написали плагин Maven для ее заполнения на самом раннем этапе жизненного цикла сборки. В теории все выглядело отлично, и это придало команде энергии, так что она с удвоенной силой принялась за дело. Но затем оказалось, что на практике эта идея неосуществима. Самый ранний этап жизненного цикла Maven — проверка. К тому моменту как запускаются плагины для проверки, URL-адреса репозиториев уже разрешены. Из-за этого наша переменная никогда не заполнялась, а URL-адрес не формировался в соответствии с именем ветки. И хотя мы использовали макет в отдельном репозитории, отличном от репозитория снимков состояния для ветки разработки, он не был изолированным и не подходил для параллельной разработки.

И снова разочарование.

Отдохнув за кружкой пива, тот самый инженер продолжил поиски решения и предложил еще один способ доработать Maven, а именно использовать расширения.

«За пиво — причину и решение всех жизненных проблем!» — воскликнул бы Гомер Симпсон.

Расширения, как и плагины, являются мощным способом улучшить рабочий процесс в Maven, однако исполняются они до задач жизненного цикла и имеют больший доступ к внутренним компонентам Maven. Используя пакет RepositoryUtils, мы настроили в Maven принудительное повторное определение URL-адресов пользовательским анализатором и их изменение на обновленные значения.[3]

Установив и протестировав расширение, мы семимильными шагами стали продвигаться по списку задач для подготовки к миграции — от состояния «нет, это невозможно!» до состояния «очень даже возможно и намечено на понедельник, поэтому до завтра надо написать десять страниц документации». Скоро я расскажу о том, как интеграция инструментов помогает поддерживать новый рабочий процесс разработки и что полезного мы почерпнули из нашей практики.

[1] Одним из недостатков такого подхода было то, что мне приходилось использовать самостоятельно написанный скрипт, который заставлял REST API в Artifactory продвигать сборки из промежуточного репозитория в репозиторий релизов. Это отнимало немного времени, однако здесь точно не помешала бы автоматизация.

[2] Персональный менеджер техподдержки. Подробнее см. здесь.

[3] После первых попыток разработки стало ясно: нужно что-то предпринять, чтобы наше решение работало всегда. Потому что в тех случаях, когда снимок состояния в Artifactory (от другого инженера) был новее локального снимка, система Maven загружала удаленный артефакт, считая его ЛУЧШЕ на том основании, что он НОВЕЕ.


Footnotes

[1]: One downside here was that I had to use a script I wrote to hit the Artifactory REST API to "promote" builds from staging to release. It's fast enough, but begging for more automation.

[2]: Technical Account Manager. More information here.

[3]: After the initial development efforts, we found that we had to do even more to make this work 100% of the time, like when a snapshot is newer in Artifactory (from another engineer) than your local snapshot, Maven grabs the remote artifact because hey, it's NEWER, so it must be BETTER, right?

Matt Shelton
Matt Shelton

Мэтт Шелтон — консультант и практик DevOps, курирующий предоставление услуг DevOps и других сопутствующих сервисов Atlassian в Северной и Южной Америке. Сейчас он редко пишет в блог и посвящает все свое время воспитанию нового поколения консультантов по передовым практикам работы.


Поделитесь этой статьей

Рекомендуемые статьи

Добавьте эти ресурсы в закладки, чтобы изучить типы команд DevOps или получать регулярные обновления по DevOps в Atlassian.

Люди сотрудничают друг с другом, используя стену со множеством инструментов

Блог Bitbucket

Рисунок: DevOps

Образовательные программы DevOps

Демонстрация функций в демо-зале с участием экспертов Atlassian

Как инструмент Bitbucket Cloud работает с Atlassian Open DevOps

Подпишитесь на информационную рассылку по DevOps

Thank you for signing up