Linear переработала CI, чтобы успевать за ИИ-агентами

Весной этого года CTO Linear Туомас поставил автору задачу «CI обходится слишком дорого» и заодно попросил ускорить сам CI. Причина в том, что ИИ-агенты сделали написание кода экспоненциально быстрее, а проверка изменений за этим темпом не поспевала: каждый пул-реквест всё равно должен пройти CI целиком, поэтому по мере ускорения разработки CI превратился в узкое место, росли расходы на инфраструктуру, а разработчики и агенты дольше ждали обратной связи.
Результат работы: несмотря на то что набор тестов вырос почти в четыре раза с начала года, время ожидания пул-реквеста в CI снизилось с более чем 6 минут до чуть больше 5, а раннер-время на один тест сократилось примерно вдвое. Изменения шли по четырём направлениям: обновление инфраструктуры и инструментов, оптимизация задач, блокирующих остальную работу, сокращение повторяющейся настройки и ускорение самого выполнения тестов. Кодовая база Linear в основном на TypeScript, но большинство приёмов применимо и к другим языкам.
Первые улучшения дала смена инфраструктуры без изменения самого конвейера: переход с GitHub Actions на сторонние раннеры с более быстрыми процессорами, более производительным хранилищем и лучшим кешированием ускорил задачи в среднем на 34% (при сравнении двух дней до и после переключения), а отдельные нагрузки вроде проверки типов (tsc), на 52%. Переход на tsgo, нативный компилятор TypeScript, сократил недельную медиану проверки tsc на 73% и убрал typecheck из числа узких мест вовсе. Отдельно переписали кастомные lint-правила: часть из них опиралась на информацию о типах TypeScript, из-за чего каждый запуск линтера сначала строил полный граф типов, самый прожорливый по памяти шаг в CI. Правила переписали на статический анализ синтаксического дерева без типов, что ускорило линтинг API на 68%, линтинг всего репозитория, на 55%, заметно снизило потребление памяти и облегчило последующий переход на Oxlint.
Далее команда посмотрела на CI как на систему и занялась мелкими задачами, блокирующими остальные. Каждый прогон начинается с проверки, какие пути затронул пул-реквест и не прогонялись ли уже такие же тесты, эти проверки стоят на критическом пути, потому что ни один из восьми шардов API-тестов не стартует, пока они не закончены. Ограничение глубины git-выборки (fetch depth) сократило самый медленный из этих шагов с 94 до 20 секунд, а полный отказ от checkout там, где рабочее дерево вообще не нужно, с 27 до 7 секунд; для событий push и merge-queue частичный checkout без части истории сэкономил ещё около 11 секунд. После смены раннеров checkout стал случайно подвисать из-за нестабильной сети между сторонними раннерами и GitHub, команда заменила стандартный actions/checkout собственным composite-действием с повтором попыток и таймаутом (обрыв зависшего соединения примерно через 30 секунд) и постоянным git-зеркалом на диске раннера. Отдельно перенесли запись кеш-меток из финальной проверки перед мержем в отдельную задачу, которая ничего не блокирует, это сэкономило 42 секунды на каждом пул-реквесте и записи в merge-queue. В сумме такие изменения критического пути убрали около минуты из обязательной проверки для API-пул-реквестов при промахе кеша.
Затем занялись повторяющейся настройкой: установкой пакетов, поднятием раннера, подготовкой зависимостей, из-за которой задача на несколько секунд полезной работы могла съедать целые минуты инфраструктурного времени. Установку клиента Postgres (7, 8 секунд на каждый из шардов API-тестов при каждом прогоне) перенесли в базовый CI-образ с Node и уже установленным клиентом. Кодовая база организована как pnpm-воркспейс, и тесты API раньше устанавливали зависимости всего воркспейса целиком, ограничение установки только пакетом API и его зависимостями сократило pnpm install с 44, 73 секунд до 16, 18. Кеширование node_modules протестировали и отказались от него: ключ кеша слишком часто менялся из-за lock-файла, и даже попадание в кеш занимало около 28 секунд против примерно 7,5 секунды на отфильтрованную установку без кеша. Вместе эти три изменения сократили настройку одного шарда примерно на 44%, со 110, 140 секунд до 67, 73. Отдельно перестали при каждом прогоне повторно накатывать всю историю миграций базы данных, даже если схема не менялась, и перешли на готовый снапшот схемы, настройка БД на контейнер упала примерно с 12 секунд до 1, 2. Семь независимых мелких проверок, каждая из которых поднимала раннер, делала checkout и ставила зависимости ради нескольких секунд полезной работы, объединили в две задачи, запускающие все семь параллельно внутри себя, по данным за июнь, это сэкономило около 87 000 раннер-минут в месяц, то есть 11,8% всего расхода CI.
Последнее направление, само выполнение тестов. Vitest распределяет работу по файлам, а не по фактической длительности тестов, поэтому несколько крупных файлов могли монопольно держать шард, пока остальные шарды уже закончили. Крупные файлы разбили на более мелкие, сохранив структуру тестов, и перешли с четырёх шардов (ранее увеличенных с трёх) к восьми, по первым замерам это ускорило и удешевило критическую задачу примерно на 19%, а спустя неделю самый медленный шард сократился с 5,25 до 4,33 минуты. Vitest по умолчанию изолирует каждый тестовый файл, из-за чего граф сущностей, GraphQL и декораторов пересобирался в каждом шарде заново, команда ввела опциональный режим isolate:false, при котором «безопасные» файлы делят общий реестр модулей внутри одного воркера. Это стало крупнейшей отдельной оптимизацией, около 17% месячной экономии: самый медленный шард упал примерно с 300, 379 до около 195 секунд, а суммарное раннер-время API-шардов, примерно с 32,8 до 22 минут за прогон. Это же изменение сочли самым рискованным с точки зрения корректности: право на режим давали явным opt-in-комментарием в каждом файле, добавили нужный teardown для общего состояния, а часть файлов с поддельными таймерами или общим состоянием, которые не удалось безопасно расцепить, оставили в изолированном режиме. Поскольку сейчас большинство тестов пишут агенты, соответствующие agent skills обновили так, чтобы сгенерированные тесты по умолчанию учитывали это ограничение.
Шардирование окупается только тогда, когда фиксированная стоимость одного шарда низкая: удвоение числа шардов удваивает и время, потраченное на их настройку. Именно сокращение настройки сделало восемь шардов практичными, при старых 110, 140 секундах на шард восемь шардов потратили бы на одну только настройку 15, 19 минут раннерного времени, больше, чем сами тесты; сейчас настройка занимает около 40 секунд, и восемь шардов в сумме тратят на неё меньше времени, чем раньше тратили четыре, при этом распараллеливая тесты вдвое сильнее. Если бы этой работы за год не было, сегодняшний набор тестов занимал бы около 11 минут, почти вдвое дольше, чем сейчас ждут разработчики. Кодовая база продолжает расти, сейчас добавляется примерно 2000 тестов в неделю, поэтому в Linear описывают удержание скорости CI как постоянную, не закрытую задачу.
Ключевые факты
- При росте набора тестов почти в четыре раза с начала года время ожидания пул-реквеста в CI снизилось с более чем 6 минут до чуть больше 5, а раннер-время на один тест сократилось примерно вдвое.
- Переход с GitHub Actions на сторонние раннеры ускорил задачи в среднем на 34% (tsc, на 52%), а переход на компилятор tsgo сократил недельную медиану проверки типов на 73%.
- Переписанные без учёта типов TypeScript lint-правила ускорили линтинг API на 68%, линтинг всего репозитория, на 55% и открыли дорогу к переходу на Oxlint.
- Сокращение повторной настройки, базовый CI-образ с предустановленным клиентом Postgres, установка только нужного pnpm-пакета вместо всего воркспейса, отказ от кеша node_modules, снапшот схемы БД вместо повтора миграций, объединение семи мелких проверок в две задачи, сократило настройку одного шарда примерно на 44% и сэкономило около 87 000 раннер-минут в месяц.
- Переход с четырёх до восьми шардов Vitest и режим isolate:false с общим реестром модулей для безопасных файлов дали крупнейшую экономию, около 17% ежемесячных расходов, но признаны самым рискованным с точки зрения корректности тестов изменением.
Почему это важно
ИИ-агенты резко ускорили написание кода, а проверка изменений в CI не поспевала за этим темпом, CI стал узким местом, которое тормозит разработчиков и агентов и увеличивает расходы на инфраструктуру. Это не частная проблема одной компании: любая команда, которая наращивает объём кода за счёт ИИ-агентов, упирается в ту же стену, конвейер проверки, рассчитанный на прежний темп людей-разработчиков.
Кому это важно
Инженерным и платформенным командам, которые поддерживают CI/CD для кодовых баз на TypeScript и в монорепозиториях на pnpm, особенно если они уже используют или планируют использовать ИИ-агентов для написания кода и тестов. Полезно и командам, выбирающим между GitHub Actions и сторонними раннерами, а также тем, кто настраивает шардирование Vitest.
Как это применить
Из разбора Linear можно взять конкретный набор приёмов: сменить раннеры на более быстрые, если узкое место в самой инфраструктуре; убрать зависимость линт-правил от построения полного графа типов там, где хватает анализа синтаксиса; ограничить глубину git-выборки и убрать checkout из задач, которым не нужно рабочее дерево; вынести операции, не блокирующие мерж, с критического пути; ставить зависимости только для нужного пакета в монорепозитории, а не для всего воркспейса; проверить, не дешевле ли пересобрать node_modules, чем восстанавливать их из кеша; заменить повтор всех миграций БД готовым снапшотом схемы; объединять мелкие независимые проверки в общие задачи; при увеличении числа шардов сначала снизить фиксированную стоимость настройки одного шарда, иначе удвоение шардов удвоит и время настройки; для доверенных тестов включать раздельный реестр модулей (isolate:false) только с явной пометкой в файле и обязательным teardown общего состояния.
Можно ли доверять
Источник, собственный инженерный блог Linear на linear.app, то есть цифры самоотчётные и независимой проверке не подвергались. При этом почти к каждому изменению приведены конкретные показатели «до/после» в секундах или процентах, а не общие формулировки, что снижает риск голословности. Публикация вызвала заметное обсуждение на Hacker News (261 балл, 301 комментарий на момент сбора), но это говорит об интересе к теме, а не о независимой проверке цифр.
Риски и подводные камни
Сама команда Linear называет режим isolate:false в Vitest самым рискованным с точки зрения корректности изменением: без явного opt-in-комментария и добавленного teardown общее состояние между тестами внутри одного воркера может незаметно испортить результаты, поэтому часть файлов с таймерами-подделками и общим состоянием намеренно оставили в изолированном режиме. Часть приёмов завязана на конкретный стек (TypeScript, pnpm-монорепозиторий, Vitest) и не переносится напрямую на другие языки и инструменты. Увеличение числа шардов без предварительного снижения стоимости настройки одного шарда просто перекладывает время с тестов на настройку, сами авторы отмечают, что восемь шардов стали оправданны только после сокращения настройки примерно до 40 секунд.
«ИИ-агенты сделали разработку кода экспоненциально быстрее, но проверка этих изменений не поспевает за тем же темпом.»
— из инженерного блога Linear