Continuous Integration (CI) - это машина, которая на каждый пуш заново собирает проект и прогоняет тесты в чистом окружении. Её ценность держится на двух свойствах: воспроизводимость (тот же коммит даёт тот же результат, у всех и всегда) и быстрая обратная связь (разработчик узнаёт о поломке за минуты, пока контекст ещё в голове). Потеряете первое - красный станет случайностью; потеряете второе - люди перестанут дожидаться сборки. Оба свойства проектируются намеренно.
Воспроизводимость начинается с фиксации входных данных. Прибейте версию Node - в CI и локально должна стоять одна и та же, иначе поведение таймеров, Intl и стримов будет разъезжаться. Устанавливайте зависимости из lockfile командой npm ci, а не npm install: ci ставит ровно то, что записано, и падает при расхождении, тогда как install волен обновить дерево. Кэшируйте store пакетного менеджера (npm/pnpm), а не node_modules вслепую: слепой кэш модулей переживёт смену Node или платформы и однажды подсунет несовместимые бинарники.
Флаг --ci меняет одно конкретное поведение и делает это ради воспроизводимости: встретив снапшот, которого ещё нет на диске, Jest в CI не запишет его молча, а провалит тест и потребует запустить с --updateSnapshot. Локально новый снапшот создаётся автоматически - удобно при разработке; в CI это дыра, через которую непроверенный снапшот утёк бы в зелёную сборку. Поэтому скрипт test:ci - это jest --ci, и порядок шагов в пайплайне фиксирован: сначала установка, потом статические проверки, потом тесты, потом сборка.
Скорость - вторая половина задачи. По умолчанию Jest поднимает пул воркеров по числу ядер минус одно и раскидывает файлы между ними - это быстро на многоядерной машине. Флаг --runInBand загоняет всё в один процесс: он нужен для отладки и для стабильности на тесных раннерах с малым лимитом памяти, но платить за него приходится временем. Промежуточный рычаг - --maxWorkers: например, --maxWorkers=50% ограничивает пул половиной ядер, чтобы не задушить контейнер с жёстким лимитом.
Когда сюита перестаёт помещаться в приемлемое время, её шардируют. Флаг --shard=1/3 в формате индекс/количество делит набор тестов на равные части, и три параллельные джобы CI берут по трети - jest --shard=1/3, --shard=2/3, --shard=3/3. Обратная связь ускоряется линейно числу шардов, а coverage потом собирается из частичных отчётов. Родственный рычаг быстрой обратной связи - --bail: он обрывает прогон после первого упавшего сюита, экономя минуты, когда и так ясно, что сборка красная.
Артефакты превращают красную сборку в диагноз. Сохраняйте отчёт тестов и coverage как artifacts джобы - тогда, не воспроизводя падение локально, видно, какой тест и на какой строке упал. Кэш здесь работает на скорость честно: он ускоряет transform в среднем вдвое, и отключать его (--no-cache) стоит лишь при доказанной проблеме именно с кэшем, потому что без него Jest минимум вдвое медленнее.
Обновление мажора - отдельная операция, а не строчка в этом же PR. Переходя на Jest 30, сперва прочитайте upgrade guide, затем прогоните сюиту на отдельной ветке и погасите все предупреждения до того, как трогать assertions. Порядок важен: если менять проверки и версию сразу, вы не поймёте, что сломалось - новый рантайм или ваша правка. Разведите изменения по времени, и красный всегда будет указывать на одну причину.
npm ci
npm run lint
npm run typecheck
npm run test:ci
npm run build
# Большой suite можно разделить:
npx jest --shard=1/3
npx jest --shard=2/3
npx jest --shard=3/3