До этого мы крутили учебные пайплайны, которые печатали "hello" и вызывали echo. На реальном проекте конвейер начинается с того, что нужно собрать приложение из исходников - и тут вылезает первая боль. На машине разработчика "у меня всё собирается", а на сервере jenkins сборка падает: не та версия JDK, нет Maven в PATH, npm тянет другие зависимости, тесты внезапно красные. Классика "works on my machine".
Цель этого урока - сделать так, чтобы jenkins build был воспроизводимым: одинаковым на ноуте разработчика, на агенте и в проде. Разберём, как подключить инструменты сборки, как выглядит честный стейдж компиляции и тестов для Java и Node, как кешировать зависимости и публиковать отчёты по тестам. И почему в 2026 году самый надёжный способ - собирать внутри контейнера, а не на "толстом" агенте с предустановленным софтом.
Маленькая сверка по версиям, чтобы не строить на песке. Актуальная LTS на середину 2026 - Jenkins 2.541.x (вышла 21 января 2026). Линия 2.541.x - последняя, где ещё поддерживается Java 11; начиная с апрельского baseline контроллер требует минимум Java 17, а weekly-сборки уже просят Java 21. Это касается JVM, на которой работает сам Jenkins. JDK, которым ты собираешь приложение, - отдельная история и задаётся в пайплайне.

Откуда берутся JDK, Maven, Gradle и Node: два подхода
Есть ровно два рабочих способа дать стейджу нужные инструменты.
Подход 1: директива tools. В "Manage Jenkins -> Tools" ты заводишь установки - JDK, Maven, Gradle, NodeJS (последнее требует плагина NodeJS). Каждой даёшь имя, например jdk21 или maven3. Дальше в Jenkinsfile блок tools подставляет их в PATH и выставляет JAVA_HEME автоматически. Если установка помечена как "Install automatically", Jenkins сам скачает нужную версию при первом запуске.
Код: Выделить всё
pipeline {
agent any
tools {
jdk 'jdk21'
maven 'maven3'
}
stages {
stage('Build') {
steps {
sh 'java -version'
sh 'mvn -B -ntp package'
}
}
}
}
Подход 2: Docker-агент. Тут окружение описано прямо в репозитории: ты говоришь "собирай вот в этом образе", и Jenkins поднимает контейнер, монтирует туда workspace и выполняет шаги внутри. Версия JDK и Maven зашита в образ - значит, она одинаковая у всех и не зависит от настроек сервера. Это и есть воспроизводимость.
Код: Выделить всё
pipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-21'
args '-v $HOME/.m2:/root/.m2'
}
}
stages {
stage('Package') {
steps {
sh 'mvn -B -ntp clean package'
}
}
}
}
Типовой CI-стейдж: jenkins maven, gradle и npm
Любая jenkins сборка приложения раскладывается на одни и те же фазы: checkout -> зависимости -> компиляция -> тесты -> упаковка. Меняется только инструмент. Разберём по очереди.
Jenkins Maven. Команда mvn package сама прогоняет жизненный цикл: компиляцию, тесты (surefire), сборку jar. Флаг -B (batch mode) убирает интерактив, -ntp (no transfer progress) гасит шумный лог скачивания.
Код: Выделить всё
pipeline {
agent { docker { image 'maven:3.9-eclipse-temurin-21'; args '-v $HOME/.m2:/root/.m2' } }
options { timeout(time: 20, unit: 'MINUTES') }
stages {
stage('Checkout') { steps { checkout scm } }
stage('Build') { steps { sh 'mvn -B -ntp -DskipTests clean package' } }
stage('Test') { steps { sh 'mvn -B -ntp test' } }
}
post {
always {
junit 'target/surefire-reports/*.xml'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
Jenkins Gradle. Здесь ключевое правило - запускать обёртку ./gradlew, а не глобальный gradle. Обёртка фиксирует версию Gradle прямо в репозитории, и сборка не зависит от того, что стоит на агенте.
Код: Выделить всё
stage('Build') {
steps {
sh 'chmod +x gradlew'
sh './gradlew --no-daemon clean build'
}
}
Jenkins npm. Для фронтенда и Node-сервисов в CI всегда используй npm ci, а не npm install. Команда ci ставит зависимости строго по package-lock.json, без "плавающих" версий, и она заметно быстрее. Перед запуском чистит node_modules - это ровно та детерминированность, которая нужна на сервере.
Код: Выделить всё
pipeline {
agent { docker { image 'node:22-alpine' } }
stages {
stage('Install') { steps { sh 'npm ci' } }
stage('Build') { steps { sh 'npm run build' } }
stage('Test') { steps { sh 'npm test -- --ci' } }
}
post {
always { junit 'reports/junit/*.xml' }
}
}
Кеширование зависимостей и сборка в Kubernetes
Скачивать зависимости заново на каждый build - дорого и медленно. Стратегия зависит от типа агента.
На постоянном агенте всё просто: каталоги ~/.m2, ~/.gradle и node_modules переживают перезапуски, и кеш греется сам собой. На эфемерном агенте (контейнер живёт только время сборки) каталог исчезает вместе с контейнером, поэтому кеш надо вынести наружу. В Docker-агенте это volume на хост (мы это сделали через args '-v $HOME/.m2:/root/.m2'). В Kubernetes - persistentVolumeClaim, смонтированный в pod, либо внешний прокси-репозиторий вроде Nexus или Artifactory, который кеширует артефакты централизованно.
Эфемерные pod-агенты в Kubernetes - это золотой стандарт масштабирования jenkins build в 2026. Каждая сборка получает свежий pod, который удаляется после завершения. Никакого мусора между сборками, никакого "загрязнённого" workspace, и контроллер не держит простаивающих агентов. Описывается это через kubernetes-плагин прямо в Jenkinsfile - pod-шаблоном с нужными контейнерами:
Код: Выделить всё
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-21
command: ['sleep']
args: ['infinity']
'''
}
}
stages {
stage('Build') {
steps {
container('maven') {
sh 'mvn -B -ntp clean package'
}
}
}
}
}
Типичные грабли
- Запуск глобального gradle/mvn вместо ./gradlew или версии из tools. Сборка зелёная у тебя и красная у коллеги - потому что версии разные. Лечится обёрткой или Docker-агентом.
- npm install в CI вместо npm ci. Подтягивает свежие минорные версии зависимостей, и сборка перестаёт быть детерминированной. На сервере - только npm ci.
- junit в обычном steps вместо post { always { ... } }. Если упадёт компиляция, до публикации отчёта дело не дойдёт, и ты не увидишь, какие тесты красные. Всегда always.
- Gradle daemon на эфемерном агенте. Висит после сборки, ест память, иногда ловит чужой кеш. Добавляй --no-daemon.
- Путаница, какой JDK где. Java для самого Jenkins (минимум 17 на LTS 2026) и JDK для сборки приложения - это разные вещи. Версию сборки задавай образом контейнера или директивой tools, не надеясь на системную.
- Не примонтирован кеш на Docker-агенте. Каждый запуск качает зависимости с нуля, сборка тянется минутами. Прокинь volume на .m2 или .gradle.
Мини-лаба
Повтори руками, минут на 20-30.
- Возьми любой простой Maven-проект (хоть сгенерированный архетипом) и напиши Jenkinsfile с Docker-агентом maven:3.9-eclipse-temurin-21.
- Разнеси сборку на стейджи Checkout, Build, Test. В post { always } добавь junit и archiveArtifacts с jar.
- Запусти, посмотри вкладку "Test Result". Сломай один тест нарочно - убедись, что сборка стала UNSTABLE (жёлтой), а не FAILED, и отчёт всё равно опубликовался.
- Примонтируй кеш .m2 через args, запусти дважды и сравни время второго прогона.
- Бонус: повтори то же для Gradle (./gradlew build, --no-daemon) или для Node (npm ci && npm run build).
- Чем npm ci отличается от npm install и почему в CI нужен именно ci?
- Почему шаг junit ставят в post { always }, а не в обычный steps?
- Какие два способа дать стейджу нужный JDK/Maven ты знаешь и в чём плюс Docker-агента для воспроизводимости?
- Где живёт кеш зависимостей на эфемерном pod-агенте в Kubernetes и почему его нельзя оставить внутри контейнера?
Реальная jenkins сборка - это всегда одна и та же цепочка: checkout, зависимости, компиляция, тесты, упаковка. Maven делает это через mvn package, Gradle через ./gradlew build, Node через npm ci и npm run build. Инструменты подключаются либо директивой tools, либо - что надёжнее в 2026 - Docker-агентом или эфемерным pod-агентом в Kubernetes, где версии зашиты в образ. Кеш выноси наружу, отчёты тестов публикуй в post { always } через junit, а артефакты сохраняй archiveArtifacts. Сделаешь так - и "у меня собирается" перестанет быть отговоркой.