Настраиваем CI/CD Android-проекта, часть 3. Автоматизация публикации версий в Play Store

от автора

О чём эта статья?

Всем привет, меня зовут Кирилл и я Android-разработчик в Scanny. В прошлых статьях мы описали то, как будет выглядеть наш CI/CD, научились запускать статический анализатор кода, выполнять Android (Marathon Labs и Firebase Test Lab) и Unit-тестирование, собирать различные Build Flavors и отправлять их в нашу Telegram-группу.

В этой статье мы настроим публикацию свежих версий в Play Market на примере Gradle Play Publisher и Fastlane, а так же создадим пометку в Gitlab Tag с описанием изменений, которые вошли в нашу сборку.

Так же мы улучшим наш CI/CD, собрав свой Docker-образ со всем необходимым окружением. Благодаря этому нам не придется каждый раз устанавливать все инструменты (Python, awscli и другие), что позволит ускорить наш pipeline.

Цикл статей про CI/CD для Android состоит из:

  1. Настраиваем CI/CD Android-проекта, часть 1. Начало.

  2. Настраиваем CI/CD Android-проекта, часть 2. Запуск Android-тестов.

  3. Вы находитесь здесь.

Настройка Play Console

Начнем с настройки Play Console и Google Services, будем считать, что у вас уже есть аккаунт разработчика в Play Console. Так же у вас уже есть страница приложения в Play Store, заполнена информация о компании и приложение уже опубликовано в первый раз.

Перейдем к делу, нам необходимо связать Google Services с нашей Play Console, для этого перейдем в Service Accounts, в верхней части страницы нажимаем на кнопку Create service account. В новом окне заполняем название, описание, устанавливаем права Basic - Viewer. Аналогичные шаги мы уже делали в прошлой статье про CI/CD, когда связывали Google Services с Firebase, поэтому так подробно расписывать не буду. После того как Service Account создан, нам необходимо сгенерировать JSON-ключ и сохранить его после создания.

Дальше переходим в наш Play Console, выбираем вкладку Users and Permissions и нажимаем на кнопку Invite new users в выпадающем меню.

В поле Email address копируем почту нашего Service Account. В Account permissions настраиваем права:

  1. Для раздела App access — либо Admin, либо View app information and download bulk reports;

  2. Раздел Releases — ставим Release to production, exclude devices, and use Play App Signing;

  3. Остальное на ваше усмотрение.

Ну и в конце нажимаем Invite User.

С подключением Google Services закончили, переходим к настройке плагина.

Настройка Gradle Play Publisher

Публикация будет состоять из 2 этапов:

  1. Сборка и публикация приложения в Play Market, а так же отправка сообщения в Telegram-группу;

  2. Создание Gitlab Tag с описанием всех изменений.

В начале разберем работу с Gradle Play Publisher — это gradle-плагин, который позволяет автоматизировать публикацию нашего Android-приложения. Про подключение я рассказывать не буду, т.к. информации достаточно в самой документации, а вот настройку мы сделаем. Перед этим хочу напомнить, что в рамках данной статьи мы будем публиковать сборку исключительно в Play Market. Если у вас есть потребность работы с RuStore, то можно использовать готовый плагин для этого. Аналогичный плагин есть и для Huawei AppGallery.

Вернемся к Gradle Play Publisher, плагин требует создать несколько файлов для работы с описанием и названием сборки, детали будут отличаться в зависимости от ваших потребностей, подробнее можно прочитать тут.

Положим, что мы сразу хотим загружать наши изменения в production, для этого создадим следующие файлы:

  1. Для названия сборки (только для внутреннего пользования) — app/src/main/play/release-names/production.txt;

  2. Для описания изменений — app/src/main/play/release-notes/ru-RU/production.txt.

Визуальная структура файлов изображена ниже.

{root} |-- app   |-- src     |-- main       |-- play         |-- release-names           |-- production.txt         |-- release-notes           |-- ru-RU             |-- production.txt

При каждом релизе лезть в Gradle, чтобы менять название сборки и версию может быть утомительным занятием. Поэтому можно упростить жизнь и определять versionName динамически. Для этого, мы сделаем функцию getVersionName(), которая будет возвращать название версии для Play Console. А вот versionCode можем установить в единицу (или любое другое), т.к. актуальную версию будет проставлять наш плагин.

Переходим к настройке плагина, для этого открываем app/build.gradle.

internal fun getVersionName(): String {       val file = file("src/main/play/release-names/production.txt")       val versionName = file.readText()       return versionName.ifEmpty { "Test Version" }   }      play {       /** Отключаем GPP по умолчанию для всех сборок, мы ведь не хотим каждый build отправлять в Play Market? **/     enabled.set(false)          /** Доступные варианты: internal, alpha, beta, production.      Однако, мы же хотим сразу в production! **/     track.set("production")      /** Указываем процент пользователей, которые получат обновление.      Где, 0.6 = 60%. Только для IN_PROGRESS и HALTED. **/     userFraction.set(0.6)      /** По умолчанию GPP отправляет APK, но нам же нужен Bundle?      Где true - по умолчанию собираем bundle, false - APK. **/     defaultToAppBundles.set(true)      /** Тут мы указываем, что делать в случае, если сборка с таким versionCode уже существует.      AUTO_OFFSET позволяет увеличивать текущий versionCode в Play Console на единицу. **/     resolutionStrategy.set(ResolutionStrategy.AUTO_OFFSET)      /** Здесь: COMPLETED - полная раскатка на всех пользователей;      DRAFT - черновик, сборка еще не готова к публикации;     HALTED - публикация остановлена;     IN_PROGRESS - релиз проходит поэтапную публикацию, например, 60% **/     releaseStatus.set(ReleaseStatus.IN_PROGRESS) }      android {       ...     playConfigs {           register("productionRelease") {             /** Включаем GPP только для определенного build flavor.              В нашем случае пусть это будет production release. **/             enabled.set(true)         }       }            defaultConfig {           ...         versionCode = 1           versionName = getVersionName()           ...      } }

Подробнее про Release Statuses можно прочитать здесь. Ранее мы связывали Google Services с Play Console, в результате чего у нас сохранился JSON-ключ, он нам понадобится для того, чтобы GPP смог связаться с нашей Play Console.

У нас есть 2 варианта, как использовать этот ключ:

  1. Сохранить в переменных окружения под названием ANDROID_PUBLISHER_CREDENTIALS, чтобы GPP по умолчанию брал его из переменной. Конечно же, мы поступим таким образом!

  2. Сохранить в локальном файле и затем указать путь к файлу.

play {     serviceAccountCredentials.set(file("your-key.json")) }

На этом настройка плагина закончена и мы можем переходить к CI/CD pipeline’у.

Настройка CI/CD

Перед нами стоит 2 задачи, это — выложить приложение в Play Market и создать пометку об изменениях, которые войдут в этот релиз, для этого мы будем пользоваться Gitlab Tag’ами. Дополнительно можно немного усложнить задачу, предположим, мы хотим еще и информировать о новом релизе в нашей Telegram-группе. В первой части мы уже разбирали скрипт, который поможет нам отправлять сообщения в Telegram-группу, поэтому здесь мы просто воспользуемся этим решением.

Перед тем как переходить дальше, добавим в stages 2 новых: deploy_release и create_git_tag.

stages:     - lint     - tests     - build_flavors     - deploy_release     - create_git_tag

Публикация сборки с помощью GPP

Дополнительно напоминаю, что ранее мы создали 2 файла: app/src/main/play/release-names/production.txt для названия нашей сборки и app/src/main/play/release-notes/ru-RU/production.txt для описания изменений, которые войдут в эту сборку. Перед публикацией мы описываем в них все изменения, которые затем будут отображаться в Play Store.

Ниже представлена Job’а, которая собирает и отправляет сборку в Play Market, а так же отправляет сообщение о новом релизе в Telegram-группу.

deployRelease:     stage: deploy_release     before_script:     - apt update       - apt install python3-pip --yes          - pip3 install awscli --upgrade   script:          - ./gradlew publishProductionReleaseBundle       - ./gradlew assembleProductionRelease      - export VERSION_NAME="#PRODUCTION_RELEASE  $(cat app/src/main/play/release-names/production.txt)\n"     - export CHANGELOG="$(cat app/src/main/play/release-notes/ru-RU/production.txt)\n"              - aws s3 cp app/build/outputs/apk/production/release/app-production-release.apk s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk --endpoint https://storage.yandexcloud.net       - chmod a+x ./upload_telegram_link.sh         - aws s3 presign s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk --endpoint-url "https://storage.yandexcloud.net/" --expires-in 604800|source upload_telegram_link.sh     artifacts:       paths:         - app/build/outputs/apk/       expire_in: 10 days     rules:       - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'

Здесь в before_script мы устанавливаем Python3 и pip для работы с awscli. Чтобы работать с ним нам нужны 2 переменные окружения AWS_ACCESS_KEY_ID (ID статического ключа) и AWS_SECRET_ACCESS_KEY (Содержимое ключа). Более подробно про то, как их получить, можно прочитать здесь.

before_script:     - apt update       - apt install python3-pip --yes          - pip3 install awscli --upgrade

Дальше специальной командой мы собираем и отправляем на проверку нашу сборку в Play Console. В данном случае мы собираем и отправляем Bundle. Если же мы по каким-то причинам хотим публиковать APK, то пользуемся командой publishProductionReleaseApk. Общая схема выглядит следующим образом: publish{Build flavor}{Bundle/Apk}.

./gradlew publishProductionReleaseBundle  

После успешной загрузки сборки в Play Console мы хотим получить новое сообщение в Telegram-группе о новой сборке, для этого мы отдельно собираем APK.

./gradlew assembleProductionRelease  

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

export VERSION_NAME="#PRODUCTION_RELEASE  $(cat app/src/main/play/release-names/production.txt)\n" export CHANGELOG="$(cat app/src/main/play/release-notes/ru-RU/production.txt)\n"   

В первой части мы уже разбирали, что здесь происходит, но давайте пройдем еще раз. Данной командой мы отправляем APK в наше s3-хранилище (Yandex Object Storage), для этого указываем путь к файлу, после задаем s3-bucket и путь по которому необходимо сохранить файл. За подробностями сюда.

aws s3 cp app/build/outputs/apk/production/release/app-production-release.apk s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk --endpoint https://storage.yandexcloud.net  

Теперь делаем upload_telegram_link.sh скрипт исполняемым. Он же у нас и отвечает за отправку сообщения в Telegram-группу. Как устроен этот скрипт описано в первой части.

chmod a+x ./upload_telegram_link.sh  

Далее получаем Pre-signed Url, по которому мы будем скачивать наш APK. Подробнее можно посмотреть тут. В --expires-in 604800 указываем время жизни ссылки в секундах (7 дней). Через pipe | передаем ссылку в source upload_telegram_link.sh скрипт.

aws s3 presign s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk --endpoint-url "https://storage.yandexcloud.net/" --expires-in 604800|source upload_telegram_link.sh  

В артефактах так же сохраняем наши сборки в Gitlab-артефактах.

artifacts:       paths:         - app/build/outputs/apk/       expire_in: 10 days  

А в rules устанавливаем правила, по которым будет запускаться наша Job’а. В данном случае, Job’а запускается при merge request’е в master. Вы можете более гибко настроить эти правила исходя из ваших задач, прочитать подробнее можно здесь.

rules:       - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'

Переменные окружения:

Название переменной

Описание

AWS_ACCESS_KEY_ID

ID статического ключа, необходим для доступа в наше s3-хранилище.

AWS_SECRET_ACCESS_KEY

Содержимое статического ключа доступа.

CHANGELOG

Описание изменений в release-сборке. Название переменной можно изменить в нашем upload_telegram_link.sh скрипте.

VERSION_NAME

Название нашей сборки. Требуется в upload_telegram_link.sh скрипте.

ANDROID_PUBLISHER_CREDENTIALS

JSON-ключ для работы с Play Console. Данную переменную использует GPP.

Сохранение информации о релизе в Gitlab Tag

Добавить описание можно вручную на Gitlab > Code > Tags, но так не интересно. Поэтому давайте автоматизируем с помощью CI/CD. Описание изменений я бы предложил разделить отдельно для Play Market и отдельно для внутреннего пользования. Для этого создадим в корне проекта файл changelog.txt, либо в любом другом удобном для нас месте. Сюда мы будем вносить более подробное описание изменений, которые будут входить в сборку.

createGitTag:     stage: create_git_tag     script:       - export GIT_TAG=$(cat app/src/main/play/release-names/production.txt)       - export GIT_TAG_MESSAGE=$(cat changelog.txt)          - git remote set-url origin https://oauth2:$CI_BOT_TOKEN@$CI_PROJECT_URL       - git config --global user.email $CI_BOT_EMAIL       - git config --global user.name $CI_BOT_USERNAME         - git tag -a -f $GIT_TAG -m $CI_BOT_USERNAME       - git push -f origin $GIT_TAG     rules:       - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'

Определим 2 локальные переменные: GIT_TAG для названия тэга и GIT_TAG_MESSAGE для описания. Название для GIT_TAG можно взять из названия нашей сборки в Play Market. А в качестве описания будем брать информацию из нашего changelog.txt, который мы создали ранее.

export GIT_TAG=$(cat app/src/main/play/release-names/production.txt)   export GIT_TAG_MESSAGE=$(cat changelog.txt) 

Дальше мы устанавливаем URL, который будет использоваться для авторизации и доступа к нашему проекту в Gitlab. В переменной CI_BOT_TOKEN хранится токен доступа нашего бота (аккаунта, от имени которого мы будем авторизовываться и работать с тэгами), а в переменной CI_PROJECT_URL хранится ссылка на проект в формате gitlab.ru/{Путь к проекту}.git.

Итоговый URL будет следующего вида: https://oauth2:{Токен доступа}@gitlab.ru/{Путь к проекту}.git

Более подробно, как их получить расскажу позже.

git remote set-url origin https://oauth2:$CI_BOT_TOKEN@$CI_PROJECT_URL

Теперь устанавливаем почту и имя бота для корректной записи в историю тэгов. Данные переменные определены в переменных окружения.

git config --global user.email $CI_BOT_EMAIL   git config --global user.name $CI_BOT_USERNAME 

И, наконец, создаем новый аннотированный тэг с названием и сообщением. Обратите внимание на параметр -f, он позволяет перезаписывать уже существующий tag.

git tag -a -f $GIT_TAG -m $GIT_TAG_MESSAGE

Ну вот и все, осталось сделать push и отправить изменения на удаленный репозиторий.

git push -f origin $GIT_TAG

Выполняем данный скрипт при merge request в master.

rules:       - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'

Переменные окружения:

Название переменной

Описание

GIT_TAG

Название нашего тэга. В данном случае мы приняли его как название нашей сборки в Play Market. Рекомендуется принимать название версии как название тэга, например, v.3.2.12

GIT_TAG_MESSAGE

Расширенное описание изменений, которые входят в сборку.

CI_BOT_TOKEN

Токен доступа бота, который будет работать с Gitlab tags.

CI_PROJECT_URL

URL проекта в формате gitlab.ru/{Путь к проекту}.git

CI_BOT_EMAIL

Email бота от имени которого будет создаваться запись.

CI_BOT_USERNAME

Username бота от имени которого будет создаваться запись.

Чтобы работать с Gitlab tags нам нужен аккаунт, у которого есть доступ к нашему Gitlab-проекту. Можно использовать свой аккаунт, либо же создать специальный для этого — как вам больше нравится.

Чтобы получить CI_BOT_TOKEN токен, заходим в профиль аккаунта нажав на Edit profile.

Дальше переходим Access tokens и нажимаем на Add new token.

В форме заполняем название токена, выбираем период действия и даем разрешения: api, read_user, read_repository. После чего копируем наш токен и добавляем его в переменные окружения.

С CI_BOT_USERNAME и CI_BOT_EMAIL я думаю понятно, что это username аккаунта и почта соответственно.

На этом все. Чтобы, увидеть наш тэг после того, как CI/CD отработал, переходим в Code > Tags и видим нашу красоту. Ниже привел пример, как он может выглядеть. Так же здесь мы можем создать наш release на основе tag’а.

Дальше мы рассмотрим, как получить аналогичный результат, но уже с другим инструментом.

Fastlane

Fastlane является инструментом, который позволяет автоматизировать рутинные задачи в разработке и развертывании мобильных приложений. Он позволяет описывать скрипты, которые будут решать конкретные задачи, однако он не заменяет полноценную CI/CD-систему. Fastlane интегрирован со множеством CI/CD-систем, в которых мы уже можем вызывать нужные нам скрипты для выполнения наших задач.

Из основного, что он умеет, можно выделить следующее:

  1. Автоматизация сборки IOS и Android-приложений;

  2. Автоматизация тестирования;

  3. Публикация приложений в Play Store и App Store;

  4. Работа со скриншотами;

  5. Распространение тестовых сборок;

  6. Интеграция с CI/CD-системами.

Подробную настройку Fastlane описывать не буду, т.к. это описано в официальной документации. Выделю лишь несколько ключевых моментов:

  1. Fastlane для работы использует Ruby, который у вас скорее всего не установлен. Поэтому первым делом необходимо его установить;

  2. Для корректной работы с зависимостями рекомендуется использовать Bundler, значит его тоже необходимо установить;

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

В итоге у нас получится следующая структура:

{root} |-- Gemfile |-- Gemfile.lock |-- fastlane   |-- Appfile   |-- Fastfile

Где:

  1. Appfile — определяет общую конфигурацию для всего приложения;

  2. Fastfile в котором мы определяем наши скрипты;

  3. Gemfile и Gemfile.lock для определения зависимостей.

В Appfile нас интересует только пакет нашего приложения, поэтому укажем его.

package_name("com.example.package")

К Fastfile придем немного позже. Мы можем написать все наши скрипты прямо в нем, но тогда он может раздуться. Поэтому давайте вынесем их в отдельные Fast-файлы.

Fastlane. Публикация приложения

Начнем с публикации приложения в Play Store, ранее мы уже произвели все подготовительные этапы и теперь нам остается только определить наш скрипт используя Fastlane. Для этого создадим файл DeployRelease по следующему пути: fastlane/lanes/DeployRelease. Название файла и его расположение можно задавать произвольное, как вам удобно.

Итогом имеем следующую структуру:

{root} |-- Gemfile |-- Gemfile.lock |-- fastlane   |-- Appfile   |-- Fastfile   |-- lanes     |-- DeployRelease

Ниже представлено содержание нашего DeployRelease файла:

platform :android do          lane :incrementVersionCode do           previous_version_code = google_play_track_version_codes(             package_name: "com.example.package",             track: "production", # Стоит по умолчанию             json_key_data: ENV["PLAY_CONSOLE_CREDENTIALS"]           )[0]              new_version_code = previous_version_code + 1           new_version_code       end          lane :buildProductionReleaseBundle do           gradle(             task: "bundle",             flavor: "Production",             build_type: "Release",             properties: {                 "android.injected.version.code" => incrementVersionCode               }           )       end          lane :buildProductionReleaseApk do           gradle(             task: "assemble",             flavor: "Production",             build_type: "Release",             properties: {                 "android.injected.version.code" => incrementVersionCode               }           )       end           lane :deployProductionRelease do           buildProductionReleaseBundle              supply(             package_name: "com.example.package",             track: "production",             aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],             json_key_data: ENV["PLAY_CONSOLE_CREDENTIALS"],             release_status: "inProgress",             rollout: "0.6",             skip_upload_metadata: true,             skip_upload_images: true,             skip_upload_screenshots: true           )       end   end

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

platform :{android/ios} do ... end

Сами скрипты обозначаются как lane, их мы можем вызывать из других lanes или терминала. Здесь мы указываем нашу логику, которая должна быть выполнена, можем передавать в него аргументы, а так же возвращать результат.

lane :{Название функции} do   ... end

При использовании Gradle Play Publisher у нас была очень удобная опция: мы могли указать, как стоит разрешать конфликты с versionCode, в нашем случае мы приняли инкремент на 1 от versionCode в Play Store. В Fastlane такой опции нет, поэтому нам самим надо написать скрипт incrementVersionCode для увеличения версии на +1 от текущей в Play Console.

Перед тем как продолжить, введу 2 термина, которые мы будем использовать в дальнейшем: action — это скрипты, которые идут из под коробки в Fastlane, а plugin — это скрипты, которые добавляются из вне, так же вы сами можете написать свои плагины и работать с ними. Подробнее про actions читаем тут, а про работу с plugins здесь.

Давайте разбираться, google_play_track_version_codes action возвращает список версий, которые есть у нас в Play Console, из которого мы берем первую (последнюю добавленную). По аргументам тут все довольно просто: package_name название пакета нашего приложения, track — internal, alpha, beta, production (по умолчанию берется production, но мы его на всякий случай все равно указываем), json_key_data — JSON-ключ, который мы ранее получили. Ну и дальше мы увеличиваем значение на +1 и возвращаем результат.

lane :incrementVersionCode do       previous_version_code = google_play_track_version_codes(         package_name: "com.example.package",         track: "production", # Стоит по умолчанию         json_key_data: ENV["PLAY_CONSOLE_CREDENTIALS"]       )[0]        new_version_code = previous_version_code + 1       new_version_code   end 

Дальше мы определим buildProductionReleaseBundle и buildProductionReleaseApk для сборки разных build flavors нашего приложения, с этим нам поможет gradle action. В properties мы устанавливаем новое значение versionCode. После сборки приложения, в lane_context мы получаем путь к сборке, которым позже сможем воспользоваться. Более подробно можно прочитать в документации по gradle action и документации по lanes.

lane :buildProductionReleaseBundle do       gradle(         task: "bundle",         flavor: "Production",         build_type: "Release",         properties: {             "android.injected.version.code" => incrementVersionCode           }       )   end      lane :buildProductionReleaseApk do       gradle(         task: "assemble",         flavor: "Production",         build_type: "Release",         properties: {             "android.injected.version.code" => incrementVersionCode           }       )   end

И, наконец, пришло время для загрузки сборки в Play Console, для этого мы соберем bundle используя наш buildProductionReleaseBundle скрипт. Для работы с Play Console воспользуемся supply action, он позволяет гибко работать с консолью разработчика.

lane :deployProductionRelease do       buildProductionReleaseBundle        supply(         package_name: "com.example.package",         track: "production",         aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],         json_key_data: ENV["PLAY_CONSOLE_CREDENTIALS"],         release_status: "inProgress",         rollout: "0.6",         skip_upload_metadata: true,         skip_upload_images: true,         skip_upload_screenshots: true       )   end 

Отдельно хочется поговорить про название сборки в Play Console и описание изменений. По умолчанию название сборки будет браться из вашего versionName в build.gradle. Можно создать version_name.txt файл в корне проекта, откуда будет браться название версии, а дальше уже использовать его в нашем build.gradle. А для описания изменений, уже надо будет создать новый файл в fastlane/metadata/android/ru-RU/changelogs/{versionCode вашей будущей версии}.txt и в него добавить описание всех изменений в новой сборке. Пример структуры файлов изображен ниже.

{root} |-- version_name.txt |-- fastlane   |-- Appfile   |-- Fastfile   |-- metadata     |-- android       |-- ru-RU         |-- changelogs           |-- 1.txt           |-- 2.txt           |-- {versionCode вашей будущей версии}.txt

Переменные окружения:

Название переменной

Описание

PLAY_CONSOLE_CREDENTIALS

JSON-ключ для работы с Play Console.

Fastlane. Загрузка сборки в s3

С публикацией приложения разобрались, теперь вспомним, что еще мы хотим отправлять сообщение о новой версии в нашу Telegram-группу. Поэтому займемся этим вопросом. Начнем с Aws, для этого создадим файл fastlane/lanes/Aws.

{root} |-- Gemfile |-- Gemfile.lock |-- fastlane   |-- Appfile   |-- Fastfile   |-- lanes     |-- DeployRelease     |-- Aws

После добавим наш uploadApkToS3 скрипт в него.

lane :uploadApkToS3 do |options|       apk_path = options[:apk_path] || lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]          if apk_path.nil?           UI.user_error!("The 'apk_path' parameter must not be null")       end          s3_path = "s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk"      endpoint = "https://storage.yandexcloud.net"          sh("aws", "s3", "cp", apk_path, s3_path, "--endpoint-url", endpoint)          presigned_url = sh("aws", "s3", "presign", s3_path, "--endpoint-url", endpoint, "--expires-in", "604800", log: false).strip       Actions.lane_context["APK_DOWNLOAD_URL"] = presigned_url          presigned_url   end

Здесь наш lane принимает уже параметры на входе, в данном случае это путь к APK-файлу. Конструкция || позволяет устанавливать значения по умолчанию. Подробней узнать про GRADLE_APK_OUTPUT_PATH и другие доступные значения можно здесь.

lane :uploadApkToS3 do |options|       apk_path = options[:apk_path] || lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]  

На всякий случай проверяем переменную apk_path на null и в случае необходимости завершаем работу с ошибкой. Подробнее об этом написано тут.

if apk_path.nil?       UI.user_error!("The 'apk_path' parameter must not be null")   end 

Дальше сохраняем в переменную s3_path путь, куда сохранить наш файл в s3 и endpoint с которым мы будем работать.

s3_path = "s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk"  endpoint = "https://storage.yandexcloud.net"  

В конце добавляем скрипт, который мы использовали ранее. Здесь мы используем sh action для работы с Shell командами.

Для работы с s3 есть соответствующий plugin, которым мы не пользуемся, но почему? Дело в том, что из под коробки данный plugin не работает с Yandex Object Storage и с pre-signed urls. По крайней мере я не нашел информации об этом, однако если вы уже сталкивались с этой проблемой, то буду рад увидеть ваш ответ в комментариях. Возвращаясь к проблеме, для ее решения мы можем воспользоваться sh action и просто работать с Yandex Object Storage из командной строки.

sh("aws", "s3", "cp", apk_path, s3_path, "--endpoint-url", endpoint)      presigned_url = sh("aws", "s3", "presign", s3_path, "--endpoint-url", endpoint, "--expires-in", "604800", log: false).strip

Так же прошу заметить, что при работе с sh action мы передаем команды в виде списка аргументов. Это общая рекомендация при работе с system и sh actions, которая продиктована необходимостью экранирования аргументов в Shell. Однако мы все еще можем передать нашу shell-команду в виде строки.

sh("aws s3 cp #{apk_path.shellescape} #{s3_path.shellescape} --endpoint-url #{endpoint.shellescape}")

После, можно записать наш presigned_url в lane_context для использования в других функциях, это скорее опциональный вариант, я лишь показал как можно.

Actions.lane_context["APK_DOWNLOAD_URL"] = presigned_url  

Fastlane. Работа с Telegram

Мы сохранили наш APK в s3, теперь можно переходить к работе с Telegram, чтобы отправлять сообщения о новой версии в нашу группу. Упростим себе жизнь и воспользуемся готовым Telegram plugin.

Для этого в терминале выполним следующую команду:

fastlane add_plugin telegram

После чего у вас появится новый файл Pluginfile, а так же обновятся Gemfile и Gemfile.lock. Не забудьте сохранить все в вашей системе контроля версий.

{root} |-- Gemfile |-- Gemfile.lock |-- fastlane   |-- Appfile   |-- Fastfile   |-- Pluginfile   |-- lanes     |-- DeployRelease     |-- Aws

Убедитесь, что в Gemfile появились следующие строки.

plugins_path = File.join(File.dirname(__FILE__), "fastlane", "Pluginfile")   eval_gemfile(plugins_path) if File.exist?(plugins_path)

Теперь создадим новый файл fastlane/lanes/Telegram, куда запишем наш sendMessageToTelegram скрипт. Здесь нам уже все знакомо, мы передаем аргументы в функцию, проверяем на null. А вот с переменной formatted_message уже интереснее, здесь мы пользуемся Ruby Heredoc для формирования многострочного сообщения. После чего мы уже отправляем сообщение в нашу группу используя telegram плагин.

lane :sendMessageToTelegram do |options|       title = options[:title]       changelog = options[:changelog]       download_url = options[:download_url]          if title.nil?           UI.user_error!("The 'title' parameter must not be null")       end          if changelog.nil?           UI.user_error!("The 'changelog' parameter must not be null")       end          if download_url.nil?           UI.user_error!("The 'download_url' parameter must not be null")       end          formatted_message = <<~MSG           #{title}              #{changelog}              #{download_url}       MSG          telegram(         token: ENV["TELEGRAM_BOT_TOKEN"],         chat_id: ENV["TELEGRAM_CHAT_ID"],         text: formatted_message       )   end

Переменные окружения:

Название переменной

Описание

TELEGRAM_BOT_TOKEN

Токен Telegram-бота

TELEGRAM_CHAT_ID

Id Telegram чата, в который будет отправляться сообщение

Fastlane. Работа с Git

Release-сборка загружена в Play Console на проверку, в Telegram появилось наше сообщение, теперь остается создать пометку об изменениях в Gitlab tags. Для этого создадим новый файл fastlane/lanes/Git, где запишем наш createGitTag скрипт. Тут я уже думаю для вас ничего нового нет, все настройки аналогичны предыдущим нашим скриптам.

lane :createGitTag do |options|       tag_name = options[:tag_name]       message = options[:message]          if tag_name.nil?           UI.user_error!("The 'tag_name' parameter must not be null")       end          if message.nil?           UI.user_error!("The 'message' parameter must not be null")       end          bot_token = ENV["CI_BOT_TOKEN"]     project_url = ENV["CI_PROJECT_URL"]     remote_url = "https://oauth2:#{bot_token}@#{project_url}"          sh("git", "remote", "set-url", "origin", remote_url)       sh("git", "config", "--global", "user.email", ENV["CI_BOT_EMAIL"])       sh("git", "config", "--global", "user.name", ENV["CI_BOT_USERNAME"])          sh("git", "tag", "-a", "-f", tag_name, "-m", message)       sh("git", "push", "-f", "origin", tag_name) end

Переменные окружения:

Название переменной

Описание

CI_BOT_TOKEN

Токен доступа для бота, который будет работать с Gitlab tags.

CI_PROJECT_URL

URL проекта в формате gitlab.ru/{Путь к проекту}.git

CI_BOT_EMAIL

Email бота от имени которого будет создаваться запись.

CI_BOT_USERNAME

Username бота от имени которого будет создаваться запись.

Fastlane. Настраиваем CI/CD

Мы написали скрипты для создания release-сборок, а так же для работы с Telegram, теперь осталось соединить все вместе. Вернемся к нашему Fastfile, в нем мы подключим наши Fast-файлы со скриптами, а так же напишем один небольшой скрипт, в котором мы будем собирать APK и затем отправлять его в нашу группу.

Для начала необходимо импортировать все наши скрипты в главный Fastfile.

import("./lanes/DeployRelease")   import("./lanes/Git")   import("./lanes/Aws")   import("./lanes/Telegram")

Теперь определим новый notifyReleaseToTelegram скрипт, в котором мы организуем сборку APK, а после отправим сообщение о нашем релизе. Здесь мы воспользуемся определенными ранее скриптами.

Сначала соберем APK используя buildProductionReleaseApk скрипт, затем загрузим его в s3 и получим ссылку на скачивание с помощью uploadApkToS3. В конце, создадим сообщение и отправим его в нашу Telegram-группу благодаря sendMessageToTelegram скрипту.

platform :android do          lane :notifyReleaseToTelegram do |options|           version = options[:version]           changelog = options[:changelog]              if version.nil?               UI.user_error!("The 'version' parameter must not be null")           end              if changelog.nil?               UI.user_error!("The 'changelog' parameter must not be null")           end              buildProductionReleaseApk           download_url = uploadApkToS3              sendMessageToTelegram(               title: "#PRODUCTION_RELEASE #{version}",               changelog: changelog,               download_url: download_url           )       end   end

Ну вот и все, теперь осталось настроить окружение в нашей Job’е и запустить в ней Fastlane-скрипты. Полный Job-скрипт изображен ниже.

deployReleaseUsingFastlane:     stage: deploy_release     variables:       LC_ALL: "en_US.UTF-8"       LANG: "en_US.UTF-8"     script:       - curl -sSL https://get.rvm.io | bash -s stable       - source /usr/local/rvm/scripts/rvm       - rvm install 3.2.2          - bundle install       - bundle exec fastlane install_plugins          - export VERSION_NAME="#PRODUCTION_RELEASE  $(cat version_name.txt)"       - export CHANGELOG="$(cat changelog.txt)"          - bundle exec fastlane deployRelease       - bundle exec fastlane notifyReleaseToTelegram version:${VERSION_NAME} changelog:${CHANGELOG}       - bundle exec fastlane createGitTag tag_name:${VERSION_NAME} message:${CHANGELOG}    rules:       - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'

Fastlane требует следующие переменные окружения для корректной работы. В частности, если в shell профайле locale не установлен UTF-8, то сборка и загрузка вашего приложения будет работать некорректно. Подробнее можно прочитать здесь.

variables:       LC_ALL: "en_US.UTF-8"       LANG: "en_US.UTF-8" 

Дальше мы скачиваем и устанавливаем RVM (Ruby Version Manager), чтобы потом установить нужную версию Ruby.

curl -sSL https://get.rvm.io | bash -s stable

Загружаем RVM в наш PATH, чтобы потом работать с ним из командной строки. (В Linux, здесь система ищет исполняемые файлы, когда мы работаем с ними из командной строки).

source /usr/local/rvm/scripts/rvm

Устанавливаем Ruby нужной нам версии, но почему же 3.2.2? Вы можете спокойно указать наиболее подходящую вам версию. Cкажу лишь, что лучше устанавливать Ruby версии 3+, т.к. в них из под коробки уже установлен bundler для работы с зависимостями и его не придется устанавливать отдельно. Но на совсем новых версиях Fastlane может работать нестабильно.

rvm install 3.2.2

Теперь загрузим все зависимости, которые определены в наших Gemfile и Gemfile.lock с помощью bundler.

bundle install

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

bundle exec fastlane install_plugins

Ранее мы создавали version_name.txt файл для названия версии и changelog.txt для Gitlab tags. Мы можем брать значения из них для описания изменений.

export VERSION_NAME="#PRODUCTION_RELEASE  $(cat version_name.txt)"   export CHANGELOG="$(cat changelog.txt)"  

В конце запускаем наши Fastlane-скрипты, здесь мы загружаем сборку в Play Console, после чего уведомляем о новом релизе в Telegram-группе и последним шагом создаем пометку с изменениями.

bundle exec fastlane deployRelease   bundle exec fastlane notifyReleaseToTelegram version:${VERSION_NAME} changelog:${CHANGELOG} bundle exec fastlane createGitTag tag_name:${VERSION_NAME} message:${CHANGELOG}

С Fastlane закончили, и, мне бы хотелось сказать, что Fastlane — это не только про публикацию приложения в Store’ах. С помощью данного инструмента можно настроить запуск тестов, раскатку тестовых сборок и т.д., т.е. это довольно гибкий инструмент для настройки нашего CI/CD.

Создаем свой Docker-образ

До этого момента мы пользовались docker-образом jangrewe/gitlab-ci-android:33, в котором идет только Android SDK и Java 11. Почти в каждой Job’е при запуске мы заново загружали новое окружение для работы нашего pipeline, например python, awscli, ruby и т.д. Это достаточно неэффективный путь, т.к. окружение из Job’ы к Job’е может дублироваться, дополнительно к этому, время выполнения нашего CI/CD pipeline может быть увеличено за счет времени, которое тратится на установку нужного инструмента.

Чтобы решить проблему с окружением, его можно заранее настроить, для этого мы создадим свой Docker-образ. С помощью него мы и настроим наше окружение, чтобы потом просто переиспользовать его в нашем CI/CD.

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

FROM jangrewe/gitlab-ci-android:33      ENV LC_ALL="en_US.UTF-8" \       LANG="en_US.UTF-8" \       MARATHON_VERSION="1.0.46"      RUN apt-get update && \       apt-get install -y --no-install-recommends \           openjdk-17-jdk \           curl \           gnupg2 \           python3-pip \           tar \           bash && \           gpg2 --keyserver hkp://keyserver.ubuntu.com --recv-keys \               409B6B1796C275462A1703113804BB82D39DC0E3 \               7D2BAF1CF37B13E2069D6956105BD0E739499BDB && \           rm -rf /var/lib/apt/lists/*    RUN curl -sSL https://get.rvm.io | bash -s stable && \       /bin/bash -lc "rvm requirements" && \       /bin/bash -lc "rvm install 3.2.2"    ENV PATH="/usr/local/rvm/gems/ruby-3.2.2/bin:/usr/local/rvm/rubies/ruby-3.2.2/bin:/usr/local/rvm/bin:$PATH"    RUN pip3 install awscli==1.36.0      RUN curl https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz --output /tmp/google-cloud-sdk.tar.gz && \       mkdir -p /google && \       tar zxf /tmp/google-cloud-sdk.tar.gz --directory /google && \       /google/google-cloud-sdk/install.sh --quiet      ENV PATH="/google/google-cloud-sdk/bin:$PATH"      RUN curl -L https://github.com/MarathonLabs/marathon-cloud-cli/releases/download/${MARATHON_VERSION}/marathon-cloud-v${MARATHON_VERSION}-{Архив, который нам нужен} -o /tmp/marathon-cloud && \       mkdir -p /marathon && \       tar -xzf /tmp/marathon-cloud --directory /marathon && \       mv /marathon/marathon-cloud-v${MARATHON_VERSION}-{Архив, который нам нужен}/marathon-cloud /usr/local/bin/ && \       chmod +x /usr/local/bin/marathon-cloud    WORKDIR /app      CMD ["bash"]

Описание нашего Docker-образа начинается с инструкции FROM, которая задает базовый образ, поверх которого мы будем строить свой собственный.

FROM jangrewe/gitlab-ci-android:33

Устанавливаем переменные окружения.

ENV LC_ALL="en_US.UTF-8" \       LANG="en_US.UTF-8" \       MARATHON_VERSION="1.0.46" 

Дальше устанавливаем зависимости через apt (Advanced Packaging Tool).

Где:

  • openjdk-17-jdk может понадобиться нам, если мы используем 17 версию Java, либо любую другую, которая используется у вас на проекте;

  • curl понадобится для скачивания и установки Marathon или Google Cloud SDK;

  • gnupg2 необходим для скачивания и установки RVM;

  • python3-pip для установки awscli и работы с google cloud;

  • tar, bash понадобятся для распаковки архивов и выполнения скриптов.

Следующим шагом устанавливаем GPG-ключи для верификации RVM, поскольку они требуются при установке RVM. И после всего, чистим кэш apt с помощью rm -rf /var/lib/apt/lists/*, чтобы уменьшить размер образа.

RUN apt-get update && \       apt-get install -y --no-install-recommends \           openjdk-17-jdk \           curl \           gnupg2 \           python3-pip \           tar \           bash && \           gpg2 --keyserver hkp://keyserver.ubuntu.com --recv-keys \               409B6B1796C275462A1703113804BB82D39DC0E3 \               7D2BAF1CF37B13E2069D6956105BD0E739499BDB && \           rm -rf /var/lib/apt/lists/*

Теперь скачиваем и устанавливаем RVM и Ruby. После установки Ruby, добавляем его в PATH, о том, что это такое я уже писал выше.

RUN curl -sSL https://get.rvm.io | bash -s stable && \       /bin/bash -lc "rvm requirements" && \       /bin/bash -lc "rvm install 3.2.2"      ENV PATH="/usr/local/rvm/gems/ruby-3.2.2/bin:/usr/local/rvm/rubies/ruby-3.2.2/bin:/usr/local/rvm/bin:$PATH"  

Ставим awscli.

RUN pip3 install awscli==1.36.0 

Дальше ставим Google Cloud SDK для работы с Firebase Test Lab, чтобы запускать наши Android-тесты с его помощью.

RUN curl https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz --output /tmp/google-cloud-sdk.tar.gz && \       mkdir -p /google && \       tar zxf /tmp/google-cloud-sdk.tar.gz --directory /google && \       /google/google-cloud-sdk/install.sh --quiet   ENV PATH="/google/google-cloud-sdk/bin:$PATH" 

Для запуска Android-тестов на Marathon, нам понадобится установить Marathon Cloud CLI.

RUN curl -L https://github.com/MarathonLabs/marathon-cloud-cli/releases/download/${MARATHON_VERSION}/marathon-cloud-v${MARATHON_VERSION}-{Архив, который нам нужен} -o /tmp/marathon-cloud && \       mkdir -p /marathon && \       tar -xzf /tmp/marathon-cloud --directory /marathon && \       mv /marathon/marathon-cloud-v${MARATHON_VERSION}-{Архив, который нам нужен}/marathon-cloud /usr/local/bin/ && \       chmod +x /usr/local/bin/marathon-cloud  

Предпоследним шагом задаем рабочую директорию, где будут выполняться все остальные наши команды в CI/CD.

WORKDIR /app

И последним шагом с помощью CMD указываем команду, которую необходимо выполнить, когда контейнер запущен.

CMD ["bash"]

Теперь осталось собрать наш образ и загрузить на Docker Hub. Для начала зарегистрируемся на Docker.com, после регистрации скачаем Docker Desktop на наш ПК. Далее, для удобства можно установить Docker Plugin для IntelliJ IDEA и уже работать через него.

Для сборки нашего образа, нажимаем на Build Image for... и ждем, когда образ будет собран.

Как только образ собран, в среде разработки переходим в Services > {Выбираем наш образ} > Dashboard, нажимаем на Tags Add... и указываем название нашего репозитория и версию образа.

Теперь осталось отправить наш образ в Docker Hub, для этого переходим в Docker Desktop, который мы установили ранее. Переходим во вкладку Images, в списке находим наш образ и нажимаем Push to Docker Hub. После этого, наш образ можно будет увидеть в личном кабинете Docker Hub, а значит и использовать его в нашем CI/CD.

После того, как наш Docker-образ будет готов, мы сможем облегчить CI/CD убрав команды, отвечающие за установку и настройку окружения. Для примера, ниже я представил упомянутый ранее скрипт публикации нашего Android-приложения с использованием Fastlane, предварительно убрав ненужные команды.

deployReleaseUsingFastlane:     stage: deploy_release     image: {Указываем наш Docker-образ для данной задачи}   script:       - bundle install       - bundle exec fastlane install_plugins      - export VERSION_NAME="#PRODUCTION_RELEASE  $(cat version_name.txt)"       - export CHANGELOG="$(cat changelog.txt)"          - bundle exec fastlane deployRelease       - bundle exec fastlane notifyReleaseToTelegram version:${VERSION_NAME} changelog:${CHANGELOG}       - bundle exec fastlane createGitTag tag_name:${VERSION_NAME} message:${CHANGELOG}    rules:       - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'

ВАЖНО
Приведенный выше пример Docker-образа является избыточным. В нем я привел инициализацию всего окружения, которое мы использовали на протяжении всех 3-х статей про CI/CD. В реальности не стоит делать ваш Docker-образ избыточным, добавляя туда все зависимости, т.к. в этом случае вы вряд-ли ускорите ваш pipeline ввиду тяжелого образа.

Лучшим вариантом будет создание нескольких Docker-образов под определенные задачи, например, для Android-тестов на Marathon сделать свой образ, куда войдет Marathon Cloud CLI и Android SDK, а для Firebase Test Lab сделать свой образ. Тем самым вы облегчите размер ваших Docker-образов.

Более подробно про оптимизацию вашего Gitlab CI/CD, можно прочитать в этой замечательной статье.

Заключение

Вот мы и подошли к концу нашей серии статей про Gitlab CI/CD для Android-проекта. Мы построили свой собственный CI/CD, который покрывает базовые потребности по сборке, публикации, тестированию нашего приложения. Рассмотрели разные инструменты для запуска Android-тестов, а так же разобрали разные варианты публикации нашего приложения в Play Store. И немного коснулись создания собственных Docker-образов для улучшения нашего CI/CD.

Темы, которые мы затрагивали, были достаточно обширными, но я постарался более подробно, пусть и по верхам, описать работу с каждым инструментом. Надеюсь, что кому-нибудь данная серия статей поможет при настройке собственного CI/CD.

Еще увидимся!


ссылка на оригинал статьи https://habr.com/ru/articles/925092/