Irbisa · Шпаргалка

CI/CD мобилки

Fastlane, GitHub Actions, подпись, TestFlight, Play Internal, build flavors

Senior-разработчик · 19 вопросов

  1. 01

    Чем отличаются continuous integration, continuous delivery и continuous deployment, и почему мобильная разработка редко доходит до последнего?

    CI собирает и тестирует каждое изменение; continuous delivery держит каждую зелёную сборку готовой к релизу за одним ручным шагом; continuous deployment выкатывает её пользователям сама.

  2. 02

    Как выглядит CI/CD-пайплайн мобильного приложения от линтера до загрузки в стор?

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

    # .github/workflows/android.yml: fast lane on PRs, emulator tests only on mainname: Android CIon:  pull_request: { branches: [main] }  push: { branches: [main] }jobs:…
  3. 03

    Что такое Fastlane и что он автоматизирует?

    Fastlane представляет собой Ruby-тулкит, превращающий релизный ритуал (собрать, подписать, залить символы, отправить в тест-трек) в именованные lanes, которые одинаково запускаются на ноутбуке и в CI.

    # fastlane/Fastfileplatform :android do  lane :internal do    gradle(task: "clean bundleRelease")    upload_to_play_store(track: "internal", release_status: "draft")   # takes the AAB gradle just built  end…
  4. 04

    Релизная сборка собирается на всех ноутбуках команды и падает на чистом чекауте в CI. С чего начать разбор?

    Раннер почти всегда прав: чего-то, что нужно сборке, нет в репозитории, либо оно называется иначе, когда файловая система перестаёт игнорировать регистр.

    #!/usr/bin/env bash# Reproduce the CI environment locally: a clean clone, nothing from your working copy.set -euo pipefail TMP="$(mktemp -d)"git clone --depth 1 "file://$PWD" "$TMP/app"…
  5. 05

    Как подписывается Android-приложение и что такое Play App Signing?

    Вы подписываете AAB своим upload-ключом; с Play App Signing ключ подписи приложения хранит Google и переподписывает им APK, которые раздаёт, поэтому ключ, который проверяют устройства, никогда не лежит ни на ноутбуке, ни на CI-раннере.

    // app/build.gradle.kts: release signing, entirely from environment variablesandroid {  signingConfigs {    create("release") {      storeFile = rootProject.file(System.getenv("KEYSTORE_PATH") ?: "release.keystore")      storePassword = System.getenv("KEYSTORE_PASSWORD")…
  6. 06

    Как пайплайн авторизуется в Google Play и App Store Connect без чьего-то личного аккаунта?

    Каждый стор получает машинные учётные данные, принадлежащие команде, а не человеку: сервисный аккаунт Google Cloud, приглашённый в Play Console, и командный API-ключ App Store Connect.

  7. 07

    Вход через Google работает в APK, собранном вашим CI, и падает у всех, кто поставил приложение из Play. Что изменилось между этими двумя случаями?

    Play переподписывает вашу загрузку app signing key, поэтому отпечаток сертификата на устройстве пользователя принадлежит Google, а не вашему upload key, и каждая интеграция, завязанная на отпечаток, должна знать оба.

    #!/usr/bin/env bashset -euo pipefail # What you signed with (the upload key), NOT what users verify.keytool -list -v -alias upload -keystore upload.jks -storepass "$KEYSTORE_PASSWORD" | grep -E 'SHA1|SHA256'…
  8. 08

    Как headless-раннер CI получает и использует сертификаты для подписи iOS?

    Раннер получает сертификаты и provisioning-профили команды в режиме readonly через fastlane match и импортирует их во временный разблокированный keychain; подпись проходит, только если сертификат, профиль, entitlements и bundle id совпадают.

    # fastlane/Fastfile: iOS signing on a CI runnerplatform :ios do  lane :beta do    setup_ci if is_ci            # creates and unlocks a temporary keychain     api_key = app_store_connect_api_key(…
  9. 09

    Во что CI обходится каждый лишний flavor и как не дать ему умножить пайплайн?

    На Android это productFlavors, на iOS это build configurations на xcconfig, которые выбирают schemes, а --flavor во Flutter управляет обоими; каждый добавленный flavor умножает то, что CI должен собрать, подписать и протестировать.

    # .github/workflows/build.yml: which variants each lane buildson:  pull_request:  push:    branches: [main]jobs:…
  10. 10

    Кто должен выставлять versionCode и CFBundleVersion в пайплайне и откуда берётся следующее значение?

    Номер сборки представляет собой состояние пайплайна, а не исходник, который правят руками: CI вычисляет значение, которое только растёт.

    # fastlane/Fastfile: the store is the source of truth for the next numberplatform :ios do  lane :beta do    api_key = app_store_connect_api_key(key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"], key_content: ENV["ASC_KEY_P8"])     # Ask App Store Connect rather than trusting whatever is committed:…
  11. 11

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

    Почти любое необъяснимое падение CI приходит из того, что никто не закрепил: образ раннера, тулчейн или зависимость, разрешившаяся в версию, которой на прошлой неделе не было.

    # Pin the image, then pin the toolchain inside itjobs:  ios:    runs-on: macos-15          # not macos-latest: the default Xcode moves under you    steps:      - uses: actions/checkout@v6…
  12. 12

    Первый краш-репорт из релизной сборки оказался стеной шестнадцатеричных адресов. Что пайплайн должен был сделать на этапе сборки?

    Загрузите dSYM, mapping от R8 и нативные debug-символы именно для этой сборки из задачи, которая собрала бинарь, до выхода релиза; пересборка их не воссоздаёт.

    # fastlane/Fastfile: symbols leave in the same lane that built the binaryplatform :ios do  lane :beta do    api_key = app_store_connect_api_key(key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"], key_content: ENV["ASC_KEY_P8"])    build_app(scheme: "MyApp", export_method: "app-store-connect")   # also zips the dSYMs    upload_to_testflight(api_key: api_key)…
  13. 13

    Как сохранить доверие к UI-тестам в CI, если они флакуют?

    Сделайте так, чтобы красная сборка надёжно означала сломанное приложение: убирайте причину каждого флака, перезапускайте один тест, а не задачу, и отправляйте повторных нарушителей в карантин с владельцем.

    // Make the device a declared build input, so every run uses the same imageandroid {  testOptions {    managedDevices {      localDevices {        create("pixel8api36") {…
  14. 14

    QA подтвердил сборку 812, а релизная джоба пересобрала проект с того же тега. Почему это уже не та же самая сборка?

    Пересборка даёт новый бинарник: другой образ раннера, иначе отрезолвившаяся зависимость, новый номер сборки.

    jobs:  build:    runs-on: macos-15    steps:      - uses: actions/checkout@v6      - run: bundle exec fastlane ios build_rc build_number:$GITHUB_RUN_NUMBER…
  15. 15

    Команда релизится раз в две недели, ревью в сторе идёт днями, а плохую сборку нельзя отозвать. Как построить ветки и релизы вокруг этого?

    Trunk-based разработка с недоделанным за флагами, релизная ветка, которую режут по расписанию поезда и в которую едут только cherry-pick фиксов, и сборки для стора из тегов.

  16. 16

    PR-пайплайн идёт сорок минут, а macOS-раннеры стоят дороже ноутбуков команды. Откуда реально забрать время и деньги?

    Отменяйте перекрытые прогоны, пропускайте задачи, которых изменение не касается, собирайте debug на PR, кешируйте по lock-файлу, шардируйте тесты и переносите на Linux всё, что не требует Xcode; build scan в Gradle или сводка таймингов Xcode покажут, что окупится раньше.

    # .github/workflows/pr.yml: the levers that actually move the numbername: PRon:  pull_request:    paths: ['android/**', 'shared/**', 'gradle/**']   # a README change builds nothingconcurrency:…
  17. 17

    Пришёл pull request из форка незнакомого человека. Что мешает ему унести ваш ключ подписи?

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

    # A fork PR builds with no secrets present. Keep it that way.name: CIon: { pull_request: , push: { branches: [main] } }permissions:  contents: read           # least privilege; the default token grant is widerjobs:…
  18. 18

    Человек ушёл из компании с копией релизного keystore и iOS-сертификата для дистрибуции на ноутбуке. Что вы реально можете ротировать?

    На iOS можно отозвать и выпустить заново всё, потому что установленное из App Store переподписывает Apple.

  19. 19

    Маркетинг хочет, чтобы фича вышла на обеих платформах во вторник утром; Android одобрен, iOS всё ещё на ревью. Как выпускать?

    Разделите выпуск и запуск: доставьте оба бинарника заранее с выключенной фичей, а включите её с сервера, когда обе версии реально разойдутся.