“Твою ж мать, какая же это хтонь!”. Примерно так можно было охарактеризовать все наши инфраструктурные скрипты до недавнего времени. Нужно было что-то менять, и мы сделали это. 

Меня зовут Павел Стрельченко, я – Android-разработчик компании hh. Я расскажу вам как эволюционировали наши CI скрипты на протяжении трех лет, с какими проблемами мы сталкивались, как анализировали их и пытались изменить, а также что вообще делали и к чему в итоге пришли. 

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

Очень важный дисклеймер

После просмотра видео или чтения этой статьи может сложиться впечатление, что настройка CI для сборок Android-приложений — невероятно сложное и трудное занятие, с которым вам может помочь утилита fastlane

Это неправильное ощущение, гоните его прочь.

Во-первых, настроить CI в 80-90% случаев — это довольно просто, благо в 2021 году уже есть множество инструментов, которые максимально упрощают этот процесс (вы можете посмотреть в сторону Jenkins / CircleCI, Github Actions, и так далее). 

Во-вторых, fastlane — не панацея от всего, и команда hh пока не готова рекомендовать его использовать. Важно помнить, что перед использованием любого инструмента необходимо проводить его изучение, чтобы понимать, насколько он подойдет в каждом конкретном случае, оценить все риски и взвесить стоимость его адаптации. 

Не верьте никому на слово, перепроверяйте информацию сами. 

Всем стабильного CI.

Как вам жилось без CI?

Начнем с истории трехлетней давности. Об этом периоде времени хорошо рассказал Саша Блинов в своем видео про удивительную историю рефакторинга. В те далекие времена одновременно с изменением кодовой базы происходили и инфраструктурные изменения.

На тот момент у нас вообще не было инфраструктурных скриптов. Роль билд-сервера взял на себя специальный разработчик по имени Антон. По требованию тестировщиков он в нужные моменты собирал релизные АПК, дебажные АПК и отдавал их тестировщикам на тестирование.

Это выглядело как-то так
Билд, это Антон. Антон, это билд, который надо собрать.
Билд, это Антон. Антон, это билд, который надо собрать.

Разумеется, ни о каких регулярных сборках речи вообще ни шло. У нас не запускались на регулярной основе никакие тесты. Следовательно, любой коммит в develop или в мастер-ветку мог разломать абсолютно всё.

Во-вторых, у нас не запускались никакие проверки статического анализа. Поэтому code style не соблюдался по всему проекту. Например, в истории коммитов нашего проекта можно было обнаружить специальные коммиты, которые применяли в очередной раз принятый код style на всю кодовую базу. Такое себе удовольствие.

Ну-ка, реально много?
В истории хватало коммитов, исправляющих код-стайл во множестве файлов
В истории хватало коммитов, исправляющих код-стайл во множестве файлов

Отсутствие чётко зафиксированного код-стайла в настройках IDE часто приводило к необходимости что-то подправить в нескольких файлах внутри коммита. Проходило несколько дней и править приходилось уже сразу большой объём файлов.

Как пытались улучшить ситуацию?

Мы решили, что больше так жить нельзя, пора заводить CI.

  • Настроили build-машины — Мы пошли к команде инфраструктуры hh и заказали у них специальные build-машины с нужной нам конфигурацией. Нам нужно было не так много: Java да Android SDK;

  • Сформулировали типы сборок — Мы сделали специальную табличку, в которую выписали название сборки, критерии, по которым нужно запускать тот или иной флоу, что конкретно надо запускать в рамках прогона.

Посмотреть на табличку
Матрица планов для CI
Матрица планов для CI

В таблице мы подробно описали: зачем нам нужен определённый тип сборки, по какому триггеру он должен отрабатывать, надо ли внутри него прогонять Unit-тесты, UI-тесты, стат. анализ, какие приложения каких build-типов он должен собирать.

И таким образом, у нас получилось четыре основных типа сборки:

  • Сборка pull request-а (PR) — разработчик создает PR, и мы запускаем сборку на нашем CI-сервере. Эта сборка проверяет компиляцию приложений, прогоняет стат. анализ и Unit-тесты;

  • Ночная сборка (Night) — это регулярные ночные сборки. Наши разработчики ведут работу надо фичами в отдельных фиче-ветках. И эти фиче-ветки мы будем собирать каждый день до тех пор, пока они не будет смерждены в develop. Здесь мы запускаем прогоны Unit-тестов, UI-тестов, проверяем компиляцию всех приложений. Короче говоря, максимально собирающий билд;

  • Сборка PR to Develop — сборка запускается при попытке merge-а в develop. Когда разработчик заканчивает разработку своей фичи, он пытается ее смерджить в develop. В этот момент мы должны хорошенько проверить эту фичу: прогоняются все тесты, прогоняется статический анализ и компиляция всего и вся;

  • Custom build — это билд, который можно было настроить абсолютно для всего. Потенциально он мог собирать и релизные версии, и дебажные версии, и различные флейворы, а также прогонять или не прогонять Unit-тесты etc.

Как мы реализовали эти четыре flow?

Мы написали один-единственный большой Gradle-скрипт, который, по нашей задумке, должен был уметь делать всё, учитывать любые шаги, запускать какие угодно тесты и так далее. То есть реализовывал бы Custom build-план.

Как это выглядело
Лишь маленький кусочек того огромного Gradle-скрипта
Лишь маленький кусочек того огромного Gradle-скрипта

На один экран весь скрипт не влезет, в этом же файле были размещены все нужные ему Groovy-классы и множество утилит.

И с помощью различных переменных с нашего CI-сервера мы настраивали включение тех или иных особенностей билда. Благодаря этому единому скрипту мы настроили все четыре наших flow.

Что за переменные?
Переменные в Bamboo для настройки очередного билда
Переменные в Bamboo для настройки очередного билда
  • is_crashlytics_release — флажок, от которого зависела отправка готовой сборки в Crashlytics Beta (скупая слеза ностальгии);

  • step_app_names — тут можно было указать список приложений для сборки, в данном плане было указано только соискательское;

  • step_build_types — можно было указать через запятую все необходимые типы сборок: Debug, PreRelease, Release;

  • step_extra — тут можно было добавить дополнительный запуск какой-нибудь gradle-задачи;

  • step_to_fabric — уже неясно, почему тут стояло true, ведь этот шаг требовал проброса пар вида “AppName:BuildType” для сборки дополнительных тестовых приложений;

  • step_ui_tests — надо ли запускать UI-тесты;

  • step_unit_tests — указание gradle-задачи для прогона тестов именно для данной сборки;

Если кратко, кастомизировалось всё довольно гибко, пусть и не очень консистентно.

Ура, у нас заработал CI!

Какие выводы сделали?

Жизнь без CI – это боль. И чем раньше вы его включите и начнете на регулярной основе запускать тесты, прогонять статический анализ, тем раньше качество вашего приложения и кодовой базы резко улучшится. 

Так прошло полгода.

Что бывает, когда не очень понимаешь, что делаешь

За полгода использования и периодических фиксов нашего супер-скрипта мы поняли, что на самом деле он не так хорош, как нам бы того хотелось. 

  • Скрипт был длинной простыней Groovy-кода — Он находился в одном-единственном Gradle-файле. Большую часть скрипта занимала огромная Gradle-таска, которая была написана совершенно без учета каких-то best practices Gradle-а;

Примечание

То, что скрипт был написан на Groovy на самом деле не так уж и плохо. Потому как в те времена про Kotlin Scripts мало кто знал, и сам KTS находился в зачаточном состоянии.

  • Мы не сразу поняли, что делала gradle-таска

А она, внимание:

  • Формировала текст bash-команд;

  • Записывала их в специальный shell-файлик;

  • А потом этот специальный shell-файлик запускала.

То есть у нас происходило так: на CI мы запускали Gradle-таску. Она запускала bash, который… снова запускал Gradle. 

Ну, вы поняли
Олды здесь? Pimp my ride, ну.
Олды здесь? Pimp my ride, ну.

В видео я вставил совсем не ту шутейку, которую хотел, так что вставлю её в статью.

В какой-то момент мы обнаружили, что наши сборки на PR-ах идут примерно по часу. А билд сканы мы посмотреть толком не можем — из-за того, что запускаемая gradle-задача была странно написана. 

  • Непонятки со статическим анализом — Эти скрипты запускались через неведомые тропы, поэтому мы не могли нормально обновить тот же detekt;

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

Как решали новые проблемы?

  • Постарались уйти от использования супер-кастомного скрипта — Для этого мы вынесли генерацию bash-команд просто в bash-скрипты, которые мы сконфигурировали с помощью UI нашего CI-сервера. На Bamboo так можно. Мы пока вообще не думали про проблемы, заключенные внутри этих bash-скриптов, мы их просто вынесли и начали запускать на регулярной основе;

Как это выглядело?
bash-скрипты прямо на Bamboo
bash-скрипты прямо на Bamboo

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

  • Начали настраивать прогон наших UI-тестов — попросили помочь с этим наших коллег из инфраструктурных команд. Они подняли кластер Kubernetes, настроили для нас запуск эмуляторов (про всё это можно посмотреть отдельный доклад). Со своей стороны мы описали небольшой Docker-контейнер, внутрь которого положили java, Android SDK и утилиту Marathon, с помощью которой собирались запускать наши UI-тесты.

Итоги работ за полгода

Что у нас получилось сделать?

  • Перестали использовать скрипт Custom Build-а на всех flow, кроме релизного — Скрипт до недавнего времени всё ещё жил в нашей кодовой базе, а избавились от него мы буквально месяц назад;

  • Логика инфраструктурных скриптов стала чуть понятнее — Мы визуализировали ее в UI-интерфейсе нашего CI-сервера, разбили на шаги, и поэтому жить стало чуть проще;

  • Исправили проблему, связанную с прогоном наших PR — Они больше уже не шли по часу, плюс мы восстановили работу build scan-ов;

  • Регулярные прогоны UI-тестов ускорили регрессы — Мы получили возможность чаще релизиться.

Чему мы научились за этот период?

Мы научились тому, что все-таки не стоит писать эти инфраструктурные скрипты на скорую руку. Лучше разобраться в используемой технологии, чтобы не получалось, что “gradle запускает bash, который запускает gradle”. 

А ещё, UI-тесты – это классно. Чем раньше вы их затащите на регулярной основе, тем лучше. Но помните, что для них понадобится подготовить инфраструктуру, принять множество решений про инструменты, обсудить процесс работы и так далее.

Так прошло еще полгода.

Я знаю, что вы делали 2 года назад

За полгода в команде произошли некоторые изменения и накопилось несколько проблем:

  • Сменились люди, работавшие над инфраструктурными скриптами — Из-за низкого bus-фактор-а, нам пришлось заново разбираться во всех build-скриптах;

  • Долго не рефакторили bash-скрипты — При этом мы постепенно расширяли их функциональность, они становились всё сложнее и сложнее;

  • Дублирование логики в скриптах — Мы вскоре заметили, что скрипты дублировали друг друга почти в каждом flow, который мы запускали на CI. Они отличались в мелочах: где-то нужен был специальный gradle-флаг, где-то необходимо было запускать тесты, где-то нет. Изменения или обновления, которые мы хотели внести в эти все скрипты, приходилось дублировать почти во всех вкладках настроек разных планов и flow;

А покажи как выглядело
Две вкладки на соседних планах — найдите 5 отличий.
Две вкладки на соседних планах — найдите 5 отличий.

А теперь представьте, что таких вкладок не две, а около десяти.

  • Изменения в CI-скриптах раскатывались сразу на всех — Конфигурация планов CI была единой для всех разработчиков, поэтому приходилось либо создавать какой-то промежуточный план для тестов обновлений, либо выдумывать специальные трюки;

  • В логах на CI было трудно разобраться — Наш CI-сервер, конечно, пишет в лог, когда запущен тот или иной шаг внутри нашего билда. Но в потоке логов, которые занимали несколько мегобайт, эти строчки легко было пропустить.

  • Иногда менялись настройки build-машины — Иногда команда инфраструктуры что-то меняла на уже настроенных машинах, и там исчезали нужные нам для запусков утилиты.

Как мы жили, что мы делали?

  • Попробовали решить задачу обновления sh-скриптов — Для этого мы вытащили их из нашего CI в отдельные sh-файлы и добавили внутрь репозитория. Теперь CI запускал не какой-то зафиксированный для всех sh-скрипт, а запускал тот скрипт, который находился внутри нашего репозитория. Бонусом к этому мы получили возможность изменять кусочек любого sh-скрипта и тестировать его внутри определённой ветки. Да, добавлять новые шаги на CI по-прежнему было нельзя, потому что это сразу раскатывалось на всех. Однако bash-скрипт мог запустить какой-нибудь другой bash-скрипт, и таким образом мы могли проверить новые шаги;

И много ли скриптов было?
sh-скрипты внутри репозитория
sh-скрипты внутри репозитория

Много. И многие из них ещё и дублировались раньше в разных планах.

  • Немного упростили чтение логов — Мы добавили к нашим скриптам небольшую утилитку, которая на вход принимала название скрипта, который нужно запустить и его аргументы. При помощи специальной конструкции в bash мы понимали, какой именно скрипт хотим запустить, писали понятное описание, добавляли кучу специальных символов, чтобы их можно было легко отличить в логах. И дальше уже запускали нужный нам скрипт.

А что за конструкция?

Если вам когда-нибудь в жизни было интересно, как выглядит выражение “switch-case” на bash-е, то вот оно:

  • Перенесли часть задач в Docker — Для уменьшения рисков, связанных с переносом сборок на различные машины, мы начали потихонечку задумываться над тем, чтобы перенести выполнение наших задач, которые пока что выполнялись на билд-машинах, внутрь Docker-контейнера. В частности, например, сборку APK.

Мы тогда вообще многое поняли

  • Нужно увеличивать bus-фактор — Расширяйте экспертизу всех разработчиков в работе инфраструктурных скриптов, чтобы не терять важную информацию при уходе людей. С тех пор мы проводим демки, пишем статьи в wiki, чтобы знания, которые хранятся в головах разработчиков, хранились еще где-нибудь;

  • Docker упрощает перенос инфраструктуры — Сейчас мы в любой момент можем перенести наши сборки с одной машины на другую, главное чтобы там были bash и Docker;

  • Не пишите сложные конструкции на bash-е — Объемные функции и всякие switch-case-ы на bash-е выглядят сложно и трудно поддерживаются.

В таком состоянии наша кодовая база жила почти до сегодняшнего дня. Сейчас мы переместимся чуть ближе к маю 2021 года.

Дела не так давно минувших дней

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

А ещё мы продолжали изменять функциональность существующих инфраструктурных скриптов без их глобального рефакторинга. Да, мы понимали, что там есть проблемы. Мы знали, как можно что-нибудь улучшить. Но повода как-то не представлялось. И ежики кололись, бодались, но продолжали жрать кактусы.

Наш редактор очень просил вставить эту картинку
Ёжики кололись, бодались, но продолжали жрать кактусы
Ёжики кололись, бодались, но продолжали жрать кактусы

Я не мог ему отказать.

С какими вообще проблемами мы сталкивались?

  • Запутанные bash-скрипты — Нам сильно аукнулось то, что мы переносили bash-скрипты из нашего старого Groovy-скрипта без каких-либо изменений. Мы особо не вчитывались в логику bash-скриптов, когда переносили их на Bamboo, мы не вчитывались в них и когда переносили их обратно в репозиторий. Это сыграло злую шутку с нами, потому что в какой-то момент мы поняли, что эти скрипты очень запутаны. Один скрипт вызывает другой, второй вызывает третий, и все это как-то непоследовательно. Аргументы передаются из скрипта в скрипт и конвертируются непонятно во что;

  • Каждый второй скрипт использует переменные Bamboo — Почему это плохо? Потому что мы оставались сильно привязаны к нашему CI-серверу. И если бы мы, допустим, захотели в это время перейти на какой-нибудь другой CI-сервер (ну скажем, Jenkins), то нас бы ждал очень неприятный сюрприз и большое количество дополнительной работы;

Что за CI-переменные?
Скрипт привязан к специфичным CI-переменным
Скрипт привязан к специфичным CI-переменным

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

  • Не вся работа выполняется внутри Docker-контейнера — На дворе 2021 год, а мы все еще не перевели всю работу наших инфраструктурных скриптов внутрь Docker-контейнера. Часть скриптов выполнялась по-прежнему на билд-машинах, и она страдала от переноса сборок на те или иные машины. Часть выполнялась внутри Docker-контейнера;

  • Зоопарк языков программирования в инфраструктурных скриптах — За три года существования этих инфраструктурных скриптов у нас сформировался целый зоопарк языков, на которых мы писали эти скрипты. Там был и Groovy, и Kotlin, и Bash, и Python, и даже Go!

  • “Посторонние скрипты” — часть инфраструктурных скриптов находилась в совершенно постороннем репозитории, который был никак не связан с мобильными разработчиками. И когда эти скрипты ломались, мы даже не знали, куда идти, кого спрашивать и как вообще фиксить проблемы, с ними связанные.

О версионировании скриптов

Добавьте ко всем проблемам еще и то, что все наши инфраструктурные скрипты по-прежнему настраивались через UI-интерфейс нашего CI-сервера.

Почему это плохо? Потому что эта конфигурация планов никаким образом не версионировалась. И она была единой для всех. Из этого вытекал целый океан новых проблем.

  • Усложнялось добавление новых шагов — нам приходилось либо терпеть и страдать от каких-то падений инфраструктурных скриптов, либо ждать, когда скрипт с фиксом будет подмержен в develop. Был и третий вариант: проверять, существует ли определенный файлик внутри нашего репозитория. А если существует, то запускать его. Это очень сильно осложняло работу по обновлению скриптов;

  • Дублирование шагов между планами Bamboo — Для переиспользования шагов описанных flow нам приходилось дублировать описания этих шагов между настройками плана на Bamboo. Допустим, один план запускал сборку APK, и другой план его запускал. Нам приходилось дублировать все эти запуски различных скриптов между планами;

  • Отсутствие код-ревью для настроек планов — а это значит, что их качество может сильно страдать;

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

  • Нельзя переиспользовать общую логику между платформами — а такая логика была. Например, отправка нотификаций в Slack, проставление ссылок в Jira, Github. Но так как скрипты конфигурировались на Bamboo, мы не могли это переиспользовать.

Всё это образовало ту самую ХТОНЬ. Нужно было от этого уходить, но нам не хватало какого-то триггера, повода всё изменить.

Как мы собирались решать проблемы

Пока триггер не появлялся на горизонте, мы всё равно планировали наши улучшения. 

  • Перенос инструментов в Docker — Вопрос запуска тех или иных утилит вне Docker-а можно было решить переносом всех инструментов, которые мы используем, и перемещением их в Docker-контейнер. Мы описали Docker-файл, внутри которого установили всё, что нам нужно: и Java, и Android SDK, и Marathon, и Allure, и Python, и Gradle-Profiler и массу всего другого;

  • Единая точка входа в Docker — Мы описали специальный bash-скриптик, который сегодня стал новой единой точкой входа для наших инфраструктурных скриптов. Этот bash-скрипт просто запускает Docker-контейнер и передает управление в него, чтобы нужные нам команды и утилиты вызывались внутри контекста Docker-контейнера;

Выглядело это довольно просто
Единая точка входа в Docker-контейнер
Единая точка входа в Docker-контейнер

Написав такой скрипт, можно использовать его на CI вот так:

sh ci/run_in_docker.sh "some_command_for_execution"
  • Конвертация CI-переменных в ENV — Проблему сильной привязки наших инфраструктурных скриптов к нашему CI-серверу можно было решить описанием списка всех CI-переменных в одном месте. Мы пробросили их внутрь Docker-контейнера как ENV-переменные, после чего используем внутри всех скриптов только их. Если мы захотим когда-нибудь уйти от Bamboo, нам будет достаточно поменять только один файл, внутри которого используются эти Bamboo-переменные;

Проброс CI-переменных
Конвертируем CI-переменные в ENV-ы
Конвертируем CI-переменные в ENV-ы

А в Docker это можно пробросить вот так:

docker run \
    --env-file <(echo "$envs" | grep -E '.+=.+') \
    ...

Это пробросит только те ENV-ы, где значение после знака '=' не является пустой строкой.

С остальными проблемами, вроде отсутствия версионирования скриптов, зоопарка языков, трудностей обновления скриптов и отсутствия review, нам мог бы помочь такой подход, как Infrastructure as Code, то есть, описание инфраструктуры как какого-то кода.

Полностью честный Infrastructure as Code мы вряд ли осилили бы. Однако по максимуму перевести инфраструктуру в код мы могли себе позволить. И нам оставалось только выбрать инструмент: как именно мы будем реализовывать все эти инфраструктурные вещи внутри нашей кодовой базы.

Из-за леса, из-за гор показал мужик QA

И тут появились они. Наши тестировщики, наш великолепный QA-отдел. В одном из выпусков “Охэхэнных историй” наш тестировщик Даня уже рассказывал об автоматизации нашего релизного флоу. В связи с этим в кодовой базе Android-приложения появились такие инструменты как Ruby и fastlane.

И наши тестировщики были настроены серьезно. Им не хотелось переписывать уже работающие под iOS скрипты на какие-то другие инструменты, поэтому они топили за использование fastlane.

Несмотря на то, что для Android-разработчиков более привычными инструментами являются Kotlin, Gradle-скрипты и Bash, мы все-таки решили попробовать и дать шанс fastlane с Ruby.

  • Сможем отказаться от зоопарка языков — Потому что всё, что может Python и Groovy, в целом может и Ruby;

  • Возможность использовать наработки iOS-команды — iOS-разработчики у нас уже имеют большую экспертизу в использовании Ruby и Fastlane. Поэтому мы могли, использовать их опыт для review и созданные ими наработки для наших автоматизаций;

  • Обширная экосистема Ruby и fastlane — У Ruby и fastlane есть довольно обширная экосистема, которая используется в задачах, связанных с continuous integration и continuous delivery. Мы провели небольшое исследование и поняли, что в целом все задачи, которые нам нужны, мы можем реализовать через Ruby и fastlane, отказавшись от использования старых скриптов;

И вообще, в ходе каких-нибудь революционных изменений, типа перехода на fastlane и Ruby, мы могли разом исправить и те проблемы, о которых я уже упоминал. Мы подумали: «Почему бы и нет?».

Да, мы понимали все риски. Android-разработчикам совершенно непривычно работать с Ruby. И перевод вообще всех скриптов на новые рельсы – очень долгое и непростое дело. Однако объем накопившихся проблем перевесил все эти риски, поэтому мы начали нашу подготовку к изменениям.

Масштабные изменения в скриптах

На этот раз мы решили подойти к изменению инфраструктурных скриптов системно, поэтому мы создали специальную Miro-доску, в которую выписали всю имеющуюся у нас информацию про наши CI flow: какие шаги где используются, какие из них можно переиспользовать, какие аргументы подаются на вход, что нам надо запускать параллельно, а что можно последовательно и так далее.

Картинки из Miro
Выписали отдельные элементы планов, что в них происходит
Выписали отдельные элементы планов, что в них происходит
Расписано было много
Расписано было много
Пример детализированной job-ы, что происходит шаг за шагом
Пример детализированной job-ы, что происходит шаг за шагом
И таких расписанных планов было много
И таких расписанных планов было много

Мы начали изменять наши скрипты, начиная с плана, связанного с pull request. Так как мы твердо решили запускать все эти действия на CI внутри Docker-контейнера, соответственно, мы туда добавили и инструменты, которые помогут нам запускать Ruby и fastlane (rvm / bundler).

И после этого мы начали переводить существующие инфраструктурные скрипты на язык Ruby. Попутно пытались переиспользовать уже имеющиеся у iOS-команды утилиты. 

Для этого мы создали отдельный репозиторий, который оформили как fastlane-плагин, и начали выносить туда общий код. Вынести получилось довольно много: работу с Jira, утилиты для git-а, общие константы и модели, да еще и код для нашего релизного флоу.

Общий репозиторий
Наш небольшой репозиторий с плагином
Наш небольшой репозиторий с плагином

Структура репозитория была создана автоматически через fastlane new_plugin

Немного о боли и страданиях

После всего этого вы бы точно спросили меня: «Неужели всё так хорошо и здорово получилось с этими Ruby и fastlane?». Кажется, стоит рассказать и про наши страдания. Я вас не разочарую, потому что, разумеется, их у нас было в достатке.

  • Android-разработчики редко работают с Ruby — перестроить свой мозг джависта и человека, который работает с Kotlin, на такой язык довольно сложно. Исчезают привычные концепции: абстрактного класса в Ruby нет, интерфейсов а-ля Java тоже нет. Зато появляются некоторые другие интересные особенности языка, тот же duck typing.

  • Динамическая типизация – это больно — у нас был довольно болючий пример, который мы обнаружили у себя только через месяц после добавления кода.

В чём была боль?

Мы написали скрипт, который должен был проверять наши тестовые стенды на их актуальность. Одним из его шагов было создание json-модельки и отправки её на сервер. При создании модели нужно было сконвертировать дату в массив чисел для нашего сервера. 

FULL_DATE_TIME_FORMAT = '[%Y,%m,%d,%H,%M,%S,0]'

private_class_method def self.current_time_for_fixtures(plus_days: nil)
    time = Time.now
    time -= (24 * 60 * 60 * plus_days) unless plus_days.nil?
    time.strftime(FULL_DATE_TIME_FORMAT).split(',').map(&:to_i)
end

Из-за ошибочного формата FULL_DATE_TIME_FORMAT конвертация происходила неправильно, и результатом функции был массив с нулями. Это поломало нам кучу UI-тестов, случались какие-то дикие флакования. По-хорошему, раз функция не смогла что-то сконвертировать, она должна была выкинуть какое-то исключение, но нет, Ruby интерпретировал ошибку как 0. 

Вот так работает правильно:

FULL_DATE_TIME_FORMAT = '%Y,%m,%d,%H,%M,%S,0'

private_class_method def self.current_time_for_fixtures(plus_days: nil)
    time = Time.now
    time -= (24 * 60 * 60 * plus_days) unless plus_days.nil?
    time.strftime(date_format).split(',').map do |arg| 
        Integer(arg, 10)
    end
end

Кратко — боль.

  • Android Studio не умеет работать с Ruby — Для работы с Ruby можно использовать VS Code (с плагинами VSCode Ruby, Ruby, ruby-rubocop, Ruby Solargraph), либо IntelliJ IDEA Ultimateплагином Ruby), либо RubyMine;

  • Недостаточная поддержка fastlane в IDE — fastlane-специфика не поддерживается и можно нехило выхватить с этого. Однажды я полчаса искал ошибку, по которой у меня не вызывался fastlane Action, и оказалось, что у меня в классе Action не было суффикса Action. IDEA-инспекции тут бы могли спасти, но не спасли. Потому что их не было. 

  • Проблемы с приватным общим репозиторием — Потратили много времени, прежде чем научились использовать написанный fastlane plugin на CI, были какие-то проблемы с пробросом специальной ENV-переменной access token для доступа к Github-репозиторию;

  • Отсутствие общих правил написания Ruby-кода между платформами — Код-стайл мы легко расшарили с помощью Rubocop, а вот как мы именуем функции, как обобщаем нужный код, — таких правил у нас не было. iOS-коллеги писали код по-своему, мы по-своему. На review у нас постоянно были какие-то непонятки;

  • Проблемы с запуском bash-команд из fastlane-а — У нас почему-то стабильно не срабатывала команда grep, но в итоге мы её переписали на Ruby, и всё стало хорошо.

Плюсы текущего подхода

Несмотря на все трудности, переход на fastlane существенно улучшил наши скрипты инфраструктуры.

  • Получилось описать flow наших CI-планов в виде высокоуровневых функций — Такие функции легко читать и поддерживать;

В качестве примера
desc 'Run checks for PR: static analysis + unit tests + app build'
lane :checks_for_pr do
  add_jira_link_to_pr(
    pull_path: HeadHunter::GitHub.pulls_path(HeadHunter::Platform::ANDROID)
  )
  static_analysis
  build_and_run_unit_tests
  approve_if_possible
end

# А дальше — отдельные функции add_jira_link_to_pr / static_analysis / etc
  • Перевели практически все CI-планы на использование fastlane — Собираемся добивать остальные, потому что нам это понравилось.

  • Ушли от зоопарка языков — У нас остался только bash в виде одного скрипта + Ruby;

  • Конфигурация CI-планов теперь лежит в исходном коде репозитория — Теперь эти скрипты проходят code review. Более того, они могут проходить ревью и у Android-разработчиков, и у iOS-команды;

  • Получилось переиспользовать общую логику с iOS-командой — Мы вынесли общие скрипты в отдельный репозиторий, обе команды могут ими пользоваться.

Подводим итоги всего рассказа

Какие решения мы считаем неудачными за всю историю нашей эволюции?

  • Отсутствие CI — Это, пожалуй, самая большая проблема и ошибка, которую мы допустили на старте. То есть, чем раньше вы запустите у себя CI, который начнет на регулярной основе собирать ваше приложение, тестировать его, проверять на валидность с помощью скриптов статического анализа, тем лучше. Не затягивайте с этим, потому что это резко улучшит качество ваших приложений и кодовой базы;

  • Маленький bus-фактор в вопросах инфраструктуры — Если вы потеряете сакральные знания о разных частях приложения, вам потребуется много времени, чтобы их восстановить. Распространяйте знания, устраивайте демо, пишите wiki;

  • Зоопарк языков в инфраструктурных скриптах – это зло — Желательно сводить к минимуму количество инструментов, которые вы используете. Меньше инструментов = проще разобраться..

  • Описание CI-задач только в UI-интерфейсе CI-сервера — Из этого вытекает множество проблем, о которых я уже писал выше.

Какие решения мы считаем удачными:

  • Подход Infrastructure as Code — Этот подход позволяет версионировать ваши инфраструктурные скрипты, позволяет их ревьювить, легко менять, обновлять, создавать новые шаги;

  • Переиспользование общих скриптов между платформами — Это может подойти далеко не каждой команде, но здорово, когда получается переиспользовать общую инфраструктурную логику;

  • Увеличение bus-фактора — Чем больше людей знает про вашу инфраструктуру и всякие тонкости, тем лучше.

На этом у меня всё. Если у вас будут какие-то вопросы, пожелания, критика оставляйте их в комментариях.

Полезные ссылки

Комментарии (0)