← назад к разделу

Каждый раз, когда разработчик отправляет код, нужно проверить: компилируется ли, проходят ли тесты, нет ли очевидных уязвимостей. Делать это вручную — медленно и ненадёжно. CI (Continuous Integration) автоматизирует эту проверку.

Эта статья о том, как выглядит типичный CI-конвейер для Java/Spring Boot проекта и почему он устроен именно так.

Почему CI вообще нужен

Раньше разработчики работали независимо неделями, а потом пытались объединить код. Это называлось «интеграционный ад»: изменения конфликтовали, тесты падали, выяснить причину было сложно.

Continuous Integration решает это так: каждый коммит — автоматическая проверка. Ошибка видна сразу, пока контекст свежий. Чем чаще интегрируешь — тем меньше конфликтов.

Из каких шагов состоит конвейер

Типичный CI для Spring-сервиса выглядит так:

1. compile + unit-тесты       ~2–4 мин   первая проверка PR
2. статический анализ         ~2–3 мин   параллельно с шагом 1
3. интеграционные тесты       ~5–10 мин  Testcontainers: PG, WireMock
4. сборка образа и публикация ~2 мин     после зелёных шагов 1–3
5. деплой на staging          автоматом из main-ветки

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

Почему Gradle-сборка бывает медленной и как это исправить

Холодная сборка Spring Boot проекта может занять несколько минут только на скачивание зависимостей. Если CI делает это при каждом коммите — конвейер работает впустую.

Решение — кеширование. CI сохраняет папку ~/.gradle/caches между запусками и восстанавливает при следующем. Зависимости скачиваются один раз, дальше берутся из кеша.

Docker-образ тоже умеет в кеширование: если вынести зависимости в отдельный слой, он переиспользуется при сборке. Зависимости меняются редко — слой почти всегда закешируется.

Без этих оптимизаций конвейер легко упирается в 20+ минут. С кешами — 8–12.

Как правильно запускать тесты в CI

Тесты делятся на уровни, и в CI они запускаются в этом порядке:

Unit-тесты — самые быстрые. Тестируют один класс без Spring-контекста, без базы данных, без сети. Работают секунды. Бегут при каждом PR первыми.

Интеграционные тесты — поднимают Spring-контекст и реальные зависимости через Testcontainers. Например, PostgreSQL или WireMock для мокирования внешних HTTP-сервисов.

@SpringBootTest
@Testcontainers
class OrderServiceIT {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @Test
    void shouldSaveOrder() { ... }
}

Testcontainers запускает настоящий PostgreSQL в Docker прямо из теста. CI-раннер должен иметь доступ к Docker-демону — большинство GitHub Actions/GitLab CI раннеров это поддерживают.

Чтобы не поднимать контейнер для каждого тестового класса отдельно, контейнер объявляют static — он запустится один раз на весь прогон.

Два правила, которые держат CI живым

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

Тесты не ходят в интернет. Интеграционный тест, который обращается к реальному staging-серверу или публичному API, упадёт от чужих проблем — лёг соседний сервис, пропал интернет у раннера. Всё внешнее нужно мокировать через WireMock или поднимать контейнером.

Quality gates: что блокирует слияние

Quality gate — проверка, которая не даёт смерджить код, если что-то не так. Смысл в том, что gate либо блокирует, либо его нет. Отчёт, который никто не читает, — это не gate, а шум.

ПроверкаИнструментБлокирует
Компиляция с анализом предупрежденийError ProneДа
Статический анализ на багиSpotBugs + FindSecBugsДа, по уровню серьёзности
Уязвимые зависимостиOWASP Dependency-CheckДа, по порогу CVSS
Секреты в кодеGitleaksДа, всегда
Уязвимости в Docker-образеTrivyДа, по уровню серьёзности

Error Prone — компилятор-расширение от Google, ловит паттерны ошибок прямо во время javac. Например, перепутанные параметры или неправильное использование коллекций.

SpotBugs анализирует байткод и находит потенциальные баги: NullPointerException, проблемы с многопоточностью, небезопасное использование API.

Gitleaks сканирует историю коммитов на случайно попавшие секреты: токены, пароли, ключи API. Блокирует безусловно — попавший в репозиторий секрет нужно считать скомпрометированным.

OWASP Dependency-Check проверяет зависимости по базе CVE. Если в проекте используется библиотека с известной уязвимостью выше заданного CVSS-порога — сборка не проходит.

Что получается на выходе

Результат успешного CI — Docker-образ. Тег образа — хэш коммита, чтобы всегда можно было понять, что именно развёрнуто.

Spring Boot умеет добавлять метаданные сборки прямо в образ:

// build.gradle.kts
springBoot {
    buildInfo()
}

После этого /actuator/info возвращает версию, хэш коммита и время сборки. Удобно для отладки.

Образ публикуется в реестр (Docker Hub, GitHub Container Registry, ECR). Дальше начинается зона CD — что с ним делать в разных окружениях.

Как укладываться в бюджет времени

Хороший конвейер умещается в 15 минут. Если выползает — разбирают по шагам:

  1. Кеши — первое, что проверяют. Если кешей нет, всё остальное бессмысленно.
  2. Параллельность — шаги, которые не зависят друг от друга (компиляция и статический анализ), запускают одновременно.
  3. Параллельные тесты — Gradle умеет запускать тестовые классы параллельно через maxParallelForks.
  4. Ревизия тестовThread.sleep(10_000) в тесте это 10 секунд на каждом прогоне навсегда. Ждать нужно через Awaitility, а не через sleep.
  5. Мощные раннеры — только если всё выше исчерпано и упёрлись в CPU/память.

Частые ошибки

Тесты перезапускают автоматически при падении — нестабильность доезжает до продакшена, разработчики перестают верить CI.

Тесты обращаются к реальным сервисам — падают от чужих причин, доверие к CI падает вместе с ними.

SAST-отчёт без блокировки — уязвимости копятся, потому что «некогда разбираться». Gate с порогом по серьёзности решает это структурно.

Нет кешей — конвейер медленный с самого начала, и с этим просто мирятся.

Форматер кода как gate — каждый PR превращается в спор о скобках. Стиль лучше выравнивать автоформатером при сохранении файла, а не блокировать слияние.

Коротко

  • CI автоматически проверяет каждый коммит: компиляция, тесты, анализ безопасности.
  • Порядок шагов — от быстрых к медленным: unit → статический анализ → интеграционные тесты → сборка образа.
  • Кеш Gradle и кеш слоёв Docker сокращают время сборки в несколько раз.
  • Testcontainers поднимает реальную базу данных и другие зависимости прямо в тесте — без моков на уровне схемы.
  • Quality gates блокируют слияние: Error Prone, SpotBugs, OWASP, Gitleaks, Trivy.
  • Нестабильные тесты нужно чинить, а не перезапускать автоматически.
  • Тесты не ходят в интернет — всё внешнее мокируется WireMock или поднимается контейнером.
  • Результат CI — Docker-образ с тегом-коммитом.

Что почитать дальше

  • Принципы конвейера — общие правила, которые работают для любого стека.
  • Стратегии релиза — что происходит с образом после CI: rolling, blue-green, canary.