CI/CD (1)


Настройка CI в GitLab для Android проекта

CI — continuous integration — можно описать как практику автоматизированного внедрения результатов работы программиста по целевому месту назначения и использования (деплой). CI имеет множество преимуществ, и особенностей, среди которых отметим следующие:

  • Единая кодовая база, в которую сливаются ветки разработчиков — достигается с помощью GitFlow
  • Автоматизация сборок — сборка запускается автоматически после вливания в нее нового кода с помощью pipeline в GitLab
  • Автоматизация тестирования — как один из шагов в pipeline-e
  • Удобный доступ к сборкам
  • Автоматизированный деплой — дополнительный скрипт может публиковать сборки в магазинах приложений или по пути дистрибуции.

CI как и любая автоматизация позволяет устранить человеческий фактор и автоматизировать повторяющиеся действия.
Поэтому CI желательно настраивать на любом серьезном проекте. А в случае, когда разработчиков несколько и релизы необходимо собирать часто, CI должен быть настроен в обязательном порядке.
Обычно настройка CI лежит в зоне ответственности DevOps инженера. Однако, хотя в большинстве компаний такой человек есть, он может быть недоступен или не уметь настраивать CI именно для Android проекта. Поэтому Android-разработчику необходимо уметь делать это самостоятельно. Как это делается, расскажу и покажу в этой статьте.

Итак, приступим.
С точки зрения GitLab, CI состоит из

  • Jobs, которые описывают что сделать. например, собрать код или протестировать
  • Stages, которые определяют когда стартовать Job-ы, например, стадия тестирования

Обычно stages бывают
— buiild job под названием compile
— test — запуск тестов
— staging — деплой на stage
— production -деплой на prod

Всю эту информацию о jobs и stages нам нужно передать GitLab-у. Сделаем мы это с помощью файла .gitlab-ci, который положим в корне проекта. В нем будут описаны все шаги, а также выполнятся необходимые скрипты.

Во-первых укажем образ, который будет выполнять Job

image: openjdk:8-jdk

Затем в разделе под названием before_script напишем скрипты, которые сервер должен выполнить, чтобы настроить сборочное окружение.

before_script:
 - apt-get --quiet update --yes
 - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
 - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip
 - unzip -d android-sdk-linux android-sdk.zip
 - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
 - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null
 - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
 - export ANDROID_HOME=$PWD/android-sdk-linux
 - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/

Никакой магии здесь нет, и, если вы знакомы unix-системами, и их командами, то вам все должно быть понятно
Не забудьте указать номера версий SDK, которые сервер должен установить. Для этого перед блоком before_script добавьте

variables:
 ANDROID_COMPILE_SDK: [номер_версии]
 ANDROID_BUILD_TOOLS: [номер_версии]
 ANDROID_SDK_TOOLS: [номер_версии]

Далее, указываем типы этапов сборки в блоке stages

stages:
 - build
 - test
 - deploy

Теперь мы готовы к выполнению собственно сборок.
Я рекомендую использовать два типа сборок — релизную и дебажную. Вы можете сделать столько вариантов, сколько вам угодня, исходя из ваших задач
Итак, для начала пропишем дебажную сборку

buildDebug:
 stage: build
 tags: [android]
 only:
 - develop
 - master
 - /^release.*$/
 cache:
 paths:
 - .gradle/caches
 variables:
 VAR_NAME: BUILD_NUMBER // автоинкрементируемый номер сборки
 TOKEN: ${CI_PIPELINE_IID_TOKEN}

Тег only позволяет триггерить запуск сборки в случае пуша в конкретнкю ветку или по тегу, как мы увидим далее
Переменная BUILD_NUMBER будет считываться с помощью токена CI_PIPELINE_IID_TOKEN из настроек Variables в разделе CI в GitLab. Она позволяет нам добавлять автоинкрементируемый номер сборки в название версии. Подробнее этот подход описан здесь: https://gitlab.com/jmarcos.cano/CI_PIPELINE_IID/snippets/1675434. А ниже я покажу как настроить build.gradle для генерирования названия версии и номера сборки. А пока продолжим с .gitlab-ci
Следующий блок будет script

script:
 # готовим переменную с номером сборки
 - GITLAB_URL=$(echo ${CI_PROJECT_URL} |awk -F "/" '{print $1 "//" $2$3}')
 - "VAR=$(curl -s -f --header \"PRIVATE-TOKEN: ${TOKEN}\" \"${GITLAB_URL}/api/v4/projects/${CI_PROJECT_ID}/variables/${VAR_NAME}\" | jq -r '.value' ) "
 - let VAR=VAR+1
 - "curl -s -f --request PUT --header \"PRIVATE-TOKEN: ${TOKEN}\" \"${GITLAB_URL}/api/v4/projects/${CI_PROJECT_ID}/variables/${VAR_NAME}\" --form \"value=${VAR}\" "
 # записываем переменную во временный файл, из которого будет считывать этот номер в build.gradle
 - echo $VAR > build.version
 - chmod +wx build.version
 - sed -i 's/android.enableBuildCache=false/android.enableBuildCache=true/g' gradle.properties
 # и собственно собираем сборку
 - ./gradlew clean app:assembleDebug

Теперь остается только указать как и где хранить артефакты, то есть apk:

artifacts:
 when: always
 expire_in: 4 weeks
 paths:
 - app/build/outputs/apk/

Вот и все, а для релизной сборки укажем такой же блок, но stage и него будет deploy, а сборка будет собираться еще и с app:assembleRelease, и собираться такая сборка будет только при создании тэга в GitLab.

Дополнительно нам нужно настроить автоматическое инкрементирование номеров сборок
Для этого в Ваш build.gradle добавьте функции, которые будут генерировать versionCode и versionName на основе минимальной версии target api

private Integer generateVersionCode() {
 def minSDK = rootProject.minSdkVersion * 1000000
 def major = rootProject.versionMajor * 10000
 def minor = rootProject.versionMinor * 100
 def patch = rootProject.versionPatch
 def versionCode = minSDK + major + minor + patch
 project.logger.debug('versonCode ', versionCode)
 return versionCode
 }

private String generateVersionName() {
 String versionName = "${rootProject.versionMajor}.${rootProject.versionMinor}.${rootProject.versionPatch}"
 versionName += '.' + getBuildNumberFromFile()
 return versionName
 }

Естественно, в корневом build.gradle у вас должны быть прописаны номера версии в виде

versionMajor = 1
 versionMinor = 0
 versionPatch = 0
 minSdkVersion = 23

Также добавьте функции для получения номера сборки из CI GitLab, а в случае локальной сборки — из гита

def getBuildNumberFromGit() {

try {
 def stdout = new ByteArrayOutputStream()
 exec {
 commandLine 'git', 'rev-list', '--all', '--count'
 standardOutput = stdout
 }
 return stdout.toString().trim()
 }
 catch (ignored) {
 return '?'
 }
 }

// Этот метод получает номер сборки из специального файла, который мы генерировали выше в скрипте .gitlab-ci
def getBuildNumberFromFile() {

File versionFile = file('../build.version')
 if (versionFile.exists()) {
 return versionFile.readLines().get(0).trim()
 } else {
 return getBuildNumberFromGit()
 }
 }

Остается вызывать этот метод для названия версии, а также для номера сборки

defaultConfig {
 applicationId "ru.andreyaleev"
 minSdkVersion rootProject.minSdkVersion
 targetSdkVersion rootProject.targetSdkVersion
 versionName generateVersionName()
 }

buildTypes {

applicationVariants.all { variant ->
 variant.outputs.each { output ->
 output.versionCodeOverride = generateVersionCode()
 output.outputFileName = "$setup.applicationId-${variant.versionName}.apk"
 }
 }
 ...

Готово с кодом, теперь нужно добавить переменную в GitLab, идея взята отсюда

Заходим в Settings->CI/CD->Variables, далее добавляем две переменные BUILD_NUMBER и CI_PIPELIBE_ID_TOKEB


BUILD_NUMBER присваиваем 1, а в CI_PIPELIBE_ID_TOKEB положим значение токена, который нужно сгенерировать через раздел Access Tokens профиля

Остается запушить изменения в GitLab, и он автоматически будет стартовать Pipeline
Для создания релизной сборки, которую мы настроили выше, нужно создать Тэг через Repository->Tags-> New Tag
Выбираете ветку, из которой хотите создать сборку и запускаете. Все готово!