
О чём эта серия статей?
Всем привет, меня зовут Кирилл и я Android-разработчик в Scanny. Сразу оговорюсь, касаемо CI/CD, о котором я буду говорить - он достаточно простой и не претендует на идеальность и т.д. Если у вас есть предложения, как можно улучшить - буду рад видеть в комментариях.
Если говорить о мотивации написания данной серии статей, да, вы можете сказать, что уже не мало статей написано на Habr, да и на других источниках тоже. И будете абсолютно правы. Но на практике я столкнулся с 2 проблемами: во-первых, информации часто не хватало, чтобы сделать всё так, как нужно именно мне. А во-вторых, даже если ответы находились, они были раскиданы по разным источникам.
Поэтому в первую очередь, мне захотелось систематизировать знания для себя. Но верю, что чем больше будет понятных и доступных материалов, тем проще станет другим разобраться в теме.
Если коротко - моя главная мотивация написания "еще одной статьи про CI/CD на Android" - помочь себе и другим, закрыть свои потребности при работе с CI/CD. К тому же, на мой взгляд, каждая статья на эту тему получается разной - ведь задачи у всех разные.
Данная серия будет состоять из 3 статей:
Настраиваем CI/CD Android-проекта, часть 1. Начало.
Настраиваем CI/CD Android-проекта, часть 2. Запуск Android-тестов.
Настраиваем CI/CD Android-проекта, часть 3. Автоматизация публикации версий в Play Market.
Изначально была идея уместить всё в одной статье, но после пришёл к мнению, что вместе с объяснением, как подключать различные сервисы, статья раздуется до огромных размеров. Поэтому пусть это будут несколько небольших статей с объяснением как подключить и наладить CI/CD.
Теперь к сути
Официальное определение CI/CD звучит следующим образом: ... означает непрерывную интеграцию и непрерывную доставку/развертывание, которые нацелены на оптимизацию и ускорение жизненного цикла разработки ПО. С моей стороны, я это вижу как набор каких-либо практик, которые упрощают разработку и доставку продукта, а также хорошо экономят время, что не сильно то отличается от официальной трактовки.
Кому подойдет?
Если вы работаете над краткосрочным проектом, где после завершения работы вы к нему больше не вернетесь, то CI/CD, скорее, отнимет у вас время. В то же время, если вы работаете над долгоиграющим проектом, то CI/CD даст ускорение в тестировании и деплое вашего продукта.
Что будем делать?
Делать будем простой CI/CD на Gitlab, который будет покрывать базовые потребности. Ниже изображено схематичное представление нашего pipeline.

Теперь немного подробнее про каждый шаг:
1. Запуск статического анализатора кода
Запускать будем базовый, что называется "из под коробки", Android Lint. На самом деле существует не мало статических анализаторов кода, например, detekt, ktlint, SonarQube и многие другие. В pipeline статический анализатор кода будет запускаться при любом merge request'е. Отчёт будем сохранять в самых обычных артефактах, которые потом можно просмотреть на Gitlab.
2. Запуск тестов
Представим, что на вашем Android-проекте пишутся как Unit-тесты, так и Android-тесты. Unit-тесты проверяют какие-либо методы бизнес-слоя, без Android-составляющей.
Например, в нашей компании для написания Unit-тестов, в качестве основного набора мы используем AssertK, Mockito Kotlin и Kotlin coroutines. Android-тесты уже могут быть как UI, где мы используем Kaspresso, так и инструментальные тесты. Для этого можно использовать Robolectric или Kaspresso
, который уже упоминался. Мы для этих целей используем Kaspresso
, т.к. на мой взгляд, он очень удобен не только для UI, но и для инструментальных тестов.
Только вот встает вопрос: где и как запускать Android-тесты? Уже есть готовые инструменты, которые позволяют запускать тесты как на эмуляторах, так и на реальных устройствах. Я бы выделил 2 решения: Marathon Labs и Firebase Test Lab. Оба варианта предоставляют готовую инфраструктуру для запуска тестов, но если вам нужно более гибкое решение, то Marathon Labs
никак не ограничивает вас, где и как запускать ваши тесты. В свою очередь, Firebase Test Lab
не дает такой гибкости, но предоставляет возможность запустить тесты на реальных устройствах. Во второй части я расскажу, как подключить и запустить Android-тесты с помощью каждого из этих решений.
В pipeline Unit-тесты будут запускаться при любом merge request'е. А вот Android-тесты будут запускаться только при merge request'ах в master.
3. Сборка различных вариантов приложения
После сборки проекта, мы будем отправлять сборку в группу Telegram с описанием, которое будет браться из последнего commit'а. В pipeline сборка будет запускаться при merge request'е в develop.
4. Выкладываем release-сборку в Play Market
Тут всё просто, представим, что в проекте у нас постепенно накапливаются исправления багов, новые фичи и т.д. Впоследствии изменения войдут в release-сборку и она улетит в Play Market на проверку. Для этого мы рассмотрим работу с Gradle Play Publisher и Fastlane для публикации нашего приложения.
В pipeline сборка для Play Market будет собираться при merge request'е в master.
5. Создание Git-Tag в Gitlab
Для отслеживания истории изменений/release'ов будем использовать Git-Tag, тут все по классике. В pipeline теги будем отправлять в Gitlab при merge request'ах в master.
Переходим к CI/CD
Подготовка
Есть много инструментов для работы с CI/CD, например GitHub Actions, Jenkins, GitLab, мы будем использовать последний. Для работы с ним создадим файл .gitlab-ci.yml
в корне проекта, тут будет располагаться наш pipeline. Да, каждый этап можно вынести в отдельный файл и т.д., но начнем с простого и сделаем все в одном файле, более подробно про то, как выносить скрипты в отдельные файлы, можно почитать тут. Для создания файла тыкаем ПКМ по корню проекта -> New -> File -> Вводим название .gitlab-ci.yml
.

В итоге получим результат ниже. Который будет запускать наш Gitlab Runner.

Далее переходим непосредственно к коду.
image: jangrewe/gitlab-ci-android:33
# Задаем этапы
stages:
- lint
- tests
- build_flavors
- release
- create_git_tag
# Определяем общий before_script для всех jobs
before_script:
- chmod +x ./gradlew
Для сборки приложения необходим Android SDK и прочее окружение. Чтобы упростить себе жизнь, можно использовать уже готовый Docker image jangrewe/gitlab-ci-android.
image: jangrewe/gitlab-ci-android:33
В stages задаем основные этапы, которые должны выполняться последовательно.
stages:
- lint
- tests
- build_flavors
- release
- create_git_tag
В before_script
, строчка chmod +x ./gradlew
делает файл gradlew
исполняемым (дает ему права на выполнение). Подробнее про before_script читаем тут.
before_script:
- chmod +x ./gradlew
Статический анализатор кода
runAndroidLint:
stage: lint
script:
- ./gradlew lint
artifacts:
paths:
- "**/lint-results.html"
expire_in: 10 days
when:
always
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Первым запускается статический анализатор кода
при любом merge request'е.
Тут мы даем название нашей Job'е.
runAndroidLint:
В stage мы указываем, к какому этапу относится эта job'а. В нашем случае lint
.
stage: lint
В блоке script
непосредственно пишем сам скрипт того, что мы хотим выполнить. Здесь строчка ./gradlew lint
запускает базовый Android Lint.
script:
- ./gradlew lint
В блоке artifacts
мы настраиваем сохранение результатов работы. В данном случае в paths
указываем путь к файлам, которые мы хотим сохранить, в expire_in
указываем сколько дней будет храниться и в when
указываем при каких условиях сохраняем артефакты. Подробнее про artifacts можно прочитать здесь.
artifacts:
paths:
- "**/lint-results.html"
expire_in: 10 days
when:
always
В блоке rules
мы задаем условия, при которых наша job'а будет запускаться. В данном случае '$CI_PIPELINE_SOURCE == "merge_request_event"'
указывает на запуск при любом merge request'е. Подробнее про rules можно прочитать тут.
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Переменные окружения:
Название переменной |
Описание |
---|---|
CI_PIPELINE_SOURCE |
Хранит информацию о том, как был запущен pipeline. Тут описание возможных вариантов |
Unit-tests
Переходим к тестам, пока расскажу только про Unit-тесты, к Android-тестам вернемся в следующей статье, где более подробно посмотрим как связать Marathon Labs
и Firebase Test Lab
с нашим CI/CD.
runUnitTests:
stage: tests
script:
- ./gradlew app:test
- ./gradlew core:some-module:impl:test
...
artifacts:
paths:
- "**/build/reports/tests/testDebugUnitTest/classes"
- "**/build/reports/tests/testDebugUnitTest/packages"
- "**/build/reports/tests/testDebugUnitTest/index.html"
expire_in: 10 days
when:
always
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
В stage
все аналогично объяснению выше, за тем исключением, что теперь это этап тестирования;
stage: tests
Для запуска тестов в определенных модулях пользуемся схемой ниже.
./gradlew {Модуль с полным указанием пакетов}:test
Как пример, для запуска тестов в главном app модуле мы пишем просто app:test
, а для запуска тестов в других модулях уже указываем сам модуль с учетом пакетов. Как пример: core:some-module:impl:test
.
script:
- ./gradlew app:test
- ./gradlew core:some-module:impl:test
В artifacts
все аналогично описанию выше.
artifacts:
paths:
- "**/build/reports/tests/testDebugUnitTest/classes"
- "**/build/reports/tests/testDebugUnitTest/packages"
- "**/build/reports/tests/testDebugUnitTest/index.html"
expire_in: 10 days
when:
always
Build flavors
Далее на очереди у нас сборка различных вариантов приложения и публикация сборки в Telegram. Доступ к APK будет осуществляться по ссылке.
assembleSomeBuildFlavor:
stage: build_flavors
script:
- ./gradlew assembleSomeBuildFlavorDebug
- apt update
- apt install python3-pip --yes
- pip3 install awscli --upgrade
- export VERSION_NAME="TEST VERSION"
- export MR_COMMIT_MESSAGE=${CI_COMMIT_MESSAGE}
- aws s3 cp app/build/outputs/apk/{Ваш flavor name}/debug/{Ваш APK name}.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/{Ваш flavor name}/
expire_in: 10 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"'
Тут несколько интереснее, т.к. сборка улетает в Telegram.
В скрипте мы собираем наш APK.
script:
- ./gradlew assembleSomeBuildFlavorDebug
Ниже в таблице указаны команды для сборки различных вариантов приложения, подробнее про это тут (Варианты сборок) и тут (Сборка с помощью command line).
Тип сборки |
Как запустить сборку |
---|---|
APK |
./gradlew assemble{Variant name/flavor} |
Bundle |
./gradlew bundle{Variant name/flavor} |
В нашей Telegram-группе, доступ к APK будет осуществляться по ссылке на файл в облаке. Для этого мы будем использовать Amazon Web Services Command Line Interface или же AWS CLI, это инструмент, который позволяет управлять ресурсами Amazon Web Services из командной строки. И Yandex Object Storage для хранения файла APK в облаке. Более подробно про работу AWS CLI с Yandex Object Storage можно прочитать здесь.
Теперь давайте пробежимся по тому, что написано в нашем скрипте.
Сначала обновим списки пакетов, чтобы использовать актуальные версии.
- apt update
Затем устанавливаем python и pip (система управления пакетами). Где, --yes
указываем чтобы не подтверждать установку вручную.
- apt install python3-pip --yes
Далее устанавливаем утилиту для командной строки (awscli) о которой говорилось выше, для того чтобы делать ссылку на скачивание нашего приложения.
- pip3 install awscli --upgrade
После объявляем локальные переменные.
- export VERSION_NAME="TEST VERSION" #Название сборки
- export MR_COMMIT_MESSAGE=${CI_COMMIT_MESSAGE} #Описание сборки. В нашем случае текст берется как последний commit в ветке
Теперь загрузим наш собранный APK в s3-хранилище. Здесь мы указываем путь к файлу, который хотим загрузить, после указываем s3-bucket и путь по которому необходимо сохранить файл. Более подробно тут.
- aws s3 cp app/build/outputs/apk/{Ваш flavor name}/debug/{Ваш apk name}.apk s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk --endpoint https://storage.yandexcloud.net
Далее делаем скрипт ниже исполняемым, об этом будет написано позже, просто скажу, что он нам необходим, чтобы отправить сообщение с помощью бота в группу Telegram.
- chmod a+x ./upload_telegram_link.sh
Затем получаем Presigned Url, по которому можно будет получить доступ к приватному файлу в хранилище на протяжении некоторого времени. Так же дополнительно можно посмотреть тут и небольшой пример тут.
- aws s3 presign s3://{Задайте название бакета в s3}/{Задайте название файла в бакете}.apk --endpoint-url "https://storage.yandexcloud.net/" --expires-in 604800|source upload_telegram_link.sh
Отдельно хочу отметить, что в команде --expires-in 604800
мы указываем время жизни ссылки в секундах, что равняется 7 дням. А через символ |
(pipe) мы передаем результат предыдущей команды (в данном случае - сгенерированную ссылку) в следующую команду, где source upload_telegram_
link.sh
запускает наш скрипт (О нем чуть ниже будет написано).
--expires-in 604800|source upload_telegram_link.sh
В артефактах GitLab сохраняем наш APK.
artifacts:
paths:
- app/build/outputs/apk/{Ваш flavor name}/
expire_in: 10 days
Выполняем наш скрипт при MR в develop ветку.
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"'
Переменные окружения, подробнее про них тут:
Название переменной |
Описание |
---|---|
VERSION_NAME |
Локальная переменная, а именно название сборки |
MR_COMMIT_MESSAGE |
Локальная переменная, сообщения изменений в сборке, берется из CI_COMMIT_MESSAGE |
CI_COMMIT_MESSAGE |
Текст последнего коммита |
CI_MERGE_REQUEST_TARGET_BRANCH_NAME |
Содержит название ветки, в которую мы хотим влить изменения |
Про то, как создавать бота я писать не буду, думаю информации полно, а вот к разбору кода перейдем. Нам необходим скрипт для отправки сообщения в Telegram. Для этого тыкаем ПКМ по корню проекта -> New - > File -> Задаем название файлу, в нашем случае upload_telegram_
link.sh
, после чего пишем следующий код:
#!/bin/bash
encodeurl() {
python3 -c "import urllib.parse; import sys; url = sys.stdin.read(); print(urllib.parse.quote(url))"
}
TIME="10"
TG_API_URL="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage"
read S3URL
ENCODED_S3URL=$(echo ${S3URL} | encodeurl)
curl -s --max-time ${TIME} -d "chat_id=${TELEGRAM_CHAT_ID}&disable_web_page_preview=1&text=${VERSION_NAME} %0A${MR_COMMIT_MESSAGE} %0A${ENCODED_S3URL}" ${TG_API_URL} > /dev/null
Указываем, что данный файл должен выполняться с помощью интерпретатора Bash.
#!/bin/bash
Создаем функцию encodeurl
, которая будет кодировать специальные символы в URL формат. Это делается для того, чтобы наши ссылки на скачивание из s3 не сломались.
encodeurl() {
python3 -c "import urllib.parse; import sys; url = sys.stdin.read(); print(urllib.parse.quote(url))"
}
Устанавливаем время ожидания (timeout) для curl
в 10 секунд.
TIME="10"
Формируем URL, для отправки сообщения с помощью бота через API Telegram. Подробнее про команду sendMessage.
TG_API_URL="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage"
Cчитываем ссылку на скачивание из s3 (она передается нам из предыдущей команды через pipe).
read S3URL
Далее используем функцию encodeurl
для кодирования ссылки, которая будет использоваться в Telegram.
ENCODED_S3URL=$(echo ${S3URL} | encodeurl)
И наконец отправляем наше сообщение в группу.
curl -s --max-time ${TIME} -d "chat_id=${TELEGRAM_CHAT_ID}&disable_web_page_preview=1&text=${VERSION_NAME} %0A${MERGE_REQUEST_COMMIT_MESSAGE} %0A${ENCODED_S3URL}" ${TG_API_URL} > /dev/null
Где указываем chat id, куда отправлять сообщение.
chat_id=${TELEGRAM_CHAT_ID}
Отключаем превью веб-страниц для ссылок.
disable_web_page_preview=1
Переменные окружения:
Название переменной |
Описание |
---|---|
TELEGRAM_BOT_TOKEN |
Токен Telegram бота |
TELEGRAM_CHAT_ID |
Id Telegram чата, в который будет отправляться сообщение |
S3URL |
URL для скачивания нашей APK, который выдает s3 |
ENCODED_S3URL |
Локальная переменная, перекодированный URL для скачивания APK |
TIME |
Время максимального ожидания к подключению |
Создаем собственные переменные окружения в Gitlab
GitLab предоставляет удобный и гибкий механизм определения собственных переменных окружения. Для того, чтобы добавить свои переменные окружения через UI, необходимо зайти в ваш проект на Gitlab'е, далее переходим по следующим вкладкам: Settings -> CI/CD -> Variables.

Теперь мы можем добавлять наши переменные. Для этого нажимаем на Add variable
. И видим окно настроек новой переменной.
Тут нас интересуют ключ (key) - уникальное название наших переменных, значение самой переменной (value), по желанию описание (description). Видимость (visibility) лучше устанавливать как Masked
(скрыто в логах CI/CD) или Masked and hidden
(скрыто в логах CI/CD и в Gitlab UI).
ВАЖНО
Чувствительные секреты (токены, ключи и т.д.) лучше хранить за пределами Gitlab variables. Как пример, можно использовать Yandex Lockbox, Google Cloud Secret Manager или HashiCorp Vault, но об этом в другой статье.

На этом все, в итоге мы получаем такую красоту:

Полезные ссылки
Хочу сказать, что список далеко не исчерпывающий:
Заключение
Надеюсь кому-нибудь эта статья будет полезна. Если вам есть что добавить или исправить, милости прошу в комментарии! С удовольствием буду читать.
В следующей части мы рассмотрим 2 сервиса для Android-тестирования: Marathon Labs и Firebase Test Lab.
Про загрузку release-сборок в Play Market я расскажу в 3 части, где подключим необходимые сервисы и напишем pipeline. Создание тэгов в Gitlab будет в этой же статье.
Еще увидимся!
olku
APK ещё можно собирать и в GitLab Release публиковать по тегу, поставить артефакту 5 лет жизни. Не подскажете самый простой способ матрицу тестов сделать в GitLab на разных Android SDK?
kirlozavr Автор
Если правильно понял вопрос, то в целом можно задать необходимые версии ОС при конфигурации тестов (Marathon/Firebase).
Например для Firebase:
--device model=Nexus6,version=21 \
--device model=Nexus7,version=19
Для Marathon:
--os-version 10
kirlozavr Автор
Извиняюсь, что не предоставил ссылки на документацию, поэтому на всякий случаю оставлю их здесь. Документация для Firebase и для Marathon.