Типичные ошибки конвейера: сериализация и неутверждённый код

Рейтинг: 67.6% · 8 голосов
Подробный курс по Jenkins и CI/CD: установка и настройка, плагины и Configuration as Code, конвейеры (pipeline) и Jenkinsfile, declarative и scripted, агенты, Docker и Kubernetes, общие библиотеки на Groovy, безопасность и credentials, интеграции (SonarQube, артефакты), траблшутинг. Актуально на 2026.
Ответить
Аватара пользователя
Andrey_CICD
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Типичные ошибки конвейера: сериализация и неутверждённый код

Сообщение Andrey_CICD »

Оглавление курса (47)
  1. Что такое Jenkins и зачем нужен CI/CD
  2. Jenkins в 2026: версии, экосистема и сравнение с GitLab CI и GitHub Actions
  3. Архитектура Jenkins: контроллер, агенты, узлы и исполнители
  4. Установка Jenkins: пакеты, Docker и Kubernetes
  5. Первичная настройка Jenkins: мастер, плагины, пользователи
  6. Плагины Jenkins: установка, обновление и ключевой набор
  7. Configuration as Code (JCasC): настройка Jenkins декларативно
  8. Конвейеры Jenkins и Jenkinsfile: pipeline as code
  9. Scripted против Declarative: два синтаксиса конвейера
  10. Структура конвейера: node, stage, steps и первый pipeline
  11. Генератор сниппетов, запуск конвейера и Replay
  12. Типы заданий Jenkins: Freestyle, Pipeline, Multibranch и Organization
  13. Декларативный конвейер: структура pipeline, agent, stages
  14. agent, environment и переменные в конвейере
  15. tools, options, parameters и triggers в декларативном конвейере
  16. stages, parallel и matrix: организация этапов
  17. Условное выполнение when и блок post
  18. Триггеры сборки: webhook от GitHub, SCM polling и расписание
  19. Параметры сборки и пользовательский ввод
  20. Управление потоком: timeout, retry, sleep и waitUntil
  21. Параллелизм и блокировки: parallel, lock, milestone
  22. Постобработка и уведомления о результате сборки
  23. Groovy для Jenkins: основы скриптового конвейера
  24. Скриптовый конвейер: логика, циклы и функции
  25. Общие библиотеки Jenkins: Shared Libraries
  26. Подключение и загрузка общих библиотек
  27. Шаги оболочки: sh, bat, powershell и коды возврата
  28. Переменные среды, withEnv и рабочие пространства
  29. Работа с файлами, артефактами и отпечатками
  30. Сборка проектов в конвейере: Maven, Gradle, npm
  31. Качество кода: SonarQube и покрытие тестами
  32. Управление артефактами: публикация в Nexus и Artifactory
  33. Docker в Jenkins: образы как агенты сборки
  34. Сборка и публикация Docker-образов из конвейера
  35. Jenkins в Kubernetes: динамические pod-агенты
  36. Развёртывание в Kubernetes из конвейера
  37. Безопасность Jenkins: аутентификация и матрица прав
  38. Учётные данные Jenkins: credentials в конвейере
  39. Безопасность сценариев: Groovy sandbox и Vault
  40. Уведомления и отчёты: email, Slack, Telegram, HTML
  41. Blue Ocean и интерфейс Jenkins в 2026
  42. Автоматизация Jenkins: CLI, REST API и script console
  43. Поиск и устранение неисправностей: читаем логи конвейера
  44. Типичные ошибки конвейера: сериализация и неутверждённый код (вы здесь)
  45. Эксплуатация Jenkins: бэкап, обновление, масштабирование
  46. Конвертация и миграция: от Freestyle к Jenkinsfile
  47. Итог: сквозной CI/CD конвейер и Jenkins против альтернатив в 2026
Конвейер собирался зелёным десять раз подряд, а на одиннадцатый упал с непонятным стектрейсом про сериализацию - и ты сидишь, тупо смотришь в лог, потому что код-то не менялся. Знакомо? В Jenkins есть отдельный класс граблей, которых нет ни в GitLab CI, ни в GitHub Actions. Это не баги твоего приложения и не флапающие тесты. Это особенности самого движка пайплайна: каждый Jenkinsfile прогоняется через Groovy CPS, а любой неизвестный движку вызов отсекает script approval. Типичная jenkins error из этой серии выглядит страшно, но лечится по понятной схеме. В этом уроке разберём, почему возникает jenkins notserializableexception, что такое неутверждённый код и script approval, как ловить jenkins exception в пайплайне и по какому чек-листу разбирать падение, чтобы не гадать.

Почему пайплайн вообще ломается: CPS под капотом

Сначала механика, без неё лечить бесполезно. Declarative и scripted pipeline исполняются не как обычный Groovy-скрипт. Jenkins переписывает твой код в стиле CPS (Continuation Passing Style) - продолжение-передающий стиль. Зачем? Чтобы пайплайн умел пережить рестарт контроллера. Сборка может идти часами, ждать на input, висеть на агенте - и если контроллер перезапустят, выполнение должно продолжиться с того же шага. Для этого движок после каждого шага сохраняет на диск всё состояние программы: значения переменных, стек вызовов, позицию.

А чтобы состояние можно было сохранить на диск, всё, что лежит в переменных между шагами, обязано быть сериализуемым - то есть реализовывать java.io.Serializable. Строки, числа, списки, мапы - сериализуются нормально. А вот объекты вроде java.util.regex.Matcher, потоки ввода-вывода, JsonSlurper-результаты в некоторых версиях, ссылки на Jenkins.instance - нет. И когда такой объект остаётся живым через границу шага, ты получаешь jenkins ошибку сериализации.

Изображение

NotSerializableException: jenkins ошибка сериализации и как её лечить

Классический пример, который ломается. Распарсили строку регуляркой, а Matcher оставили в переменной до следующего шага:

Код: Выделить всё

// ПЛОХО: Matcher несериализуем и переживает границу шага
node {
    def m = (env.BRANCH_NAME =~ /release-(\d+)/)
    if (m) {
        echo "Release: ${m.group(1)}"
    }
    sh 'sleep 1'   // тут движок пытается сохранить состояние и падает
    echo "after"
}
В логе вылезет java.io.NotSerializableException: java.util.regex.Matcher. Лечение - не держать несериализуемый объект живым через шаг. Достань нужное сразу и обнули ссылку:

Код: Выделить всё

node {
    def ver = null
    def m = (env.BRANCH_NAME =~ /release-(\d+)/)
    if (m) { ver = m.group(1) }
    m = null              // ключевая строчка: убрали Matcher
    sh 'sleep 1'
    echo "Release: ${ver}"
}
Второй инструмент - аннотация @NonCPS. Она говорит движку: этот метод выполни как обычный Groovy, без CPS-трансформации, и не пытайся сохранять его внутреннее состояние. Идеально для работы с несериализуемыми объектами и для тяжёлой обработки данных:

Код: Выделить всё

@NonCPS
def parseVersion(String branch) {
    def m = (branch =~ /release-(\d+)/)
    return m ? m[0][1] : 'snapshot'   // Matcher живёт и умирает внутри метода
}

pipeline {
    agent any
    stages {
        stage('Detect') {
            steps {
                script {
                    env.VERSION = parseVersion(env.BRANCH_NAME)
                    echo "Version: ${env.VERSION}"
                }
            }
        }
    }
}
Важная ловушка @NonCPS, на которой спотыкаются почти все. Внутри @NonCPS-метода НЕЛЬЗЯ вызывать pipeline-шаги: sh, echo, checkout, build и любые steps. CPS-код может звать non-CPS-код, но обратно - нет, интерпретатор не сможет продолжить выполнение. Поэтому @NonCPS - только для чистых вычислений: распарсить, отфильтровать, посчитать, вернуть простой результат. И ещё: метод должен возвращать сериализуемое значение. Если вернёшь из него тот же Matcher - проблему не решишь, просто перенесёшь.

Третье правило - не злоупотребляй. Если обмазать @NonCPS половину Jenkinsfile, потеряешь главную фишку - устойчивость к рестарту, потому что non-CPS-куски не чекпойнтятся. Держи pipeline-логику простой, а сложную обработку выноси в shared library с @NonCPS.

Script approval и RejectedAccessException: неутверждённый код

Второй большой класс - jenkins script approval. Пайплайны по умолчанию крутятся в Groovy Sandbox: песочнице, которая блокирует потенциально опасные вызовы, чтобы автор Jenkinsfile не смог через CI получить контроль над контроллером. Когда твой код зовёт метод, которого нет в белом списке песочницы, прилетает RejectedAccessException, и сборка падает с текстом вида "Scripts not permitted to use method ...".

Типичный пример - кто-то полез напрямую в API Jenkins:

Код: Выделить всё

// Вызовет RejectedAccessException в песочнице
script {
    def n = Jenkins.instance.getItemByFullName('deploy').lastBuild.number
    echo "Last build: ${n}"
}
Что делать. Сначала - правильный путь: открой Manage Jenkins -> In-process Script Approval. После падения там появится запись с конкретной сигнатурой метода и кнопкой Approve. Администратор смотрит, безопасно ли, и одобряет. Часть совсем опасных методов approve недоступен - и это не баг, такие вещи через песочницу пускать нельзя в принципе.

Но в 2026 году дёргать приватный API через рефлексию - это запах. Почти всегда есть штатный шаг. Номер последней сборки берётся через currentBuild и шаг build, а не через Jenkins.instance:

Код: Выделить всё

script {
    def b = build job: 'deploy', wait: true
    echo "Triggered build #${b.number}"
}
Историческая ловушка: если поймать RejectedAccessException в try/catch и не пробросить дальше, в старых версиях плагина Pipeline: Groovy (до 2.67) запись в Script Approval вообще не появлялась - и ты не понимал, что одобрять. Баг JENKINS-34973 давно починен, но если сидишь на древнем Jenkins, это ещё один аргумент обновиться. Актуальная ветка LTS на середину 2026 - линейка 2.541.x и новее, и она уже требует Java 17, а свежие weekly - Java 21. Старьё тянуть смысла нет: на нём и approval-баги, и дыры безопасности.

MissingMethodException и "шаг вернул не то"

Ещё две частые jenkins exception, которые путают новичков.

MissingMethodException обычно означает CPS method mismatch: ты передал замыкание (closure) в метод вроде .each, .collect, .findAll внутри CPS-кода, и движок не смог его правильно трансформировать. Симптом - ошибка про отсутствующий метод там, где метод очевидно есть. Лекарство простое: в CPS-коде вместо .each используй обычный for, либо вынеси итерацию с замыканием в @NonCPS-метод:

Код: Выделить всё

// Может дать CPS-несовместимость с замыканием
// servers.each { deploy(it) }

// Надёжно в CPS:
for (s in servers) {
    deploy(s)
}
"Шаг вернул не то" - вторая. Многие забывают, что sh по умолчанию не возвращает stdout, а только код возврата (и то лишь с returnStatus). Чтобы получить вывод команды в переменную, нужны явные параметры:

Код: Выделить всё

script {
    // returnStdout - забрать вывод; trim() обязателен, иначе хвостовой \n
    def commit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
    // returnStatus - не падать на ненулевом коде, а получить число
    def changed = sh(script: 'git diff --quiet', returnStatus: true)
    echo "commit=${commit} changed=${changed}"
}
Обработка исключений: catchError против try/catch

Когда пайплайн обязан что-то доделать даже после ошибки (собрать артефакты, отправить отчёт), нужна аккуратная обработка jenkins exception. Два инструмента.

Шаг catchError - декларативный способ поймать падение и при этом задать, каким станет статус сборки и статус стейджа. Очень удобно, когда хочешь пометить стейдж как FAILURE/UNSTABLE, но не ронять весь джоб:

Код: Выделить всё

stage('Tests') {
    steps {
        catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
            sh './run-flaky-tests.sh'
        }
        echo 'Этот шаг выполнится даже если тесты упали'
    }
}
Параметры buildResult и stageResult появились в Pipeline: Basic Steps 2.16. Учти ограничение: поднять статус обратно к SUCCESS из UNSTABLE или ABORTED нельзя - только зафиксировать или ухудшить.

Обычный try/catch/finally в блоке script нужен, когда логика реакции сложнее, чем "пометить и продолжить" - например, разное поведение на разные типы ошибок плюс гарантированная очистка:

Код: Выделить всё

script {
    try {
        sh './deploy.sh'
    } catch (org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException e) {
        error "Неутверждённый код, одобри метод в Script Approval: ${e.message}"
    } catch (e) {
        echo "Деплой упал: ${e}"
        currentBuild.result = 'FAILURE'
        throw e               // пробрасываем, иначе сборка останется зелёной
    } finally {
        sh 'kill $(cat app.pid) || true'   // чистим в любом случае
    }
}
Главное правило try/catch в пайплайне: если поймал и не пробросил - сборка будет зелёной, даже когда всё развалилось. Это самый частый способ молча скрыть провал. Хочешь продолжить, но честно пометить - явно ставь currentBuild.result.

И про jenkins error 503. Если шаг httpRequest или curl к внешнему сервису возвращает 503 Service Unavailable - это не баг пайплайна, а недоступность зависимости. Не лови это в catchError как "всё нормально". Лучше добавь retry с паузой - времянку переживёт, а постоянную проблему честно покажет:

Код: Выделить всё

retry(3) {
    sleep time: 10, unit: 'SECONDS'
    sh 'curl -fsS https://api.internal/health'   // -f роняет шаг на 5xx
}
Отладка shared library и чек-лист разбора падения

Shared library добавляет свой слой граблей: тот же CPS, та же песочница, плюс кэширование версии библиотеки. Что помогает. Подключай конкретную ветку или тег через @Library('mylib@feature-x'), а не плавающий default - иначе ловишь старый закэшированный код и не понимаешь, почему правка не приехала. Тяжёлую несериализуемую логику в src/ помечай @NonCPS. И помни: vars/*.groovy исполняются в CPS, а классы в src/ - тоже под CPS, если их зовут из пайплайна, так что Matcher и потоки прячь в @NonCPS-методы.

Чек-лист, когда конвейер упал с непонятным jenkins exception:
  • Прочитай ПЕРВУЮ строку стектрейса, а не последнюю - там тип и причина.
  • NotSerializableException - найди, какой объект назван в сообщении, и убери его из переменной через границу шага (обнули или заверни в @NonCPS).
  • RejectedAccessException / "Scripts not permitted" - иди в Manage Jenkins -> In-process Script Approval; но сперва подумай, нет ли штатного шага вместо API.
  • MissingMethodException - подозревай замыкание в CPS; перепиши .each на for или вынеси в @NonCPS.
  • Шаг "вернул не то" - проверь returnStdout/returnStatus и trim() у sh.
  • Замолчавший провал - проверь, не съел ли try/catch исключение без проброса.
  • Сравни с прошлой зелёной сборкой: версия Jenkins, плагинов, ветка shared library - менялось ли что-то вне Jenkinsfile.
Мини-лаба

Руками, на тестовом джобе:
  • Воспроизведи NotSerializableException: оставь Matcher в переменной перед sh. Поймай ошибку, потом вылечи через @NonCPS-метод.
  • Спровоцируй RejectedAccessException вызовом Jenkins.instance в песочнице. Найди запись в In-process Script Approval, одобри. Затем перепиши на штатный шаг build.
  • Сделай стейдж с заведомо падающим скриптом и оберни его в catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE'). Убедись, что джоб стал жёлтым, а не красным.
  • Напиши try/catch с finally, который чистит временный файл, и проверь, что без throw сборка остаётся зелёной даже при ошибке.
Контрольные вопросы
  • Почему переменные между шагами пайплайна обязаны быть сериализуемыми и при чём тут CPS?
  • Что делает @NonCPS и почему внутри такого метода нельзя вызывать sh или echo?
  • Чем RejectedAccessException отличается от NotSerializableException по причине и по способу лечения?
  • В чём разница между catchError и try/catch, и как нечаянно "спрятать" провал сборки?
Итог

Большинство загадочных падений Jenkins - это не магия, а два механизма движка: CPS-сериализация и песочница со script approval. Запомни три опоры. Не держи несериализуемые объекты через границу шага, а чистые вычисления выноси в @NonCPS. Неутверждённый код одобряй через In-process Script Approval, но сначала ищи штатный шаг вместо рефлексии в API. Исключения лови осознанно: catchError - чтобы пометить статус, try/catch с throw - чтобы не проглотить провал. С этим чек-листом разбор любого jenkins error превращается из гадания в пять минут по пунктам.
👍5 ❤️3 🔥3 😄 🤔
Аватара пользователя
janert
Сообщения: 1
Зарегистрирован: 14 май 2026, 02:52

Re: Типичные ошибки конвейера: сериализация и неутверждённый код

Сообщение janert »

Спасибо, наконец дошло почему Matcher в переменной всё ломал. Раньше тупо ставил @NonCPS на всё подряд и удивлялся что после рестарта пайплайн начинался заново.
👍2 ❤️2 🔥 😄 🤔
Аватара пользователя
antonio1
Сообщения: 1
Зарегистрирован: 07 июн 2026, 05:04

Re: Типичные ошибки конвейера: сериализация и неутверждённый код

Сообщение antonio1 »

Ловушка с try/catch без throw - это прям про меня. Полгода сборки были зелёные, а деплой по факту падал, никто не замечал пока прод не лёг. Теперь currentBuild.result везде.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Поиск и устранение неисправностей: читаем логи конвейера
Следующая глава →
Эксплуатация Jenkins: бэкап, обновление, масштабирование

Все главы курса «Jenkins: конвейеры CI/CD от основ до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: groovy в jenkins scripted pipeline как написатьпараметры и параллельные стейджи в jenkins pipelineструктура declarative pipeline в jenkins: stages, agent, steps

Вернуться в «Jenkins: конвейеры CI/CD от основ до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя