Рост команды требует большего вовлечения в процессы и договорённости, которые, в свою очередь, требуют автоматизации и инспекции. Можно взять bash-скрипты и закрыть ими эту потребность, но насколько это будет удобно? Тут нужен инструмент, который упростит разработку и будет поддерживать команду в будущем. Сегодня расскажу про один из таких инструментов — Fastlane — и его возможности.
Статья будет полезна для ознакомления с Fastlane, тем, кто ищет решения для разработки автоматизации или рассматривает альтернативные решения по автоматизации сборок и процессов внутри компании. Для наглядности все примеры запускаются локально, это же решение можно перенести на CI/CD (Gitlab, Jenkins, Github Actions и тп).
Не хочу читать, дай мне код: https://github.com/maluginp/fastlanetestproject
Подготовка проекта под Fastlane
Всегда думал, что Fastlane больше подходит для iOS-разработчиков. Ох, как же сильно я заблуждался. На самом деле это огромный набор готовых инструментов для автоматизации всевозможных “хотелок”: прогон unit-тестов, линтеров, публикаций и т.п. У Fastlane большое комьюнити, которое вносит перманентный вклад в развитие продукта. Советую посмотреть список доступных методов (action-ов) под разные платформы тут. Если объединиться с iOS командой, то можно еще и код между собой шарить.
Теперь погнали настраивать среду для Fastlane. Для удобства подготовил тестовый проект под статью.
К счастью, по настройке Fastlane комьюнити подготовило полноценную инструкцию.
Fastlane настроен и добавлен в проект. После инициализации Fastlane сгенерировал команды (lane-ы) под Android:
default_platform(:android) platform :android do desc "Runs all the tests" lane :test do gradle(task: "test") end desc "Submit a new Beta Build to Crashlytics Beta" lane :beta do gradle(task: "clean assembleRelease") crashlytics # sh "your_script.sh" # You can also use other beta testing services here end desc "Deploy a new version to the Google Play" lane :deploy do gradle(task: "clean assembleRelease") upload_to_play_store end end
Изначально получаем три простые команды, которые закрывают базовые потребности для автоматизации публикации Android приложения: прогон юнит-тестов, отрпавка беты в Crashlytics и Play store.
Как организовать код
Поделюсь опытом организации кода, который мы выработали внутри команды. Все части мы делим на модули для переиспользования кода, не превращая Fastfile в свалку. Мне так удобно, поэтому все последующие примеры выполнены в том же стиле.
Не являюсь спецом в Ruby, поэтому пишу, как умею))
Каждый модуль — это одна фитча или несколько микро-фитч (если можно логически объединить), например, линтеры:
require 'fastlane' # Linter methods module Linter def self.detekt(throw_if_fails: true) # ./gradlew detekt end def self.lint(throw_if_fails: true) # ./gradlew lint end end
Каждый модуль можно переиспользовать в любых частях кода.
Кейсы
Прогон линтеров, чтобы не упало качество кода
Для этого нам нужно:
-
Прогнать detekt
-
Прогнать lint
-
Если что-нибудь не прошло, возвращаем ошибку
Для оптимизации предлагаю прогонять detekt и lint независимо, в конце проверить результат и если есть ошибки, то возвращаем ошибку lane.
В модуле для линтеров учитываем, что запуск gradle-а может завершиться ошибкой, и даем управлять вызывающим методом:
require 'fastlane' # Linter methods module Linter def self.detekt(throw_if_fails: true) Fastlane::Actions::GradleAction.run( task: 'detekt', project_dir: '../', # Без print_* аргументов, результат # не будет выводится в консоли print_command: true, print_command_output: true ) true # При ошибке выполнение команды Fastlane кидает # исключение, которое можно отловить rescue FastlaneCore::Interface::FastlaneShellError # Кидает исключение на верх (вызывающим объектов) # если не собираемся обрабаывать по своему raise if throw_if_fails false end def self.lint(throw_if_fails: true) Fastlane::Actions::GradleAction.run( task: 'lint', project_dir: '../', print_command: true, print_command_output: true ) true rescue FastlaneCore::Interface::FastlaneShellError raise if throw_if_fails false end end
Добавим новый lane в Fastfile для вызова команды:
desc 'Check linter rules' lane :lint do lint_res = Linter.lint(throw_if_fails: false) # Пишем в логах ошибку Fastlane::UI.error('Lint failed') unless lint_res detekt_res = Linter.detekt(throw_if_fails: false) # Пишем в логах ошибку Fastlane::UI.error('Detekt failed') unless detekt_res unless detekt_res && lint_res Fastlane::UI.user_error!("Lint failed. Result detekt = #{detekt_res}, lint = #{lint_res}") end end
Для запуска в терминале набираем fastlane lint, в тестовом проекте получаю, что lane закончен неуспешно и логи:
Lint failed. Result detekt = false, lint = true
Все работает, перейдем к следующему кейсу.
Safe-merge, прогон unit-тестов
Бывает такое, что в текущей ветке все тесты и линтеры проходят, но при мерже в главную ветку линтер или тесты ломаются (для простоты считаем, что все merge request-ы идут в главную ветку). Можно просить разработчиков подливать себе свежую главную ветку, но мы пойдем по пути автоматизации. Для этого реализуем safe merge (в ветку без merge-коммита подливаем последние коммиты из главной ветки), прогоняем тесты и линтеры.
Напишем модуль для safe-merge:
require 'fastlane' # Safe merge for actual branch module SafeMerge def self.main_branch? # Название главной ветки будет брать из env-параметров ENV['MAIN_BRANCH'] == fetch_local_branch end def self.merge_main_no_commit main_branch = ENV['MAIN_BRANCH'] local_git_branch = fetch_local_branch command = [ 'git', 'merge', main_branch, '--no-commit', '--no-ff' ] Fastlane::Actions.sh(command.join(' ')) # пишем в логах успешность мержа Fastlane::UI.success("Successfully merged #{main_branch} (main branch) to #{local_git_branch}") end def self.fetch_local_branch local_git_branch = Fastlane::Actions.git_branch_name_using_HEAD local_git_branch = Fastlane::Actions.git_branch unless local_git_branch && local_git_branch != 'HEAD' local_git_branch end end
и модуль для unit-тестов по аналогии с detekt и lint:
require 'fastlane' # Unit test methods module UnitTests def self.run(throw_if_fails: true) Fastlane::Actions::GradleAction.run( task: 'test', project_dir: '../', print_command: true, print_command_output: true ) true rescue FastlaneCore::Interface::FastlaneShellError raise if throw_if_fails false end end
Осталось добавить lane для unit-тестов
desc 'Run unit tests' lane :unitTest do # Выводим отладочное сообщение, что не запускаем safe-merge # так как текущая ветка и есть главная UI.message('Skip safe merge, the branch is main') if SafeMerge.main_branch? SafeMerge.merge_main_no_commit unless SafeMerge.main_branch? UnitTests.run end
Для запуска в терминале набираем fastlane unitTest , если unit-тесты проходят успешно, то lane завершится успехом.
Пока все просто, перейдем к более сложным кейсам.
Сборка релизных версий, отправка нотификаций через Slack
Чтобы QA не собирать сборку вручную (уверен, что они умеют это делать) и не отвлекать разработчиков, запросили автоматизацию сборки и отправки в Slack.
Новый модуль для релизных сборок для QA:
require 'fastlane' require_relative 'building_slack' # QA build methods module QABuild def self.run(debug: false) # Это простой класс хелпер для отправки Slack-сообщений thread = BuildingSlackThread.start( # Параметры будем брать из env-параметров ENV['SLACK_API_TOKEN'], ENV['SLACK_QA_CHANNEL_BUILDS'], 'Building QA build...', debug ) begin # Собираем релизный APK-файл Fastlane::Actions::GradleAction.run( task: 'assemble', flavor: '', build_type: 'Release', project_dir: "../", print_command: true, print_command_output: true ) # Достаем путь к собранному APK completed_apk_path = Fastlane::Actions.lane_context[ Fastlane::Actions::SharedValues::GRADLE_APK_OUTPUT_PATH ] # Прикрепляем APK-файл к Slack-треду thread.attach_file('APK file', completed_apk_path, 'app.apk') # Собираем релизный AAB-файл Fastlane::Actions::GradleAction.run( task: 'bundle', flavor: '', build_type: 'Release', project_dir: "../", print_command: true, print_command_output: true ) # Достаем путь к собранному AAB completed_aab_path = Fastlane::Actions.lane_context[ Fastlane::Actions::SharedValues::GRADLE_AAB_OUTPUT_PATH ] # Прикрепляем AAB-файл к Slack-треду thread.attach_file('Bundle file', completed_aab_path, 'app.aab') # Обновляем главный тред - сборка выполнена успешно, # тут можно накидать смайлов thread.success('Building QA build succeed') rescue # Обновляем главный тред - сборка провалилась, # тут можно накидать смайлов и прикрепить детали thread.failure('Building QA build failed') raise # кидает исключение на верх к вызывающему методу end end end
В итоге весь процесс нагляднее, так как видим, что процесс сборки идет, и по сути нам уже не нужно открывать CI и смотреть, в каком статусе сейчас находится выполнение.
Напишем lane для запуска команды для сборки версий для QA:
desc 'Submit release builds to QA via Slack' lane :qa do QABuild.run(debug: true) end
Для запуска в терминале набираем fastlane qa
Публикация приложений в Play Store с раскаткой 10%
Модуль очень похож на модуль по сборке релизных версий для QA, основное отличие в вызове action-а для публикации в Play Store — Fastlane::Actions::UploadToPlayStoreAction
require 'fastlane' module Deploy def self.run(debug: false) thread = BuildingSlackThread.start( ENV['SLACK_API_TOKEN'], ENV['SLACK_RELEASE_CHANNEL_BUILDS'], 'Releasing build...', debug ) begin Fastlane::Actions::GradleAction.run( task: 'assemble', flavor: '', build_type: 'Release', project_dir: "../", print_command: true, print_command_output: true ) completed_apk_path = Fastlane::Actions.lane_context[ Fastlane::Actions::SharedValues::GRADLE_APK_OUTPUT_PATH ] thread.attach_file('APK file', completed_apk_path, 'app.apk') Fastlane::Actions::GradleAction.run( task: 'bundle', flavor: '', build_type: 'Release', project_dir: "../", print_command: true, print_command_output: true ) completed_aab_path = Fastlane::Actions.lane_context[ Fastlane::Actions::SharedValues::GRADLE_AAB_OUTPUT_PATH ] thread.attach_file('Bundle file', completed_aab_path, 'app.aab') unless debug Fastlane::Actions::UploadToPlayStoreAction.run( aab: completed_aab_path, track: 'production', rollout: 0.1 # 10% ) end thread.success('Releasing is completed and rollout on 10%') rescue thread.failure('Releasing is failed') raise # re-raise end end end
Напишем lane для деплоя
desc 'Deploy a new version to the Google Play' lane :deploy do Deploy.run(debug: true) end
Для запуска в терминале набираем fastlane deploy
Отправка процесса сборки через Slack
Напишем небольшой класс для работы с тредом в Slack, который позволит обновлять сообщение треда и прикреплять к нему файлы. При необходимости можно расширить функционал.
Не обязательно использовать Slack, можно выбрать любой другой мессенджер и доработать класс-хелпер.
Для упрощения работы с Slack API я подключил плагин к Fastlane: fastlane-plugin-slack_bot
Для подключения плагина нужно добавить следующие строчки в Gemfile (находится в корне проекта)
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path)
В папке fastlane создать новый файл Pluginfile с содержимым
gem 'fastlane-plugin-slack_bot'
У вас должна получиться следующая структура:
{root} |-- Gemfile |-- fastlane |-- Pluginfile
Сам код класса выглядит следующим образом
BuildingSlackThread.rb
require 'fastlane' # Manage slack thread for building class BuildingSlackThread # приватные свойства attr_accessor :api_token, :thread_ts, :thread_channel, :debug def self.start(api_token, channel, message, debug = false) Fastlane::UI.message("Sending start building message to Slack channel #{channel}") # При дебаге не шлем сообщения, но логика проходит if debug return BuildingSlackThread.new(api_token, channel, message, debug: debug) end # Создаем новый тред в канале с которым и будем взаимодействовать thread_result = Fastlane::Actions::PostToSlackAction.run( api_token: @api_token, message: message, success: true, channel: @channel, payload: [], attachment_properties: {}, default_payloads: %i[lane git_branch git_author last_git_commit], ) thread_ts = thread_result[:json]["ts"] thread_channel = thread_result[:json]["channel"] Fastlane::UI.error("Failed sending message to Slack to #{channel} channel") unless thread_ts && thread_channel BuildingSlackThread(api_token, thread_ts, thread_channel, debug) end def initialize(api_token, thread_ts, thread_channel, debug: false) @api_token = api_token @thread_ts = thread_ts @thread_channel = thread_channel @debug = debug end def attach_file(message, file_path, file_name) ext = file_name.split('.').last Fastlane::UI.message("Attaching #{file_name} (ext = #{ext}) file is located in path #{file_path}") return if @debug Fastlane::Actions::FileUploadToSlackAction.run( api_token: @api_token, initial_comment: message, file_path: file_path, file_name: file_name, file_type: ext, channels: @thread_channel, thread_ts: @thread_ts ) end def success(message) Fastlane::UI.message("Sending success building message") return if @debug Fastlane::Actions::UpdateSlackMessageAction.run( ts: @thread_ts, api_token: @api_token, message: message, success: true, channel: @thread_channel, payload: [], attachment_properties: {}, default_payloads: %i[lane git_branch git_author last_git_commit], ) end def failure(message) Fastlane::UI.message("Sending failure building message") return if @debug Fastlane::Actions::UpdateSlackMessageAction.run( ts: @thread_ts, api_token: @api_token, message: message, success: false, channel: @thread_channel, payload: [], attachment_properties: {}, default_payloads: %i[lane git_branch git_author last_git_commit], ) end end
Подытожим
В данной статье показал самый минимум, доступный для автоматизации. Возможности расширения функционала безграничны. Для удобства весь код собран и доступен в репе.
Ключевые заметки:
-
Fastlane оказался удобен для разработки инструментов автоматизации
-
Fastlane подходит для Android и iOS приложений (для iOS функционала из коробки гораздо больше)
-
Все описанные кейсы можно перенести в CI/CD
-
Понять основы Ruby можно за день, если есть бэкграунд в разработке
-
С моей точки зрения, на Ruby c Fastlane разрабатывать автоматизацию гораздо проще, чем на том же Python.
ссылка на оригинал статьи https://habr.com/ru/articles/799691/
Добавить комментарий