Как стать автором
Обновить
VK
Технологии, которые объединяют

Как мы настраивали CI в два захода

Время на прочтение6 мин
Количество просмотров3.8K

Я тружусь младшим разработчиком в отделе внутренней мобильной разработки VK. Когда я пришел в команду, у нас не было CI. При этом в одном репозитории у нас было семь приложений, и при каждом обновлении приходилось по отдельности их собирать, тратя на это кучу времени и сил. Я решил автоматизировать сборку, написав человеческий CI. И это — его история.

Первый заход

Во-первых, нам нужно было проверять коммиты и pull request’ы в dev-ветку. Во-вторых, при добавлении pull request’ов мы хотели получать apk-файлы для тестов (и заливать их в Firebase App Distribution), а при добавлении в master — получать aab-файлы для магазина приложений. В общем, задача выглядела простой, и я сел её реализовывать.

Получилось ровно так, как и хотел. Но поскольку приложений у нас много (flavor’ы), а под CI выделили Mac Mini 2014 года, то всё закончилось вполне закономерно: даже на более свежем и мощном Маке сборка выполнялась за 5-8 минут, а на Мини мы получили файлы приложений… лишь спустя 2 часа. Явно не тот результат, который мы хотели.

Второй заход

Общий конвейер CI должен был остаться таким же, только нужно было ускорить сборку необходимых flavor’ов. Также нам хотелось автоматизировать поднятие версий, что для 7 приложений было не самой тривиальной задачей. Я начал искать решение. Как никогда кстати на канале Android Academy Global вышел доклад на тему CI, который помог мне нащупать идею и начать разработку.

Процесс разработки

Для начала исправил самое востребованное: коммит в ветку задачи и pull request с фичей в dev-ветку. В целом, сборка основного flavor’а была уже реализована, но на всякий случай я добавил и сборку случайного flavor’а, чтобы не пришлось долго чинить возникающие из-за ресурсов ошибки: например, если кто-то добавил нужный для всех flavor’ов файл в папку одного из них.

script:
 - |
   COMMANDS=("./gradlew --no-daemon --stacktrace assembleCollageDebug"
   ...) # тут остальные команды
 - $(shuf -n1 -e "${COMMANDS[@]}")

Поднятие версии

В сети есть много решений на 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"'

Это промежуточный этап, чтобы:

  1. не создавать позже вручную pull request’ы;

  2. отправить нужную сборку в магазин приложений.

Расскажу о втором пункте. 

Публикация

Создаем ещё одну задачу, которая будет срабатывать на pull request’е из release-ветки в dev (или master, ведь pull request’ы там одинаковые) — именно в pull request’е, чтобы запускалась задача, собирающая и публикующая нужное приложение, в зависимости от названия, указанного в заголовке.

Теперь нужно отправить новую версию в магазин приложений. Для этого мы используем Gradle Play Publisher plugin. С помощью этого плагина загружаем сборку в магазин для внутреннего тестирования, затем этот же выпуск копируем в релиз (если есть альфа/бета-тесты, можно сначала отправлять туда, как пожелаете). Цепочка выглядит так:

  1. Создаётся pull request.

  2. Применительно к нему запускается задача, которая собирает app bundle и запускает Gradle-таску, которая публикует bundle.

  3. На всякий случай мы ещё и сохраняем его в виде артефакта в 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

И еще немного

Не секрет, что на 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"'

Резюме

Пусть реализация этого CI и не является чем-то сложным, однако благодаря ему мы экономим время на рутинных задачах, таких как поднятие версий, сборки релизных артефактов и публикации их в магазин приложений, и так далее (что, в общем, было достаточно предсказуемым итогом).

Теги:
Хабы:
Всего голосов 13: ↑10 и ↓3+16
Комментарии0

Публикации

Информация

Сайт
team.vk.company
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия