Опыт внедрения fastlane для автоматизации всех этапов мобильного CI/CD


Я бы хотела поговорить о непрерывной интеграции и доставке для мобильных приложений с помощью 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.

Оставить комментарий