Автоматизация рабочего процесса Java-проекта с помощью модифицированной модели ветвления Gitflow


Ключевые выводы


  • Gitflow — это совместная модель ветвления, которая пытается использовать мощность, скорость и простоту ветвления Git. Этот метод хорошо работал в ситуации, которую мы описываем здесь, но другие отмечали, что использование Gitflow сопряжено со своими проблемами.
  • Документация по использованию Gitflow в размещения в источнике информации в лучшем случае является нечеткой.
  • Функции изолированы внутри ветвей. Вы можете управлять своими собственными изменениями функций изолированно. Этот подход отличается от разработки на основе магистралей, когда каждый разработчик фиксируется в основной ветке не реже одного раза в 24 часа.
  • Разделение функций с использованием изолированных ветвей позволяет вам решить, какие функции включать в каждый релиз. Компромисс здесь может заключаться в сложных слияниях.



Обновление от 13 февраля 2019 г.: первоначальное оформление этой статьи вызвало большой отклик, в основном положительный, а иногда и не очень. Основным предметом разногласий было использование нами термина «непрерывная доставка» в среде, где релиз выполняются вручную. Если вы работаете в команде, которая развертывает сотни релизов в день, наша структура может вам не подойти. Однако, если вы, как и мы, работаете в жестко регулируемой отрасли, такой как финансы, где релиз более контролируемы, и вы хотите максимально использовать возможности ветвления, автоматической интеграции, автоматического размещения и управления версиями, то это решение может работать для вас так же, как и для нас


Много лет назад я был на технологической конференции, где наткнулся на новомодную штуковину на выставке под названием «Git». Я узнал, что это инструмент управления версиями нового поколения, и моя первая реакция была такой: Зачем нам это, у нас уже есть SVN? Это было тогда. Сегодня группы разработчиков массово переходят на Git, и вокруг промежуточного программного обеспечения и плагинов возникла огромная экосистема.


Gitflow — это совместная модель ветвления, которая пытается использовать мощность, скорость и простоту ветвления Git. Как было написано ранее в InfoQ, этот подход действительно сопряжен со своим набором проблем, особенно в отношении непрерывной интеграции, но это была именно та проблема, которую мы стремились решить. Gitflow был представлен Винсентом Дриссеном в его блоге 2010 года «Успешная модель ветвления Git». Gitflow упрощает совместную разработку, позволяя командам изолировать новую разработку от завершенной работы в отдельных ветвях, позволяя вам выбирать функции для релиза, при этом поощряя частые коммиты и автоматическое тестирование. Мы обнаружили, что это дает более чистый код, продвигая регулярные проверки кода во время слияния, даже проверки собственного кода, тем самым выявляя ошибки, возможности для рефакторинга и оптимизации.


Но когда дело доходит до внедрения Gitflow в автоматизированном конвейере развертывания, подробности становятся очень специфичными для вашей среды разработки, и появляются бесконечные возможности. Следовательно, документация является разреженной. Учитывая известные названия ветвей — master, development, feature и т.д., какие ветви мы строим, какие тестируем, какие развертываем в нашей команде в виде Snapshot, какие развертывают выпуски и как автоматизируют развертывания в Dev, UAT, Prod и т.д.?


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


Описанный здесь проект использует Java и Maven, но мы считаем, что любая среда может быть адаптирована аналогичным образом. Мы используем GitLab CI с настраиваемыми сценариями запуска, но также можно использовать Jenkins или плагин GitHub CI; мы используем Jira для отслеживания проблем, IntelliJ IDEA в качестве нашей IDE, Nexus в качестве репозитория зависимостей, и мы используем Ansible для нашего автоматического развертывания, но их можно заменить любые аналогичные инструменты.


Эволюция


В допотопные времена разработчики тратили недели или месяцы на создание функции приложения, откуда они передавали «завершенную» работу «интегратору» — благонамеренному и преданному человечеству парню, который брал все такие функции, интегрировал их., разрешал конфликты и подготавливал к релизу. Процесс интеграции был пугающим, чреватым ошибками, непредсказуемым по графику и последствиям, породив заслуженное название «интеграционный ад». Затем на рубеже веков Кент Бек выпустил свою книгу «Объяснение экстремального программирования», в которой отстаивал концепцию «непрерывной интеграции»; практика, когда каждый разработчик создает и интегрирует код в основную ветвь и запускает тесты в автоматическом режиме каждые несколько часов каждый день. Вскоре после этого появился Круиз-Контроль Мартина Фаулера Thoughtworks с открытым исходным кодом, один из первых в истории инструментов автоматизации CI.


Вход в Gitflow


Gitflow рекомендует использовать feature ветки для разработки отдельных функций и отдельные ветки для интеграции и релиза. Следующий рисунок, перепечатанный из блога Винсента Дриссена, теперь хорошо знаком командам разработчиков Git.



Как пользователи Git, все мы знакомы с веткой под названием «master»; это основная ветка или «trunk», создаваемая Git по умолчанию при первой инициализации любого проекта Git. Прежде чем принять Gitflow, вы, скорее всего, делали коммит в своей основной ветке.


Запуск Gitflow


Чтобы запустить проект с помощью Gitflow, существует однократный шаг инициализации, на котором вы создаете ответвление от master под названием «develop». С этого момента develop становится всеобъемлющей ветвью, где весь ваш код размещается и тестируется, по сути, становясь вашей основной ветвью «интеграции».



Как разработчик, вы никогда не будете делать коммит непосредственно в ветке develop, и вы никогда не будете делать коммит непосредственно в ветке master. Master — «стабильная» ветка, которая содержит только те работы, которые готовы к производству, либо выпущены, либо готовы к релизу. Если что-то находится в мастере, то это прошлый или будущий производственный релиз.


Бранч develop именуется "нестабильной". Возможно, это название немного ошибочно — он стабилен тем, что содержит код, предназначенный для выпуска; и он должен быть скомпилирован и испытания должны пройти. Он называется так только потому, что он содержит работу, которая может быть или не быть завершенной, и, следовательно, "нестабильной".


Итак, где мы работаем? Вот где начинает материализоваться остальная часть картины:


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



Мы договорились о том, что наши функциональные ветки называются «feat-», за которыми следует номер проблемы Jira. (Если существует более одной проблемы Jira, просто используйте задачу Epic или Parent либо один из основных номеров задач, за которым следует очень краткое описание функции.) Например, «feat-SDLC-123-add-name-field». Префикс «feat-» обеспечивает шаблон, который CI-сервер может использовать, чтобы идентифицировать это как ветвь функции. Скоро мы увидим, почему это важно. В этом примере SDLC-123 — это номер нашей проблемы Jira, который дает нам визуальную ссылку на основную проблему, а оставшееся описание дает нам краткое описание этой функции.


Теперь разработка идет параллельно: все работают над своими функциональными ветками одновременно, некоторые команды работают над одной и той же ветвью, выполняя эту функцию, другие работают над разными функциями. Мы обнаружили, что за счет частого слияния с ветвью разработки команда сократила время, затрачиваемое на «ад слияния (merge hell)».


Релизы, Snapshots и общие репозитории


Давайте проясним это несколькими словами. На большинстве предприятий есть репозиторий с одной зависимостью, например Sonatype Nexus. Это репо содержит два вида двоичных файлов. Бинарные файлы «SNAPSHOT» обычно именуются с использованием версии semver (разделенных точками из трех частей), за которыми следует слово «-SNAPSHOT» (например, 1.2.0-SNAPSHOT). Версии исполняемых файлов релиза имеют одно и то же имя, кроме суффикса «-SNAPSHOT» (например, 1.2.0). Сборки моментальных Snapshot уникальны тем, что каждый раз, когда вы создаете двоичный файл с этой версией Snapshot, он заменяет любой предыдущий двоичный файл с таким же именем. Сборки релизов не такие; как только вы создадите сборку релиза, вы можете сообщить, что двоичный файл, связанный с этой версией, никогда не будет изменен в Nexus.


Теперь представьте, что вы работаете над функцией X, а ваша партнерская команда работает над функцией Y. Вы оба одновременно ответили на разработку, так что у вас обоих одна и та же базовая версия в вашем POM (скажем, 1.2.0-SNAPSHOT). Теперь предположим, что вы запустили свою сборку и развернули ветку функций в Nexus, а вскоре после этого ваша компаньонская команда запустила свою сборку и развернула ее в Nexus. В таком сценарии вы никогда не узнаете, какой двоичный файл функции был в Nexus, поскольку 1.2.0-SNAPSHOT будет относиться к двум различным двоичным файлам, соответствующим двум отдельным ветвям функций (или более, если таких ветвей функций больше!) Это очень часто возникающий конфликт.


GitLab CI


Тем не менее, мы поручаем командам совершать коммиты чаще и быстрее! Итак, как нам избежать таких конфликтов? Необходимо указать GitLab CI, чтобы он построил ошибку, но не развертывал ее в Nexus, связав ветки feat- с этапом жизненного цикла проверки Maven (который создается локально и запускает все тесты), а не этапом развертывания Maven (который будет отправлять двоичный Snapshot в Nexus).


GitLab CI настраивается путем определения файла (с именем .gitlab-ci.yml) в корне проекта, который содержит точные шаги выполнения CI / CD. Прелесть этой функции в том, что сценарий запуска затем связывается с вашим коммитом, поэтому вы можете изменять его в зависимости от комитта или ветки.


Мы настроили GitLab CI со следующим заданием, содержащим регулярное выражение и скрипт для создания веток функций:


feature-build:
  stage: 
    build
  script:
    - mvn clean verify sonar:sonar
  only:
    - /^feat-\w+$/

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


Разработка, управляемая покрытием


Сейчас самое время обсудить тестовое покрытие. Идея IntelliJ имеет режим выполнения «покрытия», который позволяет вам запускать тестовый код с покрытием (либо в режиме отладки, либо в режиме выполнения) и закрашивает поля в зеленый или розовый цвет, в зависимости от того, был ли этот код покрыт или нет. Вы также можете (и должны) добавить в Maven плагин покрытия (например, Jacoco), чтобы вы могли получать отчеты о покрытии как часть вашей сборки интеграции. Если вы используете среду IDE, которая не окрашивает поля, вы можете использовать эти отчеты, чтобы найти участки непокрытого кода.



[Одно замечание по этому поводу — к сожалению, все еще есть много профессиональных команд разработчиков, которые, хотя и проповедуют непреодолимую ортодоксальность автоматизации и разработки, тем не менее по тем или иным причинам упускают возможность расширить охват тестированием. Теперь мы не будем теми, кто скажет таким командам вернуться и добавить тесты к каждому открытому фрагменту кода; но, как хорошие граждане-разработчики, мы считаем своим долгом ввести тесты хотя бы для кода, который мы добавили или изменили. Сопоставляя поля с цветовым кодом покрытия и введенный нами код, мы можем быстро определить возможности для введения новых тестов.]


Тесты выполняются как часть сборки Maven. На этапе тестирования Maven выполняются модульные тесты (обозначенные именем, которое начинается с Test-something.java или заканчивается Test.java, Tests.java или TestCase.java). Maven verifyphase (требуется плагин Maven Failsafe) также выполняет интеграционные тесты. Вызов mvn verify запускает сборку, за которой следует конвейер этапов жизненного цикла, включая тестирование и проверку. Мы также рекомендуем установить SonarQube и плагин Maven SonarQube для статического анализа кода на этапе тестирования. В нашей модели каждый коммит или слияние ветки выполняет все эти тесты.


Интеграция нашей работы


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


У нас также есть политика, которую мы применяем внутри GitLab, которую мы не можем объединить в разработку без проверки кода в форме запроса на слияние:




В зависимости от вашей политики SDLC вы можете заставить разработчиков провести проверку кода с кем-то еще, снабдив свои слияния списком утверждающих. Или вы можете применить более расслабленную стратегию, разрешив разработчикам выполнять свои собственные проверки кода после просмотра своего собственного запроса на слияние. Эта стратегия прекрасно работает, поскольку побуждает разработчиков хотя бы пересматривать свой собственный код, но, как и любая система, она сопряжена с очевидными рисками. Обратите внимание, что, поскольку двоичный файл никогда не будет развернут на Nexus или иным образом предоставлен, версия POM, содержащаяся в ветке разработки, не имеет значения. Вы можете назвать его 0.0.0-SNAPSHOT или просто оставить исходную версию POM, откуда она была разветвлена.


Наконец, по прошествии некоторого количества дней функция будет готова, она будет полностью объединена с версией для разработки и объявлена стабильной, и есть ряд таких функций, которые готовы к релизу. Помните, что на этом этапе мы запускали наши проверочные тесты для каждого коммита, но мы еще не развернули даже SNAPSHOT для Nexus. Это наш следующий шаг


На этом этапе мы разветвляем ветвь релиза от develop. Но в небольшом отступлении от традиционного Gitflow мы не называем это релизом; мы называем ветвь по номеру версии релиза. В нашем случае мы используем семантическое управление версиями из трех частей, поэтому, если это основной релиз (новые функции или критические изменения), мы увеличиваем основной (первый) номер, второстепенный релиз мы увеличиваем второстепенный (второй) номер, а если патч, то третий. Таким образом, если предыдущий релиз был 1.2.0, следующий релиз может быть 1.2.1, а версия моментального Snapshot pom будет 1.2.1-SNAPSHOT. Таким образом, наша ветка будет называться соответственно 1.2.1.


Настройка конвейера


Мы настроили наш конвейер GitLab CI так, чтобы он распознавал создание ветки релиза (ветка релиза идентифицируется по semver номеру, разделенному точкой из трех частей; на языке регулярных выражений: \d+\.\d+\.\d+). Средство выполнения CI/CD сконфигурировано для извлечения имени релиза из имени ветки и для использования плагина версии для изменения версии POM, чтобы включить SNAPSHOT, соответствующий этому имени ветки (1.2.1-SNAPSHOT в нашем примере).


release-build:
  stage:
    build
  script: 
    - mvn versions:set -DnewVersion=${CI_COMMIT_REF_NAME}-SNAPSHOT
    # now commit the version to the release branch
    - git add .
    - git commit -m "create snapshot [ci skip]"
    - git push
    # Deploy the binary to Nexus:
    - mvn deploy
  only:
    - /^\d+\.\d+\.\d+$/
  except:
    - tags

Обратите внимание на [ci skip] в коммит сообщении. Это очень важно для предотвращения зацикливания, когда каждый коммит запускает новый запуск и новый коммит!


После того, как исполнитель CI вносит изменения в POM, он фиксирует и отправляет обновленный файл pom.xml (теперь содержащий версию, которая соответствует имени ветки). Теперь POM удаленной ветви релиза содержит правильную версию SNAPSHOT для этой ветви.


GitLab CI, все еще идентифицирующий эту ветвь релиза по семантическому шаблону управления версиями (/^\d+\.\d+\.\d+$/, например 1.2.1) своего имени, распознает, что в ветке произошло событие push. Средство выполнения GitLab выполняет mvn deploy для создания сборки SNAPSHOT и развертывания в Nexus. Теперь Ansible развертывает его на сервере разработки, где он доступен для тестирования. Этот шаг выполняется для всех нажатий на ветку релиза. Таким образом, небольшие изменения, которые разработчики вносят в релиз-кандидат, запускают сборку SNAPSHOT, релиз SNAPSHOT для Nexus и развертывание этого артефакта SNAPSHOT на серверах разработки.


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


И, наконец, мы объединяемся в master, заставляя Git пометить релиз номером версии semver из имени исходной ветки релиза, развернуть весь пакет на Nexus и запустить тесты сонара.


Обратите внимание, что в GitLab CI все, что вам нужно для следующего шага работы, нужно обозначить как артефакты. В этом случае мы собираемся развернуть наш артефакт jar с помощью Ansible, поэтому мы обозначили его как артефакт GitLab CI.


master-branch-build:
  stage:
    build
  script:
    # Remove the -SNAPSHOT from the POM version
    - mvn versions:set -DremoveSnapshot
    # use the Maven help plugin to determine the version. Note the grep -v at the end, to prune out unwanted log lines.
    - export FINAL_VERSION=$(mvn --non-recursive help:evaluate -Dexpression=project.version | grep -v '\[.*')
    # Stage and commit the binaries (again using [ci skip] in the comment to avoid cycles)
    - git add .
    - git commit -m "Create release version [ci skip]"
    # Tag the release
    - git tag -a ${FINAL_VERSION} -m "Create release version"
    - git push 
    - mvn sonar:sonar deploy
  artifacts:
    paths:
    # list our binaries here for Ansible deployment in the master-branch-deploy stage
    - target/my-binaries-*.jar
  only:
    - master

master-branch-deploy:
  stage:
    deploy
  dependencies:
    - master-branch-build
  script:
   # "We would deploy artifacts (target/my-binaries-*.jar) here, using ansible
  only:
    - master

Устранение ошибок


Во время тестирования могут быть обнаружены исправления ошибок. Они исправляются прямо в ветке релиза, а затем снова объединяются для разработки. (Develop всегда содержит все, что было или будет выпущено.)



Наконец, ветвь релиза утверждается, и она объединяется с главной. Master имеет принудительную политику GitLab, чтобы никогда не принимать слияния, кроме как из ветки релиза. Бегун GitLab проверяет объединенный код в master, у которого все еще есть версия SNAPSHOT ветки релиза. Средство выполнения GitLab снова использует плагин версий Maven для выполнения версий: установите цель с помощью набора параметров removeSnapshot. Эта цель удалит «-SNAPSHOT» из версии POM, и runner GitLab отправит это изменение на удаленный master, пометит релиз, увеличит версию POM до следующей версии SNAPSHOT и развернет ее на Nexus. Это развернуто в UAT для тестирования QA и UAT. Как только артефакт будет одобрен для релиза в производство, группы производственного обслуживания возьмут артефакт релиза и развернут его в производственной среде. (Этот шаг также можно автоматизировать с помощью Ansible, в зависимости от вашей корпоративной политики.)



Патчи и hotfix


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



Проделана работа по завершению хотфикса. Как и ветка релиза, hotfix запускает развертывание Nexus SNAPSHOT и развертывание в UAT. Как только это сертифицировано, оно снова объединяется с разработкой, а затем сливается с masterом для подготовки к релизу. Master запустит сборку релиза и развернет двоичный файл релиза на Nexus.


Заключение


Мы можем суммировать все это в следующей схеме:



Итак, у нас есть Gitflow. Мы призываем команды разработчиков любого размера изучить и опробовать эту стратегию. Мы считаем, что он имеет следующие достоинства:


  • Feature изолированы. Легко управлять своими собственными изменениями Feature изолированно, с обычным предупреждением о ветвлении Feature, которое может затруднить интеграцию команды в очень активную Feature, или коммиты не часто объединяются
  • Разделение Feature, которое позволяет вам выбрать, какие Feature включить в релиз. Альтернативный подход к этому — постоянно релизить код, связанный с Feature, которые скрыты за флагами Feature.
  • Процесс интеграции и слияния привел к тому, что наша команда провела более дисциплинированную проверку кода, что помогло продвинуть чистое кодирование.
  • Автоматическое тестирование, развертывание и релиз для всех сред, которые соответствуют требованиям наших команд и предпочтительному способу работы.

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


Нам интересно узнать о вашем опыте работы с Gitflow и конвейерами развертывания, поэтому, пожалуйста, оставляйте свои комментарии ниже.


Дополнительная информация


Для более традиционной обработки Gitflow с использованием Atlassian Bamboo и BitBucket см. Здесь.


Существует также отличный плагин Gitflow Maven, который активно поддерживается Алексом Мащенко, который работает так же, как плагин релиза Maven на Gitflow. Это может быть адаптировано к нашей предлагаемой реализации Gitflow.


Об авторах


Виктор Граци работает в Nomura Securities над разработкой приложений для корпоративной инфраструктуры. Победитель Oracle Java, Виктор также работал ведущим редактором очереди Java в InfoQ и был членом комитета по выполнению процессов сообщества Java.


Брайан Гарднер — недавний релизник Технологического института Стивенса, где он получил степень бакалавра и магистра в области компьютерных наук. В настоящее время Брайан работает в Nomura инженером-программистом в группе разработки инфраструктуры. В основном он проводит свой день, работая над серверными службами Spring Boot или над конвейерами больших данных с помощью Apache Spark.