Привет! Меня зовут Даша, я Android-разработчик в команде онлайн-кинотеатра PREMIER и я хочу с вами поделиться историей как мы начали приводить в порядок Gitlab CI скрипты :)
В нашем проекте стало много вариантов сборок и чтобы не тратить кучу времени на ожидание и поиск необходимого билда нам нужно было хотя бы получить отбивку об окончании работы джобы. А затем - решить неудобства с копипастой, чтобы поддержка скриптов не вызывала выгорание:) Погнали!
Собираем
Начнем с простейшего скрипта в .gitlab-ci.yml:
image: jangrewe/gitlab-ci-android
stages:
- build
_Google_ProdBundle:
stage: build # стейдж сборки приложения
when: manual # запуск только вручную
script:
- ./gradlew -Pci --console=plain :app:bundleProdGoogleRelease -Pbuildnum="$CI_JOB_ID"
artifacts:
name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
paths:
- sources/app/build/outputs/bundle/prodGoogleRelease/
# путь к артефактам
expire_in: 1 week
# время жизни артефакта, у нас он должен храниться только 1 неделю
Мы используем image jangrewe/gitlab-ci-android, так как в нем уже есть Android SDK и другие библиотеки для сборки приложений для Android. Все статические переменные, касающиеся безопасности, лежат в Gitlab Variables (Settings -> CI/CD -> Variables).
Здесь у нас запускается сборка Google-варианта bundleProdGoogleRelease с параметром Pbuildnum, который мы используем в качестве кода сборки (versionCode), и также записываем его в название сборки, чтобы соотнести ее с джобой. Pbuildnum — это project property, который можно достать в самом gradle.build таким образом:
versionCode = project.property('buildnum').toString().toBigDecimal()
Как использовать project properties — вы можете прочитать здесь.
CI_JOB_ID — идентификатор джобы, используется для указания версии сборки.
CI_JOB_STAGE — стейдж джобы, CI_COMMIT_REF_NAME — имя ветки.
Также здесь имеется параметр expire_in, который указывает на время хранения готового артефакта на сервере, здесь у нас это 1 неделя. Запуск производится вручную во избежание высокой нагрузки на сервер.
В данной джобе использую предопределенные переменные Gitlab CI, такие как CI_JOB_STAGE, CI_COMMIT_REF_NAME, CI_JOB_ID, но также существует множество других, о которых описано в документации Gitlab CI.
Выглядит просто: джоба отработала — артефакт готов. На этом этапе пайплайн в работе оказывал негативный эффект на нашу психику, а глаза краснели во время поиска необходимой сборки...
Оповещаем
…а можно было бы просто получить ссылку в мессенджере сразу и скачать артефакт оттуда :)
Мессенджер можно использовать любой, а я покажу на примере Slack.
Для реализации отправки сообщений в Slack необходимо сгенерировать webhook, данный процесс детально описан в документации.
_Google_ProdBundle:
stage: build
…
#
…
after_script:
-
- >-
curl -X POST --data-urlencode 'payload={"channel": "'$SLACK_CI_CHANNEL'", "username": "AndroidCI", "text": ":white_check_mark: *Premier-Android*: Готов новый артефакт *'${CI_JOB_NAME}'* <'${ARTIFACT_URL}'|'${CI_JOB_ID}'>" }' $SLACK_WEBHOOK
Блок after_script вызывается после выполнения основного блока script.
Здесь мы отправляем POST-запрос с параметром --data-urlencode, где указали шаблон сообщения и полученный ранее SLACK_WEBHOOK.
ARTIFACT_URL у нас формируется следующим образом:
${JOBS_PATH}${CI_JOB_ID}${ARTIFACTS_ARCHIVE_PATH}${ARTIFACT_PATH}
JOBS_PATH — это путь к списку джоб в Gitlab.
CI_JOB_ID — идентификатор выбранной джобы.
Для того, чтобы сформировать ссылку на браузер в ARTIFACTS_ARCHIVE_PATH, мы указываем /artifacts/browse/, а для ссылки на скачивание — /artifacts/file/.
И указываем в ARTIFACT_PATH путь bundle или apk-файлам.
И вот в Slack нам прилетает такое сообщение:
И по клику на номер джобы откроется окно с артефактом, который можно будет скачать:
Вуаля, запустил сборку, получил уведомление со ссылкой, скачал и пошел искать баги :)
Вот мы и настроили сборку и отправку сообщений: коллеги выбирают тип сборки, нажимают на кнопочку и ждут оповещения.
Вроде бы выглядит нормально, да и для небольших проектов и этого достаточно :) Но не торопитесь радоваться! Нас остановила полиция копипасты и выписывает нам штраф. А за что?
Внимательные читатели поняли, что для каждого вида сборки нам надо копировать весь вышеперечисленный код и исправлять пару строчек :)
А именно:
./gradlew -Pci --console=plain :app:bundleProdGoogleRelease -Pbuildnum="$CI_JOB_ID"
paths:
- sources/app/build/outputs/bundle/prodGoogleRelease/
Непорядок, нам нужно это исправить, иначе наш .gitlab-ci.yml разрастется до огромных размеров, и как потом такое поддерживать… Приступим!
Модернизируем
Первым делом выделим группы пайплайнов, чтобы в каждом лежали только скрипты, выполняющие одно и то же для каждой сборки.
Для этого необходимо использовать модификатор include в .gitlab-ci.yml.
После изменений файл .gitlab-ci.yml начал выглядеть так:
image: jangrewe/gitlab-ci-android
include:
- local: .gitlab-ci-merge.yml
- local: .gitlab-ci-build.yml
- local: .gitlab-ci-build-tv.yml
- local: .gitlab-ci-deploy.yml
- local: .gitlab-ci-huawei.yml
- local: .gitlab-ci-browserstack.yml
- local: .gitlab-ci-distribution.yml
- local: .gitlab-ci-code-analyze.yml
…
# VARIABLES
…
stages:
- analyze
- build
- deploy
- test
Далее мы выделим общие действия, которые используются во всех скриптах, и добавим их в наш gitlab-ci.yml. Для этого мы используем reference-теги для объявления общих скриптов.
Сохраняем переменные в variable.env для их дальнейшего переиспользования в следующей джобе, подробнее про переменные и их использование можно почитать здесь.
.base_task:
allow_failure: false
before_script:
- echo "LAST_JOB_URL=${CI_JOB_URL}" >> variable.env
- echo "LAST_JOB_ID=${CI_JOB_ID}" >> variable.env
- echo "LAST_JOB_NAME=${CI_JOB_NAME}" >> variable.env
- cat variable.env
artifacts:
reports:
dotenv: variable.env
Выделяем общий скрипт сборки:
.base_bundle_task:
variables:
ARTIFACT_NAME: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
script:
- ./gradlew -Pbuildnum="${CI_JOB_ID}" --console=plain :${APPLICATION}:bundle${VARIANT}
- echo "${JOBS_PATH}${CI_JOB_ID}${ARTIFACTS_ARCHIVE_PATH}${ARTIFACT_PATH}" > .var_artifact_url
- echo "SUCCESS" > .var_state
- echo "APPLICATION=${APPLICATION}" >> variable.env
- echo "LAST_JOB_ID=${CI_JOB_ID}" >> variable.env
- echo "VARIANT=${VARIANT}" >> variable.env
- echo "ARTIFACT_PATH=${ARTIFACT_PATH}" >> variable.env
artifacts:
name: "${ARTIFACT_NAME}"
paths:
- $ARTIFACT_PATH
expire_in: 1 week
Более подробно про передачу переменных между джобами описано здесь.
И добавляем код отправки сообщений со ссылками на сборки.
.with_notification:
allow_failure: false
before_script:
- echo ${SLACK_CI_CHANNEL} > .var_channel
- echo ${CI_JOB_URL} > .var_artifact_url
- echo "NONE" > .var_state
after_script:
- CHANNEL=$(cat .var_channel)
- ARTIFACT_URL=$(cat .var_artifact_url)
- STATE=$(cat .var_state)
- env
- >
if [ "${STATE}" == "SUCCESS" ]; then curl -X POST --data-urlencode 'payload={"channel": "'${CHANNEL}'", "username": "AndroidCI", "text": ":white_check_mark: *Premier-Android*: Готов новый артефакт *'${CI_JOB_NAME}'* <'${ARTIFACT_URL}'|'${CI_JOB_ID}'>" }' $SLACK_WEBHOOK; fi
Далее с помощью extends наследуем джобу от вышеописанных скриптов.
В итоге после выноса общего кода в отдельный файл .gitlab-ci-build.yml пайплайн для релизной Google-сборки выглядит вот так:
_Google_ProdBundle:
extends:
- .base_task
- .base_bundle_task
- .with_notification
stage: build
when: manual
variables:
ARTIFACT_PATH: "sources/app/build/outputs/bundle/prodGoogleRelease"
APPLICATION: "app"
VARIANT: "ProdGoogleRelease"
Таким образом, скрипт стал немного гибче, для других сборок нам достаточно только поменять вариант и путь к сборке. Этот пайплайн можно будет оптимизировать и дальше, но это уже другая история… :)
Хоть и ненамного, но мы упростили жизнь нам и нашим тестировщикам. Для сборки артефакта и отправки его в тест нам нужно только нажать одну кнопочку, вот и все!
З. Ы.: на этом мы не прощаемся, в следующих статьях автоматизируем еще что-нибудь :)
Комментарии (9)
WondeRu
19.12.2022 11:16Почему следующим шагом не пушите в Google Play? Я бы все делал через Fastlane. https://docs.fastlane.tools/actions/upload_to_play_store/
Наши мобильные приложения с помощью fastlane на iOS сразу улетают в testflight, а android в google play. Т.к. мы на React Native и код кроссплатформенный, то, используя несколько раннеров (докеры и маки), мы можем собирать проект под разные платформы в одном пайплайне.
vip777swag Автор
19.12.2022 11:34+1Привет! Это только первая статья, про публикации (без фастлейна) и не только в гугл будет в следующей статье
vitaly_il1
19.12.2022 21:22+1Спасибо, интересно!
А есть ли в GitLabCI какие-то плагины для "высокоуровневой" работы со Slack? Типа "actions" в GitHub - https://github.com/voxmedia/github-action-slack-notify-build?
vip777swag Автор
20.12.2022 12:08+1Привет! Для наших потребностей хватило и обычного POST запроса, а так решений для подобной "высокоуровневой" работы я не находила. Можно выделить shell скрипты как здесь https://github.com/Weinto/gitlab-ci-slack-notification, так же можно использовать reference тэги для выделения методов отправки сообщений https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html#reference-tags
dem0n3d
20.12.2022 18:39+1Спасибо за статью! Сам буквально вчера встал на этот нелегкий путь... Но, есть одно "но". Опробовал на gitlab.com, сразу же столкнулся с ошибкой: JOB ID там уже выходит за пределы инта. Рекомендую использовать $CI_PIPELINE_IID (внутренний для проекта номер пайплайна). PS Жду продолжения про подписание и доставку.
easty
Привет. Спасибо за статью. Как относится ИБ, к тому что вы используете jangrewe/gitlab-ci-android напрямую, без пересборки образа в свои регистри и проверкой образа ИБ?
vip777swag Автор
Привет! jangrewe/gitlab-ci-android был у нас первоначально, я добавила его в статью для примера. Мы уже собрали свой образ, в следующих статьях могу рассказать и про это :)