Три шага под три платформы: sh, bat, powershell
Выбор шага зависит от того, на каком агенте крутится этап. На Linux и macOS используют sh - он запускает команду через POSIX-оболочку. На Windows для классической командной строки cmd есть bat, а для PowerShell - шаг powershell (или pwsh для кросс-платформенного PowerShell Core). Логика jenkins shell везде одинаковая: ты передаёшь строку со скриптом, Jenkins пишет её во временный файл на агенте, запускает и следит за кодом возврата.
Простейший declarative-конвейер, сборка 98, выглядит так:
Код: Выделить всё
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'echo "Собираю проект на агенте $NODE_NAME"'
sh 'make build'
}
}
}
}
Код: Выделить всё
steps {
bat 'echo Building on %COMPUTERNAME%'
powershell 'Get-ChildItem -Path . -Filter *.dll | Measure-Object'
}

Коды возврата: почему jenkins sh падает по умолчанию (и как это поведение менять)
Ключевое правило: шаг оболочки считается успешным только если процесс вернул нулевой код. Любой ненулевой exit code - и шаг бросает исключение, этап краснеет, конвейер останавливается. Это правильное поведение по умолчанию: если make build вернул 2, продолжать сборку бессмысленно.
Но иногда ненулевой код - это нормально. Например, grep возвращает 1, когда ничего не нашёл, а ты как раз хочешь проверить "есть ли в логе слово ERROR". Тогда падать не надо. Для таких случаев у шага есть параметр returnStatus:
Код: Выделить всё
script {
def code = sh(script: 'grep -q ERROR build.log', returnStatus: true)
if (code == 0) {
echo 'Нашёл ERROR в логе сборки'
} else {
echo 'Лог чистый, идём дальше'
}
}
Чтобы забрать сам вывод команды в переменную Groovy, нужен returnStdout. Это типичный паттерн "получить git-хеш / версию / список файлов":
Код: Выделить всё
script {
def commit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
def branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
echo "Собираю ${branch} на коммите ${commit}"
}
Многострочные скрипты, set -e и кавычки
Однострочниками реальная jenkins сборка не ограничивается. Многострочный скрипт оформляют тройными кавычками:
Код: Выделить всё
sh '''
set -euo pipefail
echo "Готовлю окружение"
./configure --prefix=/opt/app
make -j$(nproc)
make install DESTDIR=$WORKSPACE/out
'''
Теперь про кавычки и интерполяцию - это главные грабли всего урока. В Jenkinsfile сосуществуют два мира подстановки переменных, и их легко перепутать:
- Groovy-интерполяция ${ИМЯ} работает только внутри двойных кавычек ". Jenkins подставит значение ДО того, как скрипт уйдёт в оболочку.
- Shell-переменная $ИМЯ раскрывается уже самой оболочкой на агенте, в момент выполнения.
Код: Выделить всё
script {
def appVersion = '2.4.1'
// Groovy подставит версию ещё до запуска оболочки:
sh "docker build -t myapp:${appVersion} ."
// А тут $BUILD_NUMBER раскроет сама оболочка из переменной окружения:
sh 'echo "Это сборка номер $BUILD_NUMBER"'
}
Самая коварная ошибка - использовать двойные кавычки там, где переменную должна разворачивать оболочка, а не Groovy. Допустим, ты пишешь:
Код: Выделить всё
// ОПАСНО: Groovy первым доберётся до ${HOME}
sh "cp build/app.tar.gz ${HOME}/releases/"
Код: Выделить всё
// Безопасно: $HOME раскроет сама оболочка на агенте
sh 'cp build/app.tar.gz $HOME/releases/'
Когда sh, а когда специализированный шаг
Соблазн сделать всё через jenkins shell велик, но не всегда оправдан. Голый sh 'git clone ...' проигрывает шагу checkout / git, потому что не умеет нормально работать с учётками, ретраями и сабмодулями. sh 'docker build' можно заменить на методы Docker Pipeline. Архивацию артефактов делает archiveArtifacts, а не sh 'cp' в случайную папку. Плюс специализированные шаги переносимы между Linux и Windows, а sh - нет.
Эмпирика 2026 года: если для задачи есть зрелый шаг плагина - бери его, получишь обработку ошибок, логи и кросс-платформенность даром. sh оставляй для того, чего плагин не покрывает: твои билд-скрипты, утилиты, разовая логика. И помни про эфемерных Kubernetes-агентов - в pod-шаблоне у каждого контейнера своя оболочка, и sh внутри блока container('maven') пойдёт именно в тот контейнер. Это, кстати, выгодно отличает Jenkins от линейных шагов run в GitLab CI и GitHub Actions: тут ты явно маршрутизируешь команду в нужное окружение.
Мини-лаба
Собери конвейер и прогони руками:
- Сделай stage, где sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() кладёт хеш в переменную, и выведи её через echo.
- Добавь шаг с returnStatus: true на команду, которая заведомо вернёт ненулевой код (например exit 3), и убедись, что конвейер НЕ упал, а ты разобрал код в if.
- Напиши многострочный sh с set -euo pipefail и сломай одну команду внутри - посмотри, как падает этап.
- Сделай два echo: один с Groovy-переменной в двойных кавычках ${ver}, другой с $BUILD_NUMBER в одинарных. Поменяй кавычки местами и поймай разницу в выводе.
- Чем отличается поведение sh с returnStatus: true от обычного sh при ненулевом коде возврата?
- Зачем после sh(..., returnStdout: true) почти всегда вызывают .trim()?
- В каких кавычках писать переменную, которую должна развернуть оболочка агента, а в каких - ту, что известна Groovy, и почему это вопрос безопасности?
- Почему checkout или archiveArtifacts часто лучше, чем sh 'git clone' и sh 'cp'?
Шаги sh, bat и powershell - это мост между конвейером и реальной работой на агенте. Запомни три якоря: ненулевой код по умолчанию валит шаг (и это хорошо), returnStatus и returnStdout дают тебе код и вывод соответственно, а кавычки решают, кто разворачивает переменную - Groovy или оболочка. Освой это - и большая часть загадочных падений Jenkinsfile перестанет быть загадкой.