Groovy CPS, Serialization, @NonCPS, Pipeline State, Restarts, and Common Pipeline Programming Pitfalls: Guided Hands-On Workflow and Core Operations
Run a disposable CPS laboratory: prove simple state survives suspension, trigger and diagnose a non-serializable-object failure, repair it with a pure @NonCPS helper, and verify the same Pipeline run resumes after a controller restart.
Learning objectives
- Create a synthetic SCM-backed Pipeline and capture a reproducible CPS/plugin baseline.
- Prove a simple serializable state object survives sleep and controller restart.
- Trigger a controlled NotSerializableException by retaining a regex Matcher across a suspension point.
- Repair the problem by confining the non-serializable object inside a pure @NonCPS helper.
- Verify restart continuity using the same build number, source SHA, and persisted run evidence.
1. Lab preflight and safety boundary
Use a disposable Jenkins controller and a disposable agent labeled
lab-linux. The controller must use persistent
JENKINS_HOME. Do not run this experiment on a shared
production controller because the lesson deliberately restarts
Jenkins and deliberately fails builds.
jenkins-ch13-controller and dedicated to this lab,
adapt the restart command only after proving you are targeting the
disposable instance. Never use an uncontrolled
docker restart $(...) pattern.
# On the disposable controller host
printf 'controller=%s\n' jenkins-ch13-controller
docker inspect --format '{{.Name}} {{.State.Status}}' jenkins-ch13-controller
# Record the image identity and mounted JENKINS_HOME volume before starting.
Inside Jenkins, record core/Java and plugin versions, agent status, and the job’s exact SCM URL and revision.
2. Create a synthetic repository
mkdir jenkins-ch13-cps-lab && cd jenkins-ch13-cps-lab
git init
git config user.name "Jenkins Chapter 13 Lab"
git config user.email "jenkins-ch13@example.invalid"
printf 'chapter13-cps-lab\n' > README.txt
git add README.txt
git commit -m 'ch13: baseline'
git rev-parse HEAD
Create one Pipeline job from SCM, for example
training/ch13-cps-lab. Keep sandbox enabled. Use no
real credentials; a local/public synthetic repository is sufficient.
3. First run: prove simple state survives a suspension
node('lab-linux') {
checkout scm
def state = [operation: 'cps-safe-001', revision: sh(script: 'git rev-parse HEAD', returnStdout: true).trim()]
echo "before=${state}"
sleep 5
echo "after=${state}"
writeFile file: 'evidence-safe.txt', text: "${state}\n"
archiveArtifacts artifacts: 'evidence-safe.txt', fingerprint: true
}
Record the build URL, build number, source SHA, node, workspace, and both log lines. The map contains strings and is intentionally small.
4. Broken run: retain a regex matcher across sleep
Commit this intentionally broken revision:
node('lab-linux') {
checkout scm
def matcher = ('release build-42 ready' =~ /build-(\d+)/)
assert matcher.find()
echo "before-suspend=${matcher.group(1)}"
sleep 3
echo "after-suspend=${matcher.group(1)}"
}
A regex matcher is a runtime object rather than durable Pipeline
data. When Jenkins persists the continuation around the suspension,
the run can fail with a serialization error such as
java.io.NotSerializableException. Preserve the exact
exception, build URL, source SHA, and nearest Pipeline line before
editing the code.
5. Repair: confine native objects inside @NonCPS
@NonCPS
String buildIdFrom(String text) {
def m = text =~ /build-(\d+)/
return m.find() ? m.group(1) : 'none'
}
node('lab-linux') {
checkout scm
def buildId = buildIdFrom('release build-42 ready')
echo "before-suspend=${buildId}"
sleep 3
echo "after-suspend=${buildId}"
}
The matcher exists only while the native helper is executing. The
CPS program retains a plain string. Do not move echo or
sleep into buildIdFrom; Pipeline steps do
not belong inside @NonCPS.
6. Observe persisted run state without editing it
While a build is paused at input or sleep,
inspect only metadata on the disposable controller:
# Example for a simple top-level job; folder jobs have nested jobs/ directories.
find "$JENKINS_HOME/jobs" -path '*/builds/*/program.dat' -type f \
-printf '%TY-%Tm-%TdT%TH:%TM:%TS %s %p\n' 2>/dev/null | tail -n 10
Do not copy a program.dat between builds and do not
modify it. Its value here is evidence that the run has persisted
Pipeline program state.
7. Controlled restart during a safe suspension
Commit a revision with an explicit input gate outside any agent allocation:
def runIdentity = [job: env.JOB_NAME, build: env.BUILD_NUMBER, url: env.BUILD_URL]
echo "before_restart=${runIdentity}"
timeout(time: 15, unit: 'MINUTES') {
input message: 'Chapter 13 restart checkpoint — resume after controller returns', ok: 'Continue'
}
echo "after_restart=${runIdentity}"
node('lab-linux') {
checkout scm
sh 'git rev-parse HEAD > restart-source-sha.txt'
archiveArtifacts artifacts: 'restart-source-sha.txt', fingerprint: true
}
Wait until the build is visibly paused at the input step. Record its build number and URL. Then restart only the disposable controller:
docker restart jenkins-ch13-controller
After Jenkins returns, open the same build URL. Confirm it is still waiting at the same input step. Approve the input and verify the same build number continues; it must not become a new build merely because the controller restarted.
8. Groovy versus agent process: prove the boundary
def labels = ['alpha', 'beta', 'gamma']
def compact = labels.collect { it.toUpperCase() }.join(',') // controller-side Groovy
echo "labels=${compact}"
node('lab-linux') {
sh 'printf "agent_pid=%s\\n" "$$" > process-evidence.txt' // agent process
archiveArtifacts artifacts: 'process-evidence.txt', fingerprint: true
}
The agent executes the shell process. The Groovy collection operation is controller-side even though both appear in one Jenkinsfile.
9. Challenge: choose the right layer
You need to parse a 200 MB JSON test report and retain only three totals. Which layer should do it?
Expected design: run a parser/tool on an agent
against the file, write a tiny summary, then read only that summary
into Pipeline state. Do not load the 200 MB report into a Groovy map
on the controller, and do not mark a giant parser
@NonCPS merely to make it faster.
10. Cleanup
Download the evidence, stop/delete only the Chapter 13 job/repository and disposable controller resources you created, and leave shared plugins/agents untouched. Preserve the broken build record until the checkpoint review is complete.
Knowledge check
What is the defect in retaining a regex Matcher across
sleep?
The Matcher is not durable Pipeline data; if it remains reachable at a suspension point, serialization can fail.
Why does the repaired helper return a String?
The non-serializable matcher remains local to the @NonCPS method while the CPS program receives a small serializable value.
What proves restart continuity?
The same build URL/number and source revision resume after the controller restart; a new build is not created.
Should you edit program.dat after a serialization
failure?
No. Preserve evidence and fix Pipeline code or restore supported Jenkins state; program.dat is internal engine state.
Where should a large report transformation run?
On an agent or external tool, with only a compact summary returned to Pipeline orchestration state.
Official references and version notes
- Jenkins LTS changelog — current Jenkins LTS and tested Java configurations.
-
Pipeline CPS Method Mismatches
— CPS transformation boundaries,
@NonCPS, constructors, closures, and mismatch diagnostics. -
Pipeline: Groovy plugin
— Pipeline execution engine, persistence model,
program.dat, rehydration, and@NonCPScontract. -
Pipeline Best Practices
— controller/agent boundaries, serializable state, and safe use of
@NonCPS. - Scaling Pipelines — durability modes, persistence trade-offs, controller CPU/memory, and restart implications.
- Script Security plugin — Groovy sandbox and script-approval security boundary.
- Jenkins Pipeline handbook — durable, pausable Pipeline fundamentals.
Rechecked on 2026-09-16. Examples assume Jenkins 2.568.3 LTS (tested with Java 21 and 25), Pipeline: Groovy 4380.v6eb_8378b_9647, Pipeline: Supporting APIs 1015.v785e5a_b_b_8b_22, and Script Security 1422.v06869826dd9b_. The mandatory path is local/disposable, uses synthetic state and fake identities, and requires no commercial service. Plugin releases are independent of Jenkins core, so record the versions actually installed on your controller before applying these lessons.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.