Приветствую! Меня зовут Алексей Денискин, я тимлид мобильной команды СберМаркета. В этой статье я на примере покажу, как организовать CI для мобильных приложений на Android и iOS. Я буду использовать GitLab CI, но описанный подход применим к большинству стандартных стеков.

Зачем нужен CI. Опыт СберМаркета

До интеграции CI тяжело было следить за здоровьем проекта и поддерживать ручное тестирование без валидации изменений. А для каждого коммита приходилось запускать 15+ команд для проверки и сборки. Если у вашего приложения стабильный цикл релиза, это очень неудобно.

После интеграции CI снизилось количество «ручного труда», повысились надёжность проекта и качество кода, а также уменьшился Time-to-Market.

Определяем цели CI

Для примера настройки CI мы взяли стандартные цели:

  • валидация изменений (Lint, Test),

  • сборка для тестового стенда,

  • релизные сборки.

Создаём окружение

Сразу описать задачи для пайплайнов не получится. Нужно убедиться, что у нас описаны переменные и воркфлоу.

Описание переменных. Внесём в окружение проекта необходимые переменные:

  • ANDROID_CI_IMAGE — docker-образ с предустановленным Android SDK,

  • MY_KEYCHAIN — сертификат для подписи приложения,

  • MY_TOKEN_1, MY_TOKEN_2 — любые другие необходимые токены. Например, для доступа к Firebase.

Описание воркфлоу. Нужно определить типы пайплайнов. Их 3 по нашим целям:

  • release,

  • staging,

  • merge_request.

Напишем условия запуска для каждого из типов пайплайнов.

stages:
  - test # Этап проверки кода
  - build # Этап сборки кода
  - deploy # Этап деплоя сборки

workflow:
  rules:
    - if: $CI_COMMIT_TAG # Если был git tag
      variables:
        PIPELINE_TYPE: "release"
    - if: $CI_COMMIT_BRANCH == "master" # Если ветка -- master
      variables:
        PIPELINE_TYPE: "staging"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" # Если это Merge Request
      variables:
        PIPELINE_TYPE: "merge_request"
    - when: never

Теперь можно переходить к описанию задач.

Описываем ключевые шаги пайплайна

Для примера разберемся, как описать три ключевые шага пайплайна:

  • линтеры и тесты,

  • сборка,

  • деплой.

Обратите внимание, что для валидации изменений последние два этапа не запускаются.

Линтеры и тесты

Для Android запускаем JUnit.

junit:
  image:
    name: $ANDROID_CI_IMAGE # Используем контейнер с Android SDK
  stage: test # Этап проверки кода
  before_script:
    - cd ./android # Для React Native проекта
  script:
    - ./gradlew testDebugUnitTest # Запускаем JUnit
  rules:
    - if: $PIPELINE_TYPE # Для любого типа пайплайнов
      changes:
        - "**/*.{kt,java}" # Если были изменения в .kt или в .java
      when: always
    - when: never

Также запускаем Ktlint.

ktlint:
  image:
    name: $ANDROID_CI_IMAGE # Используем контейнер с Android SDK
  stage: test # Этап проверки кода
  before_script:
    - cd ./android # Для React Native проекта
  script:
    - ktlint --verbose --color "android/**/*.kt" # Запускаем KtLint
  rules:
    - if: $PIPELINE_TYPE # Для любого типа пайплайнов
      changes:
        - "**/*.{kt,java}" # Если были изменения в .kt или в .java
      when: always
    - when: never

Для iOS запускаем xcodebuild test.

xcode-test:
  stage: test # Этап проверки кода
  before_script:
    - cd ./ios # Для React Native проекта
  script:
    # Запускаем тесты XCode
    - xcodebuild \
      -project demoMobileCI.xcodeproj \
      -scheme demoMobileCI \
      -destination 'platform=iOS Simulator,name=iPhone 8,OS=15.2'\
	test
  tags:
    - osx-runner # Запускаем на машине с MacOS и XCode
  rules:
    - if: $PIPELINE_TYPE # Для любого типа пайплайнов
      changes:
        - "**/*.{swift}" # Если были изменения в .swift
      when: always
    - when: never

Если используете React Native, стоит также добавить джобы на JavaScript- и TypeScript-тесты.

Пример скриптов в package.json:

{
	"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
	"tsc": "tsc --project tsconfig.json --noEmit",
	"test": "jest --silent",
}
# Для React Native проекта #
eslint:
  image:
    name: $NODE_CI_IMAGE # Используем контейнер с Node.JS
  stage: test # Этап проверки кода
  before_script:
    - yarn install # Устанавливаем node_modules
  script:
    - yarn lint # Запускаем Eslint
  rules:
    - if: $PIPELINE_TYPE # Для любого типа пайплайнов
      changes:
        - "**/*.{js,jsx,ts,tsx}" # Если были изменения в JS или TS
      when: always
    - when: never

jest:
  image:
    name: $NODE_CI_IMAGE # Используем контейнер с Node.JS
  stage: test # Этап проверки кода
  before_script:
    - yarn install # Устанавливаем node_modules
  script:
    - yarn test # Запускаем NPM тесты
  rules:
    - if: $PIPELINE_TYPE # Для любого типа пайплайнов
      changes:
        - "**/*.{js,jsx,ts,tsx}" # Если были изменения в JS или TS
      when: always
    - when: never

typescript:
  image:
    name: $NODE_CI_IMAGE # Используем контейнер с Node.JS
  stage: test # Этап проверки кода
  before_script:
    - yarn install # Устанавливаем node_modules
  script:
    - yarn tsc # Запускаем typescript compiler
  rules:
    - if: $PIPELINE_TYPE # Для любого типа пайплайнов
      changes:
        - "**/*.{js,jsx,ts,tsx}" # Если были изменения в JS или TS
      when: always
    - when: never

Сборка

Если сборка запускается для master-ветки или релиза, приложение нужно подписать сертификатом.

Для Android запускаем ./gradlew assembleRelease.

Важно: нужен docker-образ, в котором установлен Android SDK.

gradle-build:
  image:
    name: $ANDROID_CI_IMAGE # Используем контейнер с Android SDK
  stage: build # Этап сборки кода
  needs: ["junit", "ktlint"]
  before_script:
    - cd ./android # Для React Native проекта
  script:
    - ../gradlew assembleRelease # Запускаем сборку
    - cp android/app/build/outputs/bundle/release/app-release.aab .
  artifacts:
    expire_in: 1 months
    paths:
      - app-release.aab # Путь к AAB / APK
  rules:
    - if: $PIPELINE_TYPE != "merge_request" # Запускаем только для master-ветки и релизов
      when: always
    - when: never

Для iOS запускаем xcodebuild build.

Важно: требуется, чтобы gitlab-runner был запущен на MacOS.

xcode-build:
  stage: build # Этап сборки кода
  needs: ["xcode-test"]
  before_script:
    - cd ./ios # Для React Native проекта
  script:
    # Запускаем сборку XCode
    - xcodebuild \
      -project demoMobileCI.xcodeproj \
      -scheme demoMobileCI \
      -destination 'platform=iOS Simulator,name=iPhone 8,OS=15.2' \
      build
    - cp ios/builds/results/demoMobileCI.ipa .
  artifacts:
    expire_in: 1 months
    paths:
      - demoMobileCI.ipa # Путь к IPA
  tags:
    - osx-runner # Запускаем на машине с MacOS и XCode
  rules:
    - if: $PIPELINE_TYPE != "merge_request" # Запускаем только для master-ветки и релизов
      when: always
    - when: never

Деплой

В нашем примере деплой будет происходить в Firebase. Для отгрузки в AppStore и GooglePlay можно также использовать Fastlane.

Для Android в needs указываем gradle-build.

deploy-android:
  image:
    name: $FIREBASE_IMAGE # Контейнер с установленным <https://github.com/firebase/firebase-tools>
  stage: deploy # Этап деплоя сборки
  needs: ["gradle-build"]
  script:
    - firebase appdistribution:distribute app-release.aab --app $MY_APP_ID --groups "QAMobile" --token "$MY_TOKEN_1"
  rules:
    - if: $PIPELINE_TYPE == "release" # Запускаем только для релизов
      when: always
    - when: never

Для iOS в needs указываем xcode-build.

deploy-ios:
  image:
    name: $FIREBASE_IMAGE # Контейнер с установленным <https://github.com/firebase/firebase-tools>
  stage: deploy # Этап деплоя сборки
  needs: ["xcode-build"]
  script:
    - firebase appdistribution:distribute demoMobileCI.ipa --app $MY_APP_ID --groups "QAMobile" --token "$MY_TOKEN_1"
  rules:
    - if: $PIPELINE_TYPE == "release" # Запускаем только для релизов
      when: always
    - when: never

В итоге

Что получилось. Мы создали пайплайн с необходимой структурой, который позволяет валидировать изменения и собирать приложение для тестов и для релиза.

Визуализация пайплайна в GitLab CI
Визуализация пайплайна в GitLab CI

Что можно улучшить. Это простой вариант реализации CI, который далёк от идеала. Вот что можно добавить, чтобы улучшить CI:

  • кэширование,

  • GitLab Releases, Badges,

  • инкапсуляция CI в отдельном репозитории,

  • автоматическое ветвление,

  • менеджмент Merge Requests при помощи CI, Codeowners,

  • автотестирование,

  • автоматизация и интеграция с Jira, Slack, Confluence.

Кстати, прямо сейчас ищу в свою команду разработчика react native. Пишите :)

Комментарии (0)