Привет! Я Дмитрий Прокоп, инженер в Авито. Я не люблю рутину и обожаю автоматизацию, поэтому я идеально подхожу для работы с мобильными релизами. 

В 2018 году мой коллега Алексей Шпирко уже рассказывал про наши релизные поезда и как мы пришли к этой концепции. Эта статья — логическое продолжение того, о чём говорил Алексей. Я расскажу, что у нас изменилось за прошедшие пять лет.

Релизный процесс, или релизные поезда 

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

Релизный поезд Авито состоит из шести этапов 
Релизный поезд Авито состоит из шести этапов 

Релиз начинается с этапа фича-фриза, на котором мы от dev-ветки отделяем новую ветку с релизом. С этого момента в неё запрещено вносить продуктовые правки. 

Затем для релизной ветки выполняем сборку и тестирование. Что мы делаем на этом этапе:

  • Прогоняем все автоматические проверки.

  • Подготавливаем нужные артефакты для регрессионного тестирования.

  • Создаём описание релиза для магазинов приложений.

  • Отправляем нотификации заинтересованным участникам. 

Следующий этап — стабилизация. Команды проводят ручное регрессионное тестирование, проверяют результаты автотестов, ищут и исправляют баги. На этом же этапе идёт ручной мониторинг крашей и отзывов. Исправляем ошибки, которые нашли, и заливаем исправления в релизную ветку.

После этого мы входим в цикл регрессионного тестирования: повторяем итерации «сборка-стабилизация», пока релиз не будет отвечать всем требованиям качества.

Как только мы понимаем, что релиз стабильный, мы начинаем раскатывать его на пользователей. Сначала поэтапно, на небольшой процент аудитории, при этом постоянно мониторим отзывы и краш-репорты. 

Если всё отлично, мы завершаем раскатку в продакшен на сто процентов пользователей.

Весь процесс от фича-фриза до попадания релиза в прод занимает не больше семи дней, обычно от понедельника до понедельника. Разработкой под iOS и Andoid занимаются одни и те же команды, поэтому релизы платформ чередуются. Получается, что мы выпускаем новую версию каждые две недели.  

Какие проблемы были у старого поезда

Релизный поезд держится на четырёх основных принципах:

  • регулярность,

  • частота,

  • стабильность,

  • прозрачность.

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

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

Эта упрощённая схема релизного пайплайна на основе TeamCity в несколько раз меньше реальной 
Эта упрощённая схема релизного пайплайна на основе TeamCity в несколько раз меньше реальной 

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

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

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

У нас был отлично работающий процесс, который позволял выпускать релизы раз в две недели, но при этом возникали проблемы с единственным «хранителем» информации. Хотелось немного разгрузить релиз-инженера, потому что его время стоит дорого. Нужно было сделать так, чтобы он мог в течение недели заниматься ещё какими-то задачами, помимо мониторинга релиза. 

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

Разработка сервиса релизов 

Будущий сервис должен был отвечать нескольким требованиям: 

  1. Простая схема работы без неявных связей и запутанных зависимостей. 

  2. Возможность перезапустить релиз с точки, в которой произошел сбой.

  3. Прозрачный и понятный для всех участников процесс.

  4. Лёгкая кастомизация и тестирование.

  5. Возможность использовать сервис в разных мобильных проектах Авито.

  6. Разработка отдельно для команд, которые занимаются релизами, и отдельно для тех, кто занимается тестированием и сборкой под платформы.

Первое и последнее требование были самыми важными, потому что они могли значительно улучшить релизные поезда. 

Мы начали с того, что разделили зоны ответственности: 

  • всё, что относится к сборке, тестированию, загрузке приложения в магазин мы отнесли к CI-части, которая зависит от платформы;

  • доставку приложения пользователям, управление процессом, нотификации, создание артефактов и тому подобное мы выделили в CD-часть.

После этого мы разработали концепцию CI/CD-контракта: CD-часть отправляет json-файл в CI-часть, чтобы запустить некоторую работу. Когда работа выполнена, CI отправляет ответную часть контракта с результатами.

Схема СD/CI-контракта
Схема СD/CI-контракта

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

{
"schema_version": 1,
"project": "avito",
"release_version"; "777.5",
"output_descriptor": {
"path": "http: //artifactory.ru/android/777.5_1/output. json"
"skip_upload": false
},
"deployments":	[
{
"type"; "google-play",
"artifact_type": "bundle"
"build variant": "release"
"track": "beta"
}
]
}

А после завершения работы CI-часть возвращает отчёт о том, что всё прошло успешно, все файлы лежат на своих местах: 

{
"schema _version": 1,
"teamcity_build_url": "https://mct.ru/viewLog.html?buildId=17317583",
"build number": "777",
"release version": "777.5",
"git_branch": {
"name": "release-avito/777.5",
"commit_hash": "2c54c50c220bf91bc1a6ca10b34f53a540c80551"
},
"test_results": {
"report_id": "5f3e94d23d67bf434e5c1b8",
"report_url": "https://tests.avito.ru/report/AvitoAndroid/FunctionalTests/2c54c50c220bf91",
},
"artifacts":	[
{
"type": "apk"
"name": "avito-777.5-777-release.apk"
"url": "http: //example.com/artifactory/android/avito/777.5-777/release.apl
"build _variant": "release"
},
]
}

Когда концепция контракта была готова, мы начали выстраивать вокруг неё сервис. Платформ-специфичную CI-часть мы решили оставить на базе TeamCity, но сократить до одного билда. Команда, которая работает с Android, написала билд, который запускает Gradle-таски, а iOS-команда настроила запуск скриптов для Python. Так мы обеспечили независимую работу команд. 

Кто-то или что-то должен был хранить состояние релиза и отвечать за CD-часть. Также у нас был ряд внешних сервисов, с которыми нужно взаимодействовать: Jira, Git, система управления тестами и мессенджеры. Поэтому мы решили сделать бэкенд-сервис на Python.

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

А финальной точкой для CI/CD-контракта мы выбрали Artifactory, потому что не хотели, чтобы CI что-то знал о сервисе. 

Загрузку артефактов в магазин на этом этапе мы также оставили в CI-части.

Новая схема сервиса намного проще и делает процесс прозрачнее для всех участников
Новая схема сервиса намного проще и делает процесс прозрачнее для всех участников

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

await self.ci_steps.build_and_test_avito_android_binary (
force=force_build_and_test, 
skip_deploy=skip_deploy, 
avito_track=self.data.google_play_track,
deploy_qapps-True,
)
await self.ci_steps.bump_version_name( )
await self.google_play_steps.upload_build( )
await self.feature_toggles_steps.upload_toggles()

Такой файл отдельный для каждого проекта и платформы. Релиз в нём собирается как конструктор, по кусочкам. В примере выше мы собираем бинарник, бампим версию и загружаем всё это в Google Play. Для каждого релиза набор может быть абсолютно уникальным и не зависящим от всех остальных. 

При этом все шаги можно запускать многократно с одними и теми же входными данными и получать одинаковый результат. Это позволяет перезапускать релиз с точки отказа. Если процесс на какой-то точке упал, мы просто нажимаем Restart и сервис автоматически валидирует корректность уже выполненных шагов. Если всё в порядке, процесс пропускает шаг и таким образом доходит до момента, в котором произошел сбой. 

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

Фронтенд сервиса выглядит примерно так
Фронтенд сервиса выглядит примерно так

После завершения этого проекта у нас появились:

  • дашборд как единая точка управления и хранения всей информации о релизах;

  • возможность перезапускать релиз с момента сбоя;

  • гибкая настройка релиза для каждой платформы или проекта;

  • простой, стабильный и прозрачный процесс;

  • независимость команд.

В этот момент мы поняли, что раз у нас такой классный сервис релизов и удобный автоматизированный процесс, то почему мы всё ещё релизим раз в две недели? Стало понятно, что нужно ускоряться. Решение выбрали не самое очевидное: добавили в релизный поезд этап beta.

Beta в релизном поезде: ожидание и реальность

В релизном процессе у нас есть цикл сборки, тестирования и стабилизации. Мы хотели просто добавить в него ещё шаг: после сборки приложения и до тестирования заливать его в beta-канал. 

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

В реальности все оказалось сложнее:

  • непонятно, как доставить beta-версию пользователям;

  • мы не понимали особенности платформ при beta-тестировании;

  • неясно, как получать обратную связь;

  • неизвестно, какие метрики автоматизировать и как; 

  • и самое важное — как мотивировать людей участвовать в beta-тестах. 

Разбираться со сложностями начали с особенностей платформ.

Для Android все оказалось достаточно просто. Мы отправляем пользователю ссылку на beta-версию приложения, он переходит по ней и регистрируется. После этого он получает обновления не из основного канала в Google Play, а из beta. При этом он может оставить отзыв так же, как для обычного приложения.

В случае с iOS достаточно пройти регистрацию по ссылке, но для загрузки beta-версий придётся установить специальное приложение TestFlight. При этом тестировщику очень сложно оставить отзыв: нужно сделать скриншот, написать комментарий. Наконец, даже для beta-сборок обязательно проходить ревью от Apple. 

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

Также у обеих платформ есть ограничение по количеству участников: Android-приложение могут тестировать не больше 200 тысяч человек, а iOS — 10 тысяч.

Ещё нам нужно было понять, как получить обратную связь и автоматизировать мониторинг отзывов

Android умеет выгружать отзывы по почте, поэтому мы можем просто забирать их своим сервисом по API, фильтровать и отправлять в канал. 

У iOS официального варианта получения отзывов по API нет, поэтому мы настроили сбор через FastLane. Дальше так же фильтруем отзывы и отправляем в канал. Такой способ работает, но примерно раз в месяц ломается. Это происходит потому, что Apple использует двухфакторную аутентификацию и ежемесячно инвалидирует токен для доступа к приватному методу, по которому мы забираем отзывы.

Кроме отзывов мы захотели настроить автоматический мониторинг крашей. Мы разделили все краши на два основных потока: 

  1. Важные, которые говорят о том, что у нас все горит и срочно нужно с этим что-то делать. На языке firebase это называется velocity alert. 

  2. Остальные, на которые нужно просто посмотреть и проверить, что они не критичные.

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

Единственная проблема, решение которой я не смогу подсказать, — это как привлекать людей. Но могу дать совет: определите когорту самых мотивированных пользователей, которые чаще других заходят в ваше приложение. Экспериментируйте и ищите способ привлечь их к beta-тестам.

Мы пытались привлекать пользователей рассылками, пушами, предлагали бонусы или деньги за участие. Конверсия всегда была одинаковая.

Сейчас у нас 117 тысяч beta-тестировщиков в Android и 2700 в iOS. У нас появились новые каналы мониторинга: от пользователей начали сыпаться сложные и странные баги. Это хорошо, потому что как бы мы ни тестировали, всё равно не сможем пользоваться приложением как обычные люди.

С бета-тестировщиками есть ещё одна проблема: людей надо мотивировать оставаться в программе. В случае с Android мы можем выстраивать диалог, когда отвечаем на отзывы в Google Play. В iOS связь строго односторонняя, и напрямую ответить пользователю, который прислал баг-репорт или отзыв, невозможно. Я знаю, что некоторые компании заводят специальные чаты в Telegram для beta-тестировщиков, чтобы удерживать их внимание хотя бы так.

Работу мы проделали огромную, получили хорошие результаты, но всё ещё оставались в двухнедельном цикле релизов. И тут нам пришла гениальная идея: а что если убрать из релизного процесса людей? 

Smoothie-release, или релизы без людей

Как я говорил ранее, продуктовой разработкой Android и iOS занимаются одни и те же команды и не могут тестировать две сборки одновременно. Поэтому мы решили попробовать сделать релизы автономными и независящими от людей. Мы назвали их smoothie, потому что ты пьешь смузи, а релиз катится сам по себе.

Чего мы ожидали от smoothie-релизов:

  • уменьшение TTM (time to market) и DLT (defect life time),

  • возможность быстро экспериментировать с продуктовыми и техническими характеристиками приложения,

  • уменьшение скоупа ручного тестирования.

Но главное, чего мы хотели — это выпускать релизы каждую неделю.

Концепция smoothie-релиза держится на beta-версиях приложения. Мы решили в каждом втором релизе ручное тестирование сделать опциональным: просто собираем и сразу передаём в beta.

Затем мы три дня мониторим отзывы и краши. Если за это время ничего не отвалилось, то считаем, что всё хорошо. Затем очень медленно, по 1% раскатываем релиз на обычных пользователей. Если на этом этапе тоже всё хорошо, значит, релиз жизнеспособный и его можно раскатывать на всех.

Зелёный сценарий: все прошло хорошо и мы получили два релиза за две недели
Зелёный сценарий: все прошло хорошо и мы получили два релиза за две недели

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

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

Если на каком-то этапе возникают нерешаемые проблемы, то мы отменяем релиз. Но всё равно считаем его успешным — ведь мы нашли критичный баг до основного релиза.

Красный сценарий: мы выкатили один релиз за две недели, но нашли критичный баг
Красный сценарий: мы выкатили один релиз за две недели, но нашли критичный баг

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

В какой-то момент мы поняли, что хотя всё работает, мы действуем по принципу «мне кажется, так будет лучше». То есть, на самом деле не понимаем состояние нашей системы, а значит нам нужны понятные метрики. 

Какие метрики мы отслеживаем и зачем

Мы ставили несколько целей:

  • повысить прозрачность системы,

  • подсветить возможные проблемы,

  • улучшить коммуникацию с другими командами Авито.

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

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

Метрики показали, что в среднем мы релизим iOS-версии приложения раз в девять дней. Мы уверены, что во всём виновато ревью от Apple
Метрики показали, что в среднем мы релизим iOS-версии приложения раз в девять дней. Мы уверены, что во всём виновато ревью от Apple

Также мы решили считать Success rate релизов, потому что есть smoothie-релизы, которые не всегда докатываются до прода. 

Smoothie-релизы не очень хорошо работают на iOS, скорее всего тоже из-за долгого ревью
Smoothie-релизы не очень хорошо работают на iOS, скорее всего тоже из-за долгого ревью

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

Оказалось очень важно отслеживать метрики по багам: их количество, владельцы и критичность. Это помогло узнать, в каких командах есть проблемы с качеством кода.

Последняя метрика — это число хотфиксов. Особенно интересовало, нет ли зависимости между выпусками хотфиксов и smoothie-релизами.

Самый грустный график: в посленовогодний горячий период больше половины релизов не докатывались до прода
Самый грустный график: в посленовогодний горячий период больше половины релизов не докатывались до прода

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

Но метрики также показали, что нас очень тормозит ревью Apple. Одновременно с этим появился запрос от бизнеса на релизы в новых магазинах приложений: RuStore, AppGallery и GetApps. Поэтому мы решили автоматизировать работу с магазинами.

Как мы автоматизировали работу с магазинами приложений

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

В целом хотелось за счёт управления магазинами повысить прозрачность, добавить дополнительную информацию на наш дашборд. 

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

В случае с Android-магазинами (GooglePlay, RuStore, AppGallery и GetApps) мы хотели автоматизировать всю рутинную работу: загрузку сборки, раскатку на группы пользователей, организовать управление раскаткой одной кнопкой на платформе.

На iOS самым важным было автоматизировать отправку на ревью и контроль прохождения. Дополнительно решили автоматизировать раскатку также на уровне платформы по одной кнопке. При автоматизации мы использовали API магазинов там, где они есть: у Google Play, AppGallery и AppStore. И для Android, и для iOS есть понятная документация по API.

Но в целом на Android пользоваться API удобнее и быстрее. Например, там есть удобная транзакционная модель: нужно создать объект релиза, наполнить его и запушить в Google Play. У AppGallery тоже есть несколько удобных API-ручек. 

У iOS не такой простой API: каждое ручное действие нужно повторить с помощью отдельной ручки. Это медленно, неудобно и часто заканчивается ошибкой по таймауту соединения. 

На платформе Android мы автоматически выкатываем beta-версию в Google Play. После цикла стабилизации дополнительно раскатываем релиз на ревью в AppGallery. Затем запускаем поэтапную раскатку в Google Play и вручную отправляем на ревью в RuStore. При этом релиз-инженер не может продолжить раскатку, пока не отметит галочкой, что версия ушла на ревью в RuStore. 

Схема автоматизации работы с магазинами приложений на платформе Android при поэтапной раскатке
Схема автоматизации работы с магазинами приложений на платформе Android при поэтапной раскатке

При раскатке в прод мы добавляем GetApps в полуавтоматизированном режиме. 

Схема выкатки в прод на Android
Схема выкатки в прод на Android

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

В формах есть все нужные ссылки, например, на credential RuStore 
В формах есть все нужные ссылки, например, на credential RuStore 

На iOS релиз-инженер вручную отправляет сборку на ревью на этапе билда и тестирования. С этого момента все новые пересборки автоматически взаимодействуют с AppStore. 

Форма для запуска новой версии приложения на ревью
Форма для запуска новой версии приложения на ревью

После ревью релиз-инженер получает уведомление, и может начать поэтапную раскатку и релиз на прод. 

Автоматизация действительно снизила влияние человеческого фактора на релизы, дала нам контроль над ревью Apple, повысила безопасность, стабильность и предсказуемость релизов. И главное — снизилась нагрузка на релиз-инженеров. 

Общие итоги и планы

За пять лет мы улучшили наш релизный поезд и построили качественную, классную автоматизацию. Сейчас мы релизимся в пять магазинов, при этом дежурит во время релиза всего один человек — и то в фоновом режиме. 

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

Предыдущая статья: Система Quality Score: как оценивать внешнее качество продукта

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