Я бы хотела поговорить о непрерывной интеграции и доставке для мобильных приложений с помощью fastlane. Как мы внедряем CI/CD на все мобильные приложения, как мы к этому шли и что получилось в итоге.
В сети уже достаточно материала по инструменту, которого так не хватало нам на старте, поэтому я намеренно не буду подробно описывать инструмент, а лишь сошлюсь на то, что было у нас тогда:
- Официальная документация fastlane
- Примеры других компаний
- Автоматизируем сборку iOS приложений с помощью Fastlane
Статья состоит из двух частей:
- Предыстория появления мобильного 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.