Я тружусь младшим разработчиком в отделе внутренней мобильной разработки VK. Когда я пришел в команду, у нас не было CI. При этом в одном репозитории у нас было семь приложений, и при каждом обновлении приходилось по отдельности их собирать, тратя на это кучу времени и сил. Я решил автоматизировать сборку, написав человеческий CI. И это — его история.
![](https://habrastorage.org/getpro/habr/upload_files/515/de5/c12/515de5c126a2145f5bae27fa7168d0a2.jpeg)
Первый заход
Во-первых, нам нужно было проверять коммиты и pull request’ы в dev-ветку. Во-вторых, при добавлении pull request’ов мы хотели получать apk-файлы для тестов (и заливать их в Firebase App Distribution), а при добавлении в master — получать aab-файлы для магазина приложений. В общем, задача выглядела простой, и я сел её реализовывать.
![](https://habrastorage.org/getpro/habr/upload_files/2be/3c3/5ca/2be3c35caf5399c60d62e8072764fc2d.png)
Получилось ровно так, как и хотел. Но поскольку приложений у нас много (flavor’ы), а под CI выделили Mac Mini 2014 года, то всё закончилось вполне закономерно: даже на более свежем и мощном Маке сборка выполнялась за 5-8 минут, а на Мини мы получили файлы приложений… лишь спустя 2 часа. Явно не тот результат, который мы хотели.
![](https://habrastorage.org/getpro/habr/upload_files/33f/134/567/33f1345679be49a9064186ed52ed384e.png)
Второй заход
Общий конвейер CI должен был остаться таким же, только нужно было ускорить сборку необходимых flavor’ов. Также нам хотелось автоматизировать поднятие версий, что для 7 приложений было не самой тривиальной задачей. Я начал искать решение. Как никогда кстати на канале Android Academy Global вышел доклад на тему CI, который помог мне нащупать идею и начать разработку.
Процесс разработки
Для начала исправил самое востребованное: коммит в ветку задачи и pull request с фичей в dev-ветку. В целом, сборка основного flavor’а была уже реализована, но на всякий случай я добавил и сборку случайного flavor’а, чтобы не пришлось долго чинить возникающие из-за ресурсов ошибки: например, если кто-то добавил нужный для всех flavor’ов файл в папку одного из них.
script:
- |
COMMANDS=("./gradlew --no-daemon --stacktrace assembleCollageDebug"
...) # тут остальные команды
- $(shuf -n1 -e "${COMMANDS[@]}")
![](https://habrastorage.org/getpro/habr/upload_files/7b6/c83/c64/7b6c83c649905f756264043f335a3491.png)
Поднятие версии
В сети есть много решений на Java и Python, но все они довольно громоздкие, поэтому не имело смысла вносить их в репозиторий или клонировать. Все наши версии нумеруются по шаблону <major>.<minor>.<patch>, и мы решили написать Gradle-задачу, которая поднимает нужный фрагмент версии. Версии всех flavor’ов добавили в файл versions.properties. Задача берёт версию и номер нужного flavor’а, увеличивает его на единицу, и поднимает у версии нужный фрагмент. Это выглядит так:
– файл version.properties до –
…
app_code=1260
app_name=2.0.60
…
– вызов команды –
./gradlew bumpVersion --flavor app --field minor
– файл после –
…
app_code=1261
app_name=2.1.0
…
Имя версии мы записываем в отдельный файл, а потом из него создаём ветку release/flavor-version. Затем делаем коммит и отправляем в эту ветку, версии уже подняты. Подробнее про коммит и пуш с GitLab CI можно посмотреть тут.
Триггер на задачу
Теперь нужно было настроить задачу, которая будет срабатывать по триггеру и выполнять описанные выше действия, мы ведь не хотим объединять и создавать ветки вручную. Эту функциональность мы хотели добавить в имевшийся у нас чат-бот, поэтому я начал изучать в Gitlab информацию о триггерах. Этот вопрос неплохо описан в документации, но можно не увидеть что-то нужное с первого раза. В итоге получился такой запрос:
request = requests.post(f'https://self-hosted-gitlab.com/api/v4/projects/{project_id}/trigger/pipeline/', data={
"token": token,
"variables[flavor]": flavor,
"variables[field]": field,
"ref": 'dev'
}).json()
А в gitlab-ci.yml условие выглядит так:
only:
variables:
- $CI_PIPELINE_SOURCE == "trigger" && $CI_COMMIT_BRANCH == "dev" && $flavor == "app""
В этой задаче происходит вся магия.
Pull request’ы
После того, как мы подняли версию и запушили код в ветку, должна отработать другая задача, которая создаёт pull request’ы в dev и master. Для этого мы запускаем методы GitLab API, связанные с pull (merge) request’ами, и выполняем задачу:
release_merge_request:
tags:
- android
stage: release
script:
- 'curl --request POST
--form title="Release [`cat ./ci/app_name.txt`]"
--form id=`echo ${CI_PROJECT_ID}`
--form ref=`echo $CI_COMMIT_BRANCH`
--form source_branch=`echo $CI_COMMIT_BRANCH`
--form target_branch=master
--form remove_source_branch=true
--form assignee_id=1809
--form private_token=token
"https://gitlab.corp.mail.ru/api/v4/projects/${CI_PROJECT_ID}/merge_requests"'
- 'curl --request POST
--form title="Release [`cat ./ci/app_name.txt`]"
--form id=`echo ${CI_PROJECT_ID}`
--form ref=`echo $CI_COMMIT_BRANCH`
--form source_branch=`echo $CI_COMMIT_BRANCH`
--form target_branch=dev
--form remove_source_branch=true
--form assignee_id=1809
--form private_token=token
"https://gitlab.corp.mail.ru/api/v4/projects/${CI_PROJECT_ID}/merge_requests"'
rules:
- if: '$CI_COMMIT_BRANCH =~ /^release.*/ && $CI_PIPELINE_SOURCE == "push"'
Это промежуточный этап, чтобы:
не создавать позже вручную pull request’ы;
отправить нужную сборку в магазин приложений.
Расскажу о втором пункте.
![](https://habrastorage.org/getpro/habr/upload_files/8e2/e4f/902/8e2e4f90264585589917b6b31752794c.jpeg)
Публикация
Создаем ещё одну задачу, которая будет срабатывать на pull request’е из release-ветки в dev (или master, ведь pull request’ы там одинаковые) — именно в pull request’е, чтобы запускалась задача, собирающая и публикующая нужное приложение, в зависимости от названия, указанного в заголовке.
Теперь нужно отправить новую версию в магазин приложений. Для этого мы используем Gradle Play Publisher plugin. С помощью этого плагина загружаем сборку в магазин для внутреннего тестирования, затем этот же выпуск копируем в релиз (если есть альфа/бета-тесты, можно сначала отправлять туда, как пожелаете). Цепочка выглядит так:
Создаётся pull request.
Применительно к нему запускается задача, которая собирает app bundle и запускает Gradle-таску, которая публикует bundle.
На всякий случай мы ещё и сохраняем его в виде артефакта в Gitlab.
app_release_bundle:
variables:
GIT_CHECKOUT: "true"
tags:
- android
stage: bundle
script:
- echo $APP_KEYSTORE | base64 -d > app.jks
- export FIREBASE_TOKEN=`echo $FIREBASE_CI_TOKEN`
- ./gradlew --no-daemon bundleWorldRelease
-Pandroid.injected.signing.store.file=$(pwd)/app.jks
-Pandroid.injected.signing.store.password=$APP_PASSWORD
-Pandroid.injected.signing.key.alias=$APP_ALIAS
-Pandroid.injected.signing.key.password=$APP_KEY_PASSWORD
- mkdir release
- cp app/build/outputs/bundle/worldRelease/app-world-release.aab app/release/appRelease.aab
- ./gradlew --no-daemon publishWorldReleaseBundle
rules:
- if: '$CI_COMMIT_BRANCH =~ /^release.*/ && $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE =~ /\[(app)]/'
artifacts:
expire_in: 3 days
paths:
- app/release/appRelease.aab
![](https://habrastorage.org/getpro/habr/upload_files/7d1/8b8/1ba/7d18b81ba8f229cd34665e49d43f33b9.png)
И еще немного
Не секрет, что на CI можно автоматизировать очень многое, не только проверки и релизы. Например, переводы. У нас уже была программа, которая скачивала переводы из облака и выдавала .xml-файл (или файл для iOS, достаточно было указать платформу в аргументе к запуску). В итоге я решил переводы тоже вынести наружу. Получилась задача, которая берёт скрипт для переводов, копирует нужные файлы куда надо и отправляет результат в репозиторий. Остается только при необходимости добавить это в свою ветку. Код:
make_new_translate:
image: python:latest
cache: []
variables:
GIT_CHECKOUT: "true"
tags:
- android
stage: translate
before_script:
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
- echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p ~/.ssh
- ssh-keyscan self-hosted-gitlab.com >> ~/.ssh/known_hosts
script:
- cd
- git clone ...
- cd (склонированный проект)
- pip install -r requirements.txt
- python translate.py # + параметры запуска
- (cd ./Build/android/ && tar c .) | (cd /builds/$CI_PROJECT_PATH/app/src/main/res && tar xf -) # копируем в проект
- # аналогичные действия проделываем с модулями, если есть
- cd /builds/$CI_PROJECT_PATH
- git add ./app/src/main/res
- # + модули
- git config --global user.email "" # что-нибудь, можно и почту автора
- git config --global user.name "[CI]"
- git checkout -B translations/all-`echo $CI_JOB_ID`
- git commit -m 'Translations'
- git push git@self-hosted-gitlab.com:group/repo.git HEAD:translations/all-`echo $CI_JOB_ID`
rules:
- if: '$CI_PIPELINE_SOURCE == "trigger" && $CI_COMMIT_BRANCH == "dev" && $translate == "true"'
![](https://habrastorage.org/getpro/habr/upload_files/c0e/ec4/603/c0eec4603ce3d2f6a182f77a944aaa2e.png)
Резюме
Пусть реализация этого CI и не является чем-то сложным, однако благодаря ему мы экономим время на рутинных задачах, таких как поднятие версий, сборки релизных артефактов и публикации их в магазин приложений, и так далее (что, в общем, было достаточно предсказуемым итогом).