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