Тесты перед каждым релизом шли 29 минут даже без изменений в коде. Сократили до 14 — и мерж теперь доезжает до прода за 3–5 минут

Курьерская платформа, которую мы построили для своего же курьерского парка, живёт на диалогах с реальными людьми: сценарий бота, передача заявки диспетчеру, статус заказа курьеру. Правки в неё прилетают по несколько раз в день — то нужно починить ветку сценария, то добавить кнопку, то закрыть дырку в проверке документов. Чем дольше код едет от «слили в основную ветку» до «работает у живого курьера», тем дольше мы терпим баг, который давно нашли и давно исправили в коде, просто он ещё не доехал.
Курьерская платформа, которую мы построили для своего же курьерского парка, живёт на диалогах с реальными людьми: сценарий бота, передача заявки диспетчеру, статус заказа курьеру. Правки в неё прилетают по несколько раз в день — то нужно починить ветку сценария, то добавить кнопку, то закрыть дырку в проверке документов. Чем дольше код едет от «слили в основную ветку» до «работает у живого курьера», тем дольше мы терпим баг, который давно нашли и давно исправили в коде, просто он ещё не доехал.

Как увидели

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

- 31% прогонов тестов в день отменялись на середине, потому что человек делал новый пуш раньше, чем предыдущий прогон успевал закончиться — машина честно тратила минуты, а результата не было никогда;

- в 18 из последних 30 слияний в основную ветку набор тестов гонялся повторно на коде, который уже проверялся тем же набором тестов минуту назад внутри самого запроса на слияние — просто потому что слияние считается новым событием, а не тем же самым кодом;

- параллельный прогон тестов брал 4 потока на машине, где по квоте было выделено 1,5 процессорного ядра — три потока из четырёх не ускоряли проверку, а дрались за одно и то же ядро.

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

Как решили

Разделили два разных вопроса — «что проверено» и «что сейчас работает на проде» — и связали их жёстко по идентификатору коммита, а не по положению в очереди:

- выкладка теперь явно запрашивает тот самый коммит, который прошёл тесты, а не «всё, что сейчас лежит в ветке»;

- если дерево кода слияния совпадает с уже зелёным прогоном тестов по этому же запросу на слияние, конвейер берёт готовый результат, а не гоняет проверку с нуля — внутри эту логику назвали «переиспользование по дереву»;

- число параллельных потоков тестов подровняли под реальную квоту процессора (с четырёх до двух), а правило «не пушь, пока не закончился предыдущий прогон» записали рядом с остальными правилами репозитория — это уже для людей, не для машины;

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

Результат

Набор тестов — 29 минут → 14. Первый мерж, прошедший по новой схеме «переиспользования», занял 1 минуту вместо полного прогона, потому что код действительно не менялся с прошлой проверки. Журнал выкладки стал писать открыто: «запрошен такой-то коммит… выкатываем… успешно» — то есть в любой момент видно, какой именно код сейчас отвечает курьерам. По итогам дня слияние кода доезжает до живого прода за 3–5 минут, если ветка не менялась параллельно. Раньше этот путь не измеряли в минутах вообще — просто ждали, пока «докатится», и почти всегда это время уходило на холостой повторный прогон, а не на реальную проверку.

Что я для себя унёс

Пока «проверено» и «развёрнуто» в голове команды — это одно и то же, конвейер будет врать именно в момент, когда это важнее всего: под нагрузкой, когда мержей много и они идут подряд. Привязывайте выкладку к конкретному id коммита, а не к позиции в очереди или моменту запуска — правка на вид копеечная, но без неё можно месяцами думать, что в проде работает проверенный код, хотя это не так. И второе: если дерево кода не изменилось с прошлого зелёного прогона, повторная проверка — это не аккуратность, а чистая потеря времени команды. У нас разница получилась в разы: от «подождём, пока докатится» до пяти минут на весь путь.

11