Опыт мобильного CICD: один стандарт fastlane на много мобильных приложений


Я бы хотела поговорить о непрерывной интеграции и доставке для мобильных приложений с помощью fastlane. Как мы внедряем CI/CD на все мобильные приложения, как мы к этому шли и что получилось в итоге.


В сети уже достаточно материала по инструменту, которого так не хватало нам на старте, поэтому я намеренно не буду подробно описывать инструмент, а лишь сошлюсь на то, что было у нас тогда:



Статья состоит из двух частей:


  • Предыстория появления мобильного CI/CD в компании
  • Техническое решение раскатки CI/CD на N-приложений

Первая часть — больше ностальгия по былым временам, а вторая же — опыт, который можно применить у себя.


Так исторически сложилось


Год 2015


Мы только начали заниматься разработкой мобильных приложений, тогда еще мы ничего не знали про непрерывную интеграцию, про DevOps и другие модные штуки. Каждое обновление приложения выкатывал сам разработчик со своей машины. И если для Android это достаточно просто — собрал, подписал .apk и закинул в Google Developer Console, то для iOS тогдашний инструмент дистрибуции через Xcode оставлял нам шикарные вечера — попытки загрузить архив часто заканчивались ошибками и приходилось пробовать еще раз. Получалось, что самый прокачанный разработчик несколько раз в месяц не пишет код, а занимается релизом приложения.


Год 2016


Мы подросли, за плечами уже были мысли о том, как освободить разработчиков от целого дня для релиза, а так же появилось второе приложение, что только сильнее нас подталкивало к автоматизации. В тот же год мы впервые поставили Jenkins и написали кучку страшненьких скриптов, очень похожих на те, что показывает fastlane в своей документации.


$ xcodebuild clean archive -archivePath build/MyApp \
    -scheme MyApp

$ xcodebuild -exportArchive \
                        -exportFormat ipa \
                        -archivePath "build/MyApp.xcarchive" \
                        -exportPath "build/MyApp.ipa" \
                        -exportProvisioningProfile "ProvisioningProfileName"

$ cd /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Versions/A/Support/

$ ./altool —upload-app \
-f {abs path to your project}/build/{release scheme}.ipa \ 
-u "appleId@example.com" \
-p "PASS_APPLE_ID"

К сожалению, о том как эти скрипты работают и для чего нужна эта нескончаемая пачка ключей до сих пор знали только наши разработчики, а когда что-то в очередной раз ломалось, "шикарные вечера" для разборов логов им же и доставались.


Год 2017


В этот год мы узнали, что есть такая штука как fastlane. Было не так много информации, как сейчас — как завести, как использовать. Да и сам инструмент был тогда еще сыроват: постоянные ошибки, только разочаровывали нас и в волшебную автоматизацию, которую они обещали, верилось с трудом.


Однако основные утилиты, входящие в ядро fastlane, — gym и pilot, у нас получилось завести.


Наши скрипты немного облагородились.


$ fastlane gym  —-workspace "Example.xcworkspace" 
                --scheme "AppName" 
                —-buildlog_path "/tmp" 
                -—clean

Облагородились хотя бы потому, что не все параметры, необходимые для xcodebuild, нужно указывать — gym самостоятельно поймет где и что лежит. А для более тонкой настройки можно указать те же самые ключи, что и в xcodebuild, только нейминг ключей понятнее.


На этот раз, благодаря gym и встроенному форматеру xcpretty, логи сборки стали намного разборчивее. Это стало экономить время на починку сломанных сборки, а иногда в этом могла самостоятельно разобраться релиз-команда.


К сожалению, замеров по скорости сборки xcodebuild и gym мы не сделали, но будем верить документации — до 30% ускорения.


Единый процесс на все приложения


Год 2018 и настоящее время


К 2018 году сам процесс сборки и выкатки приложений полностью переехал на Jenkins, разработчики перестали релизить со своих машин, право на релиз имела только релиз-команда.


Нам уже захотелось докрутить запуск тестов и статический анализ, а наши скрипты росли и росли. Росли и менялись вместе с нашими приложениями. На тот момент приложений было около 10. Учитывая, что платформы у нас две — это порядка 20 «живущих» скриптов.


Каждый раз, когда мы хотели добавить новый шаг в скрипт, приходилось копипастить кусочки во все shell-скрипты. Возможно, можно было работать и поаккуратнее, но часто такие изменения заканчивались опечатками, которые уже превращались в вечера релиз-команды на починку скриптов и выяснения, кто из умников добавил эту команду и что она вообще делает. В целом, нельзя сказать что скрипты для сборки под одну платформу были хоть сколько-нибудь похожими. Хотя безусловно делали одно и тоже.


Для того чтобы завести процесс для нового приложения — нужно было потратить день, чтобы подобрать «свежую» версию из этих скриптов, отладить и сказать что «да, работает».


Летом 2018 мы еще раз посмотрели в сторону все еще развивающегося fastlane.


Задача №1: обобщить все шаги скриптов и переписать их в Fastfile


Когда мы начинали, наши скрипты выглядели портянкой из всех шагов и костылей в одном shell-скрипте в Jenkins. Мы еще не перешли на pipeline и деление по stage.


Посмотрели на то что есть и выделили 4 шага, подходящих под описание нашего CI/CD:


  • build — установка зависимостей, сборка архива,
  • test — запуск unit-тестов разработчика, подсчет покрытия,
  • sonar — запуск всех линтеров и отправка отчетов в SonarQube,
  • deploy — отправка артефакта в альфу (TestFlight).

И если не вдаваться в подробности, опустить используемые ключи у actions, получится вот такой Fastfile:


default_platform(:ios)

platform :ios do
  before_all do
    unlock
  end

  desc "Build stage"
  lane :build do
    match
    prepare_build
    gym
  end

  desc "Prepare build stage: carthage and cocoapods"
  lane :prepare_build do
    pathCartfile = ""
    Dir.chdir("..") do
      pathCartfile = File.join(Dir.pwd, "/Cartfile")
    end
    if File.exist?(pathCartfile)
      carthage
    end
    pathPodfile = ""
    Dir.chdir("..") do
      pathPodfile = File.join(Dir.pwd, "/Podfile")
    end
    if File.exist?(pathPodfile)
      cocoapods
    end
  end

  desc "Test stage"
  lane :test do
    scan
    xcov
  end

  desc "Sonar stage (after run test!)"
  lane :run_sonar do
    slather
    lizard
    swiftlint
    sonar
  end

  desc "Deploy to testflight stage"
  lane :deploy do
    pilot
  end

  desc "Unlock keychain"
  private_lane :unlock do
    pass = ENV['KEYCHAIN_PASSWORD']
    unlock_keychain(
      password: pass
    )
  end
end

На самом деле, первый Fastfile у нас получился монструозным, учитывая некоторые костыли, которые нам все еще были нужны, и количество параметров, которые мы подставляли:


lane :build do
carthage(
  command: "update",
  use_binaries: false,
  platform: "ios",
  cache_builds: true)
cocoapods(
  clean: true,
    podfile: "./Podfile",
    use_bundle_exec: false)

gym(
  workspace: "MyApp.xcworkspace",
  configuration: "Release",
  scheme: "MyApp",
  clean: true,
  output_directory: "/build",
  output_name: "my-app.ipa")
end 

lane :deploy do
 pilot(
  username: "appleId@example.com",
  app_identifier: "com.example.app",
  dev_portal_team_id: "TEAM_ID_NUMBER_DEV",
  team_id: "ITS_TEAM_ID")
end

На примере выше, только часть параметров, которые нам нужно указать: это параметры сборки — схема, конфигурация, названия Provision Profile, а также параметры дистрибуции — Apple ID аккаунта разработчика, пароль, идентификатор приложения и так далее. В первом приближении, мы положили все эти ключи в специальные файлы — Gymfile, Matchfile и Appfile.


Теперь в Jenkins можно вызывать короткие команды, которые не "замыливают" взгляд и хорошо считываются глазом:


# fastlane ios <lane_name>

$ fastlane ios build
$ fastlane ios test
$ fastlane ios run_sonar
$ fastlane ios deploy

Ура, мы молодцы


Что получили? Понятные команды для каждого шага. Причесанные скрипты, аккуратно разложенные в файлы fastlane. Обрадовавшись, мы было побежали к разработчикам с просьбой добавить все что нужно в свои репозитории.


Но вовремя осознали, что столкнемся с теми же сложностями — у нас все еще будет 20 скриптов сборки, которые так или иначе начнут жить своей жизнью, править их будет сложнее, так как скрипты переедут в репозитории, а у нас доступа туда нет. И, в общем, решить нашу боль таким образом не получится.



Задача №2: получить единый Fastfile для N-приложений


Сейчас уже кажется, что решить задачу не так уж и сложно — задайте переменные, и поехали. Да, собственно, так задачу и решили. Но в тот момент, когда мы это вкручивали, у нас не было ни экспертизы в самом fastlane, ни в Ruby, на котором написан fastlane, ни полезных примеров в сети — каждый, кто писал про fastlane тогда, ограничивался примером для одно приложения для одного разработчика.


Fastlane умеет в переменные окружения, и это мы уже попробовали, задав пароль от Keychain:


ENV['KEYCHAIN_PASSWORD']

Посмотрев на наши скрипты, мы выделили общие части:


#for build, test and deploy
APPLICATION_SCHEME_NAME=appScheme
APPLICATION_PROJECT_NAME=app.xcodeproj
APPLICATION_WORKSPACE_NAME=app.xcworkspace
APPLICATION_NAME=appName

OUTPUT_IPA_NAME=appName.ipa

#app info
APP_BUNDLE_IDENTIFIER=com.example.appName
APPLE_ID=appleID@example.com
TEAM_ID=ABCD1234
FASTLANE_ITC_TEAM_ID=123456789

Теперь для того, чтобы начать использовать эти ключи в файлах fastlane'а, нужно было придумать как их туда доставлять. У Fastlane есть для этого решение: загрузка переменных через dotenv. В документации сказано, если вам важно подгружать ключи для разных целей, наплодите в директории fastlane несколько конфигурационных файлов .env, .env.default, .env.development.


И тогда мы решили использовать эту библиотеку немного по-другому. Поместим в репозитории разработчиков не скрипты fastlane и его мета информацию, а уникальные ключи этого приложения в файле .env.appName.


Сами Fastfile, Appfile, Matchfile и Gymfile, мы спрятали в отдельный репозиторий. Туда же спрятали дополнительный файл с ключами-паролями от других сервисов — .env.
Пример, можно посмотреть здесь.



На CI вызов не сильно поменялся, добавился ключ конфигурации конкретного приложения:


# fastlane ios <lane_name> --env appName

$ fastlane ios build --env appName
$ fastlane ios test --env appName
$ fastlane ios run_sonar --env appName
$ fastlane ios deploy --env appName

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


git clone git@repository.com/FastlaneCICD.git fastlane_temp

cp ./fastlane_temp/fastlane/* ./fastlane/
cp ./fastlane_temp/fastlane/.env fastlane/.env

Пока оставили это решение, хотя у Fastlane есть решение для загрузки Fastfile через action import_from_git, но он работает только для Fastfile, для остальных же файлов — нет. Если хочется «прям совсем красиво», можно написать свой action.


Аналогичный набор сделали для Android приложений и ReactNative, файлы лежат в одном репозитории, но в разных ветках iOS, android и react_native.


Когда релиз команда хочет добавить какой-нибудь новый шаг, изменения в скрипте фиксируются через MR в git, больше не надо искать виновников поломанных скриптов, да и в целом — сломать теперь, это надо постараться.


Теперь точно все


Раньше мы тратили время на поддержку всех скриптов, их обновление и починку всех последствий обновлений. Было очень обидно, когда причины ошибок и простоев релизов были простыми опечатками, за которыми так сложно уследить в мешанине shell-скрипта. Теперь же такие ошибки сведены к минимуму. Изменения накатываются сразу на все приложения. А новое приложение завести в процесс стоит 15 минут — настроить шаблонный pipeline на CI и добавить ключи в репозиторий разработчика.


Кажется, остался неосвещенным пункт с Fastfile для Android и подпись приложений, если статья будет интересна, напишу продолжение. Буду рада вашим вопросам или предложениям "как бы вы решили эту задачу" в комментариях или в Telegram bashkirova.

  • +10
  • 2,4k
  • 4
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 4

    0
    Я так понял у вас в компании отсутсвует ReleaseEngineer/DevOps/Сисадмин и сборкой занимаються тестировщики? Из статьи совсем не понятно, где преимущество перед сборкой отдельными job'ами с хранением результата сборок в Nexus/Артифактори/etc? В каком шаге pipeline у вас тестирование на соответствие гайдланам Google Play и AppStore? Автоматизации тестирования мобильных приложений к примеру с популярным Appium тоже нет? Сразу деплоите в магазин и без подписей? Можете рассказать подробнее, а то складывается ощущение, что сделано не для удобства, а как дань моде?
      0
      у вас в компании отсутсвует ReleaseEngineer/DevOps/Сисадмин и сборкой занимаються тестировщики

      Да. Сборкой мобильных приложений занимаются тестировщики. Это не сложно, тем более когда мобильщики помогают.
      Из статьи совсем не понятно, где преимущество перед сборкой отдельными job'ами с хранением результата сборок

      Не было цели показывать преимущество. Что именно включать в сборку, каким порядком запускать и каким образом — решать вам.

      В каком шаге pipeline у вас тестирование на соответствие гайдланам Google Play и AppStore?

      Что конкретно имеете в виду? Раскройте, я не поняла.
      Ручное тестирование проводим после успешной загрузки альфа-сборки.
      Автоматизации тестирования мобильных приложений к примеру с популярным Appium тоже нет?

      Есть.
      Сразу деплоите в магазин и без подписей?

      Сначала подписываем, потом отправляем в альфу google play market и TestFlight. А у вас как?
      Можете рассказать подробнее, а то складывается ощущение, что сделано не для удобства, а как дань моде?

      Могу рассказать поподробнее.

      В нашей компании процессом автоматизации разработки исторически начали заниматься тестировщики. Тестировщик тогда в компании был единственным, в свободное время погружался в ci/cd и понемногу внедрял каждый из шагов.
      Так что поначалу наверное это была дань моде и желание достичь экономии в тестировании и разработке. В цифрах никто экономию не считал, но задачи свои процесс решает — разработчики не выливают приложения вручную, доступа до ключей подписи у них нет, запуск автотестов происходит до загрузки архива в сторы, тестировщики всегда в курсе, когда выходит новая сборка на тест (у нас нет необходимости вводить расписания релизов).

      0
      А можно узнать название вашей компании? Вы пишите "… настроить шаблонный pipeline на CI и добавить ключи в репозиторий разработчика..." Из чего можно сделать вывод, что и безопасников у вас нет? Потому как хранить ключи для подписи в репозитории это совсем не хорошо по моему скромному мнению.
        0
        безопасников у вас нет

        Отдельных нет, но мы к этому идём.

        хранить ключи для подписи в репозитории это совсем не хорошо по моему скромному мнению.

        Полностью с вами соглашусь — совсем не хорошо, и так у нас было на Android проектах до того, как переехали на этот процесс. Но даже это не так страшно, так как у нас развернут локальный Gitlab.
        Выше в статье приведён пример Fastfile, где, к примеру, вызывается match — экшен, который подписью и занимается. Ключи живут в отдельном закрытом репозитории, куда доступ только у технической учетки и ребят, которые настраивали процесс. Как оно работает описано в codesigning guide . Аналогично сделали для ключей Android.

        :) а как у вас?

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое