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
Выбираете ветку, из которой хотите создать сборку и запускаете. Все готово!