Chapter 13Lesson 05~180 minutes

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.

CheckpointCPSSerialization@NonCPSRestart drillEvidence packet

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.

Scope: this lab intentionally creates failing builds and restarts Jenkins. Use only a controller dedicated to this chapter. No real credentials, production repositories, package registries, deployments, or external mutations are allowed.

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-controller and 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:

  1. When a Matcher remains reachable across sleep, predict that the build may fail while persisting CPS state; the external synthetic target remains unchanged.
  2. When a Pipeline is paused at input and the controller restarts cleanly, predict that the same build number/URL and Pipeline continuation return.
  3. Predict that a plain workspace scratch file is agent/workspace state and is not equivalent to persisted CPS program state.
  4. 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

  1. Start the final repaired build and wait until it pauses at input.
  2. Record build URL/number, source SHA, and current stage/step.
  3. On the disposable controller host, verify the exact container name.
  4. Run docker restart jenkins-ch13-controller.
  5. After Jenkins returns, reopen the same build URL and verify the input step is still associated with that same run.
  6. Approve and let the build complete.
  7. Compare before_restart and after_restart: job, build number, and the compact state map 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.dat path/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.

Next lesson

Chapter 14 — Artifacts, stash/unstash, archiveArtifacts, Fingerprints, Test Reports, Coverage, and Build Evidence

Turn ephemeral workspace outputs into deliberate build evidence and understand the distinct roles of stash, archived artifacts, fingerprints, reports, and external artifact systems.

Knowledge check

What three pitfalls must the checkpoint diagnose?

What proves the controller restart resumed rather than reran the Pipeline?

Why is the external marker checked separately?

What security control must remain enabled throughout?

What is the bridge to Chapter 14?

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.