Как настроить CI в мобильных приложениях

от автора

Приветствую! Меня зовут Алексей Денискин, я тимлид мобильной команды СберМаркета. В этой статье я на примере покажу, как организовать 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. Пишите 🙂


ссылка на оригинал статьи https://habr.com/ru/company/sbermarket/blog/688598/