Каждый раз, когда разработчик отправляет код, нужно проверить: компилируется ли, проходят ли тесты, нет ли очевидных уязвимостей. Делать это вручную — медленно и ненадёжно. 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 минут. Если выползает — разбирают по шагам:
- Кеши — первое, что проверяют. Если кешей нет, всё остальное бессмысленно.
- Параллельность — шаги, которые не зависят друг от друга (компиляция и статический анализ), запускают одновременно.
- Параллельные тесты — Gradle умеет запускать тестовые классы параллельно через
maxParallelForks. - Ревизия тестов —
Thread.sleep(10_000)в тесте это 10 секунд на каждом прогоне навсегда. Ждать нужно через Awaitility, а не черезsleep. - Мощные раннеры — только если всё выше исчерпано и упёрлись в 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.