В этом уроке разбираем оба подхода бок о бок: scripted pipeline (полный Groovy, гибкий и опасный) и declarative pipeline (структурированный и читаемый). Покажу одну и ту же задачу в двух вариантах, объясню, когда хватает declarative (а это процентов девяносто случаев), когда реально нужен scripted, и как блок script внутри declarative служит мостом между мирами.
Откуда взялись два синтаксиса pipeline в Jenkins
Сначала был только scripted. Когда в Jenkins 2 появился Pipeline-as-Code, конвейер описывали обычным кодом на Groovy - языке поверх JVM. Это давало абсолютную свободу: хочешь цикл - пиши цикл, хочешь сгенерировать стадии на лету - пожалуйста. Но свобода обернулась болью. Каждый автор изобретал свою структуру, jenkins groovy расползался, и поддерживать чужой конвейер было мучением.
В ответ команда Jenkins сделала declarative - надстройку с жёсткой структурой. Ты не пишешь произвольный код, а заполняешь заранее определённые блоки: pipeline, agent, stages, steps, post. Меньше гибкости - больше порядка, читаемости и понятных сообщений об ошибках. На 2026 год официальная рекомендация однозначна: declarative - выбор по умолчанию для новых конвейеров, scripted - только когда декларативного синтаксиса реально не хватает.
Важная деталь под капотом: declarative - это не отдельный движок, а слой проверки и структуры поверх того же Groovy. Поэтому внутри он умеет в нужном месте пускать произвольный код через блок script. Об этом ниже.
Пара слов про окружение, чтобы примеры были применимы. Актуальная Jenkins LTS на середину 2026 - 2.541.x (релиз 2.541.1 от 21 января 2026). Эта линейка LTS - последняя с поддержкой Java 11; weekly-сборки уже требуют Java 21, а ближайшие LTS перейдут на Java 17/21. Если ставишь Jenkins с нуля - бери Java 21, не прогадаешь. И ещё: визуальный редактор Blue Ocean заморожен - новых функций не будет, только критические правки безопасности. Так что синтаксис мы пишем руками в Jenkinsfile, а смотреть стадии удобнее через Pipeline Stage View или Pipeline Graph View.

Одна задача, два синтаксиса: declarative pipeline jenkins и scripted рядом
Возьмём типовую задачу: собрать Maven-проект, прогнать тесты, при успехе - условно задеплоить, и в любом случае сохранить артефакты и убрать рабочую директорию. Сначала declarative.
Код: Выделить всё
pipeline {
agent { label 'linux' }
options {
timeout(time: 20, unit: 'MINUTES')
disableConcurrentBuilds()
}
environment {
APP_VERSION = '1.4.0'
}
stages {
stage('Build') {
steps {
sh 'mvn -B -DskipTests clean package'
}
}
stage('Test') {
steps {
sh 'mvn -B test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh "./deploy.sh ${APP_VERSION}"
}
}
}
post {
always {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
cleanWs()
}
}
}
Теперь то же самое на scripted pipeline:
Код: Выделить всё
node('linux') {
def appVersion = '1.4.0'
timeout(time: 20, unit: 'MINUTES') {
try {
stage('Build') {
sh 'mvn -B -DskipTests clean package'
}
stage('Test') {
try {
sh 'mvn -B test'
} finally {
junit 'target/surefire-reports/*.xml'
}
}
if (env.BRANCH_NAME == 'main') {
stage('Deploy') {
sh "./deploy.sh ${appVersion}"
}
}
} finally {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
cleanWs()
}
}
}
Когда хватает declarative, а когда нужен scripted pipeline
Практическое правило простое. Если твой конвейер - это линейная или почти линейная последовательность стадий с парой условий, declarative покрывает всё: matrix для параллельных матриц сборки, parallel внутри стадии, when для условий, post для уведомлений, credentials для секретов. Это и есть те самые девяносто процентов.
Scripted реально оправдан, когда нужна динамика, которую структура declarative не выражает. Классические примеры: генерация стадий в цикле по списку, который известен только во время выполнения; сложные вычисления над результатами предыдущих шагов; нетривиальная работа с замыканиями и Map. Вот где scripted на месте - динамические стадии:
Код: Выделить всё
node {
def services = ['auth', 'billing', 'gateway']
def branches = [:]
for (svc in services) {
def name = svc
branches["build-${name}"] = {
stage("Build ${name}") {
sh "docker build -t registry.local/${name}:latest ./${name}"
}
}
}
parallel branches
}
Но прежде чем тянуться к scripted, вспомни про мост. Внутри declarative-стадии есть блок script, который пускает произвольный jenkins groovy ровно там, где он нужен, не ломая остальную структуру:
Код: Выделить всё
pipeline {
agent any
stages {
stage('Resolve version') {
steps {
script {
def pom = readMavenPom file: 'pom.xml'
env.APP_VERSION = pom.version
echo "Собираем версию ${env.APP_VERSION}"
}
}
}
stage('Build') {
steps {
sh "docker build -t app:${env.APP_VERSION} ."
}
}
}
}
И раз речь про современный стек: declarative прекрасно дружит с эфемерными агентами в Kubernetes. Pod создаётся под сборку и удаляется после неё, а описывается это прямо в agent:
Код: Выделить всё
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 clean package'
}
}
}
}
}
- Блок pipeline должен быть на самом верху Jenkinsfile - никакого кода до него. Если хочешь определить функцию или переменную глобально, выноси её ВЫШЕ блока pipeline, а внутри стадий вызывай через script. Перемешивать свободный Groovy и declarative на одном уровне нельзя - получишь ошибку разбора.
- Присваивание env-переменных делается в блоке environment или внутри script - не пытайся писать env.FOO = 'bar' прямо в steps без script.
- В scripted легко забыть try/finally и потерять post-обработку при падении. В declarative блок post гарантирует выполнение - не воспроизводи его руками без необходимости.
- Не путай scripted с declarative-через-script. Блок script внутри стадии - это островок Groovy, а не разрешение писать весь конвейер императивно.
- Захват переменной цикла в замыкании (как в примере с динамическими стадиями) - переприсваивай значение в локальную переменную внутри тела цикла, иначе все замыкания получат последнее значение.
- Возьми любой свой проект и напиши для него declarative Jenkinsfile с тремя стадиями: Build, Test, Deploy. Деплой оберни в when по ветке main.
- Перепиши тот же конвейер в scripted-стиле через node и try/finally. Сравни длину и читаемость - почувствуй разницу руками.
- Добавь в declarative-вариант стадию с блоком script, которая читает версию из файла (readFile или readMavenPom) и кладёт её в env.
- Попробуй сгенерировать 3 параллельные стадии в цикле в scripted-варианте через parallel. Убедись, что без переприсваивания переменной цикла стадии "слипаются".
- Чем принципиально отличается подход declarative от scripted: что ты описываешь в каждом из них?
- Где разрешено размещать блок pipeline в Jenkinsfile и почему нельзя ставить код выше него?
- Какую роль играет блок script внутри declarative-стадии и почему он закрывает большинство случаев "мне нужен scripted"?
- Назови задачу, которую чисто декларативно выразить нельзя и где scripted оправдан.
Два синтаксиса - это не вкусовщина, а инструмент под задачу. Declarative даёт структуру, читаемость и встроенные гарантии (post, when, options) - и поэтому он выбор по умолчанию в 2026, особенно для новичков. Scripted даёт полную мощь Groovy для редких динамических сценариев. А блок script внутри declarative объединяет лучшее: читаемый каркас плюс точечная гибкость там, где она нужна. Начинай с declarative, переходи на scripted осознанно - когда декларативного синтаксиса действительно перестаёт хватать, а не из привычки писать всё кодом.