Checkpoint Lab — Groovy CPS, Serialization, @NonCPS, Pipeline State, Restarts, and Common Pipeline Programming Pitfalls
Diagnose and repair three minimal CPS/serialization pitfalls, then restart a disposable Jenkins controller during a safe suspension and prove exactly which Pipeline state resumes and which external state remains independent.
Learning objectives
- Capture a complete baseline of Jenkins, Pipeline plugins, source revision, build identity, agent/workspace, and durability assumptions.
- Reproduce and repair a non-serializable-object failure without destroying the broken run.
- Reproduce and repair two CPS/native boundary mistakes using current Jenkins guidance.
- Restart a disposable controller during a safe input suspension and prove the same run resumes.
- Produce an evidence packet that distinguishes Pipeline program state, workspace state, and external synthetic state.
1. Checkpoint mission
You will use one synthetic SCM-backed Pipeline job to demonstrate three common CPS programming pitfalls, preserve each broken build as evidence, repair the code in new revisions, then pause a final safe build, restart the disposable controller, and prove that the same Pipeline run resumes.
2. Record the baseline before any run
- Jenkins core: expected lab baseline 2.568.3 LTS.
- Java: record actual controller Java (21 or 25 supported by this LTS).
- Pipeline: Groovy, Pipeline: Supporting APIs, Script Security versions.
- Job full name, SCM URL, initial source SHA, sandbox enabled.
-
Agent label
lab-linux, executor count, and workspace root. - Pipeline durability setting in effect for the job.
-
Disposable controller name
jenkins-ch13-controllerand persistent JENKINS_HOME mount.
git init jenkins-ch13-checkpoint
cd jenkins-ch13-checkpoint
git config user.name "Jenkins Chapter 13 Checkpoint"
git config user.email "jenkins-ch13-checkpoint@example.invalid"
printf 'chapter13-checkpoint\n' > README.txt
git add README.txt && git commit -m 'ch13 checkpoint baseline'
git rev-parse HEAD
3. Predictions required before execution
Write predictions.md before running anything:
-
When a Matcher remains reachable across
sleep, predict that the build may fail while persisting CPS state; the external synthetic target remains unchanged. -
When a Pipeline is paused at
inputand the controller restarts cleanly, predict that the same build number/URL and Pipeline continuation return. - Predict that a plain workspace scratch file is agent/workspace state and is not equivalent to persisted CPS program state.
- Predict that the synthetic external marker, stored outside the cleaned workspace for this lab, remains independent of Pipeline continuation state.
4. Pitfall 1 — non-serializable object across suspension
node('lab-linux') {
checkout scm
def matcher = ('candidate build-77' =~ /build-(\d+)/)
assert matcher.find()
echo "candidate=${matcher.group(1)}"
sleep 3
echo "candidate_after=${matcher.group(1)}"
}
Run and preserve the complete failure. Repair in a new commit:
@NonCPS
String candidateId(String text) {
def m = text =~ /build-(\d+)/
return m.find() ? m.group(1) : 'none'
}
def candidate = candidateId('candidate build-77')
sleep 3
echo "candidate=${candidate}"
Expected repair evidence: a new source SHA, new build URL, no
serialization exception, and value 77 after suspension.
5. Pitfall 2 — Pipeline step from @NonCPS
@NonCPS
def brokenAgentWork() {
node('lab-linux') {
echo 'should-not-be-called-from-NonCPS'
}
}
brokenAgentWork()
Preserve the mismatch/anomalous behavior. Repair by making the native helper pure and moving Jenkins steps back into CPS code:
@NonCPS
String normalizeLabel(String value) {
return value.trim().toLowerCase()
}
def label = normalizeLabel(' LAB-LINUX ')
node(label) {
echo "allocated=${env.NODE_NAME}"
}
6. Pitfall 3 — CPS closure passed to native toSorted
def ordered = ['bbb', 'a', 'cccc', 'dd'].toSorted { left, right ->
left.length() <=> right.length()
}
echo "ordered=${ordered}"
Record any CPS mismatch warning or incorrect result. Repair by
moving the complete native sort into @NonCPS:
@NonCPS
List<String> sortNames(List<String> names) {
return names.toSorted { left, right -> left.length() <=> right.length() }
}
def ordered = sortNames(['bbb', 'a', 'cccc', 'dd'])
echo "ordered=${ordered}"
The returned list contains strings and is safe durable state.
7. Final repaired Jenkinsfile with restart checkpoint
@NonCPS
String candidateId(String text) {
def m = text =~ /build-(\d+)/
return m.find() ? m.group(1) : 'none'
}
@NonCPS
List<String> sortNames(List<String> names) {
return names.toSorted { a, b -> a.length() <=> b.length() }
}
def state = [candidate: candidateId('candidate build-77'), names: sortNames(['bbb','a','cccc','dd'])]
echo "before_restart job=${env.JOB_NAME} build=${env.BUILD_NUMBER} state=${state}"
timeout(time: 15, unit: 'MINUTES') {
input message: 'Restart the disposable controller now, then return and continue', ok: 'Continue'
}
echo "after_restart job=${env.JOB_NAME} build=${env.BUILD_NUMBER} state=${state}"
node('lab-linux') {
checkout scm
sh '''
set -eu
mkdir -p evidence
git rev-parse HEAD > evidence/source-sha.txt
printf 'job=%s\nbuild=%s\nnode=%s\nworkspace=%s\n' \
"$JOB_NAME" "$BUILD_NUMBER" "$NODE_NAME" "$WORKSPACE" > evidence/run.txt
'''
writeFile file: 'evidence/cps-state.txt', text: "${state}\n"
archiveArtifacts artifacts: 'evidence/*', fingerprint: true
}
8. Restart drill
-
Start the final repaired build and wait until it pauses at
input. - Record build URL/number, source SHA, and current stage/step.
- On the disposable controller host, verify the exact container name.
- Run
docker restart jenkins-ch13-controller. - After Jenkins returns, reopen the same build URL and verify the input step is still associated with that same run.
- Approve and let the build complete.
-
Compare
before_restartandafter_restart: job, build number, and the compactstatemap must match.
9. Prove external state is independent
Create a synthetic marker outside the cleaned agent workspace before the restart, for example in a dedicated lab volume or host-mounted directory:
mkdir -p /tmp/jenkins-ch13-external
printf 'operation=checkpoint-001\n' > /tmp/jenkins-ch13-external/checkpoint-001.done
sha256sum /tmp/jenkins-ch13-external/checkpoint-001.done
After Jenkins resumes, verify the same marker and digest independently. The marker did not survive because of CPS—it survived because it is external state. This distinction is the point of the exercise.
10. Required evidence packet
-
baseline.txt: Jenkins/Java/Pipeline plugin versions and durability assumption. predictions.md.- Broken build URLs/numbers/source SHAs for all three pitfalls.
- Exact first-failure exception or CPS mismatch warning for each pitfall.
- Repaired source SHAs and successful build URLs.
-
Read-only
program.datpath/size/timestamp metadata for one paused run, if available. - Restart-before and restart-after console lines proving the same build identity/state.
- Agent/node/workspace/source evidence artifact.
- External marker path/digest and explanation that it is not CPS state.
-
limitations.md: timing of serialization errors can vary; internal run files are not a supported repair API.
11. Verification checklist
| Check | Pass condition |
|---|---|
| Serialization diagnosis | Broken Matcher build preserved; repair retains only a String |
| @NonCPS boundary | No Pipeline step called inside repaired @NonCPS methods |
| Closure mismatch | Native sort runs wholly inside @NonCPS and returns strings |
| Restart continuity | Same build number/URL resumes after controller restart |
| State separation | CPS state, workspace state, and external marker are documented separately |
| Security | Sandbox remains enabled; no Script Console workaround or broad approval |
| Evidence | Broken and repaired builds remain distinguishable by source SHA and build URL |
12. Cleanup and rollback
Download the evidence packet, then remove only the synthetic Chapter 13 repository, job, lab-only external marker directory, and disposable controller resources created for this checkpoint. Do not remove shared Pipeline or Script Security plugins, shared agents, credentials, or unrelated jobs.
13. What Chapter 13 adds to the operating model
You can now explain why a Pipeline can survive a controller restart,
why some ordinary Groovy patterns do not survive CPS transformation,
how to keep native helpers inside a safe
@NonCPS boundary, and how to prevent controller
resources from becoming the hidden bottleneck.
Chapter 14 builds on that state model by separating workspace files
from durable evidence using stash/unstash,
archiveArtifacts, fingerprints, test reports, coverage,
and explicit build-evidence retention.
Knowledge check
What three pitfalls must the checkpoint diagnose?
A non-serializable object retained across suspension, a Pipeline step called from @NonCPS/native code, and a CPS closure passed to an incompatible native method such as toSorted.
What proves the controller restart resumed rather than reran the Pipeline?
The same build URL/number and persisted state continue after restart, rather than a new build being created.
Why is the external marker checked separately?
Because external state survives independently of Jenkins CPS persistence; the exercise demonstrates that Pipeline durability is not external-system atomicity.
What security control must remain enabled throughout?
The Groovy sandbox/Script Security boundary; the lab must not disable it or use Script Console as a workaround.
What is the bridge to Chapter 14?
Now that Pipeline program state is understood, the next chapter separates ephemeral workspace files from durable build artifacts, stashes, fingerprints, and reports.
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.