Fastlane для Android разработчиков

от автора

Рост команды требует большего вовлечения в процессы и договорённости, которые, в свою очередь, требуют автоматизации и инспекции. Можно взять 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 и смотреть, в каком статусе сейчас находится выполнение.

Процесс сборки отображается в Slack

Процесс сборки отображается в Slack

Напишем 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.

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

Что используете для автоматизации Android проектов?

28.57% Fastlane2
42.86% Bash-скрипты3
28.57% Kotlin Custom Scripts (kts)2
57.14% Gradle-скрипты4
0% Другое (поделитесь опытом в комментариях)0

Проголосовали 7 пользователей. Воздержались 3 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/799691/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *