https://habrastorage.org/webt/n8/lo/r5/n8lor5lkvnqev0xktqwxayrv1t0.png


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


Существует большое количество статей и видео, в которых подробно описывается преимущество использования автоматизированной сборки проекта и содержимое этих статей применительно к React Native (впрочем и к другой кроссплатформе) сводится к следующим действиям:


  1. Инициализируем Fastlane в папках iOS и Android
  2. Копируем платформо-зависимые скрипты вида: clean, build, publish
  3. Вставляем их в соответствующие iOS и Android директории
  4. Публикуем приложение!

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


  1. Как синхронизировать код публикации между платформами?
    Очевидно, что процесс дистрибуции будет одинаков для всех платформ (увеличить версию, запушить код, отправить сообщение об ошибках или успешной публикации и т.д.). Зачем нам поддерживать два разных Fastfile файла, если логика в них будет дублироваться и противоречить принципу DRY?
  2. Что делать если хочется кастомного поведения?
    Учить Ruby, потому что Fastlane использует его, и любое действие не покрытое командой Fastlane обречено на написание собственного костыля. Бедным JS разработчикам, незнакомым с синтаксисом Ruby, приходится учить еще один не типизированный язык, чтобы добавлять новый функционал, что увеличивает сложность проекта.
  3. Что с тестами?
    Да, мы покрываем процесс сборки тестами, поскольку он содержит в себе много кастомной логики, решающей например проблемы автоматического версионирования в Android. Для iOS у Fastlane есть механизм, а для Android нет. В Ruby мы не нашли простого способа подключить механизм тестов, потому что он требовал создания полноценного Ruby проекта, что было через чур, поэтому мы пошли немного другим путём.
  4. Как шарить скрипт публикации между проектами?
    Банальный копипастинг из проекта в проект приводит к устареванию скрипта. Его эволюционное развитие в разных проектах происходит совершенно по разному, и через какое-то время команде становится сложно принять решение, какой скрипт стоит использовать для следующего проекта.

Решение этих проблем по-началу вылилось в один большой Ruby файл с унифицированным API, который импортировался в директорию платформы и использовался как основа. С его помощью код публикации для Android платформы стал похож примерно на это:


# Импортим наш файл с основной логикой
import "../../scripts/Fastfile.rb"

lane :publish do |options|
  # обязательно указываем платформу для которой производится сборка
  self.runner.current_platform = :android

  assert_git()
  assert_environment(env: options[:env])
  assert_credentials()

  new_version=increment_version(type: options[:increment])
  version_description=set_version(version: new_version)

  build(env: options[:env])

  upload(track: options[:track])

  commit(text: 'Up version to ' + new_version)
  commit_tag(text: 'android/' + new_version)
  push_all()

  message(text: 'Новая версия приложения "'+ app_name + '" отправлена в Google Play. Публикация в ' + options[:track].upcase + ' ' + version_description + url)
end

Из скрипта видно, что сборка происходит методом build, реализация которого зависит от платформы и находится в Fastfile.rb. До какого-то времени нас устраивало это решение: мы вынесли большую часть дублирования в общий файл и просто импортировали его где необходимо.


Но каждый раз лезть в Ruby, чтобы починить баг или изменить поведение, становилось болью для всех. Поскольку мы пишем на React Native и используем JS окружение, ставшее для нас родным, мы подумали что было бы неплохо использовать для публикации именно его, максимально абстрагируясь от Ruby. Так родился новый JS фреймворк! (на самом деле нет)


Мы лишь написали простенький фронтенд (бридж, если угодно) к Fastlane, что позволило нам описывать всю логику публикации не выходя из родного болота JS, с ее готовыми библиотеками на каждый чих. Теперь написание скрипта выглядит примерно так:


https://habrastorage.org/webt/vq/bi/pp/vqbippe_pkcckg5utfsdseb948a.gif


Обратите внимание на подсказки, автокомплит и документацию доступную прямо во время написания скрипта.


В итоге получается нечто следующее:


import { android, gradle, AndroidPlatform, ui, supply, Incrementer } from "@lamantin/fastpush"

const androidPlatform = new AndroidPlatform()

const [oldVersionCode, newVersionCode] = await androidPlatform.incrementVersionCode()
ui.success(`Success update build [${oldVersionCode}] -> [${newVersionCode}]`)

android([
  gradle("clean"),
  gradle("assemble", {
    build_type: "Release",
  }),
  supply({ track: "beta" }),
])

И ладно писать (это делается один раз), но поддерживать скрипты публикации стало гораздо проще:


  • Мы получили типизацию с помощью TypeScript и больше не боимся передать параметр туда, где его не ждут
  • У нас есть возможность подключить любую JS библиотеку
  • Скрипт публикации больше не копипастится из проекта в проект, а распространяется как обычная библиотека, имеет версионирование и синхронизирована между всеми проектами
  • Для публикации мы имеем только 1 файл, который необходимо поддерживать. Больше не нужно скакать между платформами и папками ios и android как в обычном Fastlane окружении
  • Использование одного языка для разработки и дистрибуции приложения, уменьшило сложность проекта и позволило привлечь джунов писать тесты для него

Предположим, что мы написали отличный скрипт, покрыли его тестами и теперь хотим использовать всегда. Нет проблем, подключаем любую CLI библиотеку, описываем опции и аргументы, чтобы передать в скрипт и получаем следующее:


https://habrastorage.org/webt/mf/2q/yy/mf2qyyzncnmxlbijlkctcl8lzwu.png


Всё это доступно и вам в публичном Github репозитории, с документацией и примерами использования.


Как подключить себе


Одной командой вашего любимого пакетного менеджера


yarn add @lamantin/fastpush --dev
# или
npm install @lamantin/fastpush --save-dev

Ниже приведен пример, который мы используем когда нужна небольшая кастомизация в процессе сборки, но менять что-то кардинально не хочется:


import { publish } from '@lamantin/fastpush/build/src/cli/publish'
import { fastpush, FastpushResult } from '@lamantin/fastpush/build/src/cli/fastpush'
import { git } from '@lamantin/fastpush/build/src/utils'
import Telegram from 'our-own-app/Telegram'

// парсим аргументы командной строки
const options: FastpushResult = fastpush(process.argv)

// вызываем основной скрипт публикации (доступный также через CLI), переопределяя один из процессов сборки
publish(options, {
  // onPostPublish - функция, которая вызывается каждый раз при успешной сборке
  onPostPublish: async (platform, [prevVersion, version], [prevBuild, build]) => {
    const store = platform.type === 'ios' ? 'App Store ' : 'Google Play '
    const message = `App "My App Name"  sended to ${store}, track ${options.track.toUpperCase()}.\\n Version: ${tag}`
    Telegram.sendMessage(message)    
  }
})

Здесь мы используем функцию fastpush, которая парсит аргументы командой строки, преобразуя их в JS объект options, с которым дальше мы можем оперировать как угодно. В текущем варианте, он просто передается в функцию publish предоставляемую нашей библиотекой. Все это сделано лишь для того, чтобы переопределить процесс выполняемый после публикации (в данном случае нам требовалось только отправить сообщение, без лишних действий).


Как насчет Flutter?


Хоть наше решение и ориентировано в первую очередь под React Native, по идее не должно быть никаких проблем использовать его вместе с Flutter (но мы не пробовали).




Проект находится в стадии активного допиливания и приветствует ваши Pull Requests, Issues или хотя бы комментарии.
Надеемся, что он будет полезен кому-то кроме нас или подтолкнет на написание более гибких решений.