Chapter 11Lesson 05~165 minutes

Checkpoint Lab — Scripted Pipeline, Groovy Control Flow, node Blocks, Dynamic Logic, and When to Use Scripted Pipelines

Implement equivalent Declarative and Scripted workflows, demonstrate a justified dynamic Scripted graph, inject a serialization error, repair it with a new source revision, and preserve controller/agent/build evidence.

Checkpoint labDeclarative comparisonDynamic stagesSerializationEvidenceRecovery

Learning objectives

  • Implement the same synthetic workflow in Declarative and Scripted forms and compare their evidence.
  • Justify the part of the workflow that benefits from Scripted dynamic orchestration.
  • Inject and diagnose a non-serializable Pipeline-state failure without hiding the original cause.
  • Repair the Pipeline by reducing temporary state to serializable values.
  • Produce a complete evidence packet tied to exact source/build/node/artifact identities.

1. Checkpoint scenario

You own a synthetic repository whose trusted local manifest selects a subset of three allowed checks: lint, unit, and package. First implement a fixed Declarative Pipeline. Then implement a Scripted Pipeline that creates stages from the validated manifest. Inject a serialization error into the Scripted version, preserve the failure, repair it in a new commit, and prove the repaired evidence.

What must be justified: Declarative could embed dynamic Groovy inside script {}. In this lab, “Scripted-only value” means the workflow's primary requirement is a dynamic stage graph; choosing a fully Scripted Jenkinsfile keeps that dynamic control flow explicit rather than hiding most of the Pipeline inside a Declarative escape hatch.

2. Baseline and preflight

Layer Lab baseline Evidence
Controller Jenkins 2.568.3 LTS or your recorded current LTS; Java 21/25 supported Core and JVM version
Pipeline engine Pipeline: Groovy 4380.v6eb_8378b_9647 Plugin inventory
Security Script Security 1415.v9a_f9b_3a_c253d; sandbox kept enabled Plugin inventory/no new approvals
Node/workspace lab-linux disposable agent Node label/executor/workspace
Source Synthetic repository only Immutable Git SHA per revision
Credentials None in mandatory path State this explicitly
External side effect None; local archived files only Artifact digest/fingerprint

3. Write predictions before execution

Create predictions.md with at least these predictions:

  1. The fixed Declarative version will always display the same three stage names.
  2. The Scripted version will create stages only for manifest entries that pass the allowlist and cardinality checks.
  3. All sh commands will run inside node('lab-linux'); planning logic will not require an executor.
  4. The intentionally retained regex Matcher across sleep will fail with serialization/CPS evidence before the final artifact stage.
  5. The repaired revision will convert the temporary matcher result to a serializable String before suspension and archive evidence tied to the new SHA.

4. Create the synthetic repository

mkdir jenkins-ch11-checkpoint && cd jenkins-ch11-checkpoint
git init
git config user.name "Jenkins Chapter 11 Checkpoint"
git config user.email "jenkins-ch11-checkpoint@example.invalid"
cat > lab-task <<'EOF'
#!/usr/bin/env sh
set -eu
mkdir -p out
case "$1" in
  lint)    printf 'lint=ok\n' > out/lint.txt ;;
  unit)    printf 'unit=ok\n' > out/unit.txt ;;
  package) printf 'package=ok\n' > out/package.txt ;;
  *)       printf 'unsupported task\n' >&2; exit 2 ;;
esac
EOF
chmod +x lab-task
printf 'lint\nunit\npackage\n' > tasks.txt
git add .
git commit -m 'ch11: checkpoint project'
BASE_SHA=$(git rev-parse HEAD)
printf 'base_sha=%s\n' "$BASE_SHA"

5. Implement the fixed Declarative comparison

Save this as Jenkinsfile.declarative:

pipeline {
  agent { label 'lab-linux' }
  stages {
    stage('Checkout') {
      steps { checkout scm }
    }
    stage('Lint') {
      steps { sh './lab-task lint' }
    }
    stage('Unit') {
      steps { sh './lab-task unit' }
    }
    stage('Package') {
      steps { sh './lab-task package' }
    }
  }
  post {
    always {
      archiveArtifacts artifacts: 'out/*', allowEmptyArchive: true, fingerprint: true
    }
  }
}

This is clearer if all three tasks are always required. Run it as an SCM-backed Pipeline configured with Script Path Jenkinsfile.declarative. Record the source SHA, build number/URL, node/workspace, stages, and artifact digests.

6. Implement the dynamic Scripted version

Save this as Jenkinsfile:

def allowed = ['lint', 'unit', 'package'] as Set

def requested = []
readFile('tasks.txt').readLines().each { raw ->
  def taskName = raw.trim()
  if (taskName) {
    if (!allowed.contains(taskName)) {
      error "unsupported task: ${taskName}"
    }
    requested << taskName
  }
}
requested = requested.unique()
if (requested.size() > 3) {
  error "task count ${requested.size()} exceeds limit 3"
}

stage('Plan') {
  echo "selected=${requested.join(',')}"
}

node('lab-linux') {
  stage('Checkout') {
    checkout scm
    sh '''
      set -eu
      mkdir -p evidence
      {
        printf 'job=%s\n' "$JOB_NAME"
        printf 'build=%s\n' "$BUILD_NUMBER"
        printf 'build_url=%s\n' "$BUILD_URL"
        printf 'node=%s\n' "$NODE_NAME"
        printf 'workspace=%s\n' "$WORKSPACE"
        printf 'source_sha=%s\n' "$(git rev-parse HEAD)"
      } > evidence/context.txt
    '''
  }

  requested.each { taskName ->
    stage("Task: ${taskName}") {
      sh "./lab-task '${taskName}'"
    }
  }

  stage('Evidence') {
    sh 'sha256sum out/* > evidence/artifacts.sha256'
    archiveArtifacts artifacts: 'evidence/*,out/*', fingerprint: true
  }
}
Important correction: readFile requires a workspace, so the manifest must be read after entering a node, or the manifest must come from previously available controller-safe/SCM metadata. For this checkpoint, keep the manifest read inside the node as shown in the corrected version below. This intentionally demonstrates that dynamic planning still has context requirements.
def allowed = ['lint', 'unit', 'package'] as Set

def requested = []

node('lab-linux') {
  stage('Checkout') {
    checkout scm
  }

  stage('Plan') {
    readFile('tasks.txt').readLines().each { raw ->
      def taskName = raw.trim()
      if (taskName) {
        if (!allowed.contains(taskName)) {
          error "unsupported task: ${taskName}"
        }
        requested << taskName
      }
    }
    requested = requested.unique()
    if (requested.size() > 3) {
      error "task count ${requested.size()} exceeds limit 3"
    }
    echo "selected=${requested.join(',')}"
  }

  requested.each { taskName ->
    stage("Task: ${taskName}") {
      sh "./lab-task '${taskName}'"
    }
  }

  stage('Evidence') {
    sh '''
      set -eu
      mkdir -p evidence
      {
        printf 'job=%s\n' "$JOB_NAME"
        printf 'build=%s\n' "$BUILD_NUMBER"
        printf 'node=%s\n' "$NODE_NAME"
        printf 'workspace=%s\n' "$WORKSPACE"
        printf 'source_sha=%s\n' "$(git rev-parse HEAD)"
      } > evidence/context.txt
      sha256sum out/* > evidence/artifacts.sha256
    '''
    archiveArtifacts artifacts: 'evidence/*,out/*', fingerprint: true
  }
}

Run this good Scripted revision first and preserve its evidence. The dynamic value is visible: stage count follows the validated manifest, bounded to three allowed tasks.

7. Inject a serialization failure

In a new commit, add this between Plan and the task loop:

def releaseMatcher = ('release-42' =~ /release-(\d+)/)
sleep 2
echo "release=${releaseMatcher[0][1]}"

Commit it and record the broken SHA. Run the build. Preserve the first serialization/CPS stack trace, build URL, stage state, and proof that later evidence did not complete. Do not replay yet.

8. Repair the state model

Replace the retained matcher with a serializable string before the suspension:

def releaseId = ('release-42' =~ /release-(\d+)/)[0][1].toString()
sleep 2
echo "release=${releaseId}"

Commit the repair as a new revision. Run again. Verify that the build reaches all dynamic task stages and the Evidence stage.

9. Verify every prediction independently

Prediction Evidence Pass condition
Declarative graph fixed Stage visualization/build record Lint, Unit, Package always visible
Scripted graph validated/dynamic Plan log + stage names + tasks.txt Only allowed manifest entries become stages
Agent boundary correct Node/executor/workspace logs Shell commands run on lab-linux
Broken revision preserves failure Exact SHA + stack trace Serialization failure retained before repair
Repaired revision durable New SHA + completed stages/artifacts Serializable releaseId survives sleep

10. Evidence packet

  • baseline.txt: Jenkins core/Java and Pipeline: Groovy/Script Security/Nodes plugin versions.
  • predictions.md.
  • Job full names, build numbers/URLs, causes, and exact source/Jenkinsfile SHAs for Declarative, good Scripted, broken, and repaired runs.
  • Queue/node/label/executor/workspace evidence.
  • Dynamic selected-task list and stage names.
  • Broken serialization stack trace.
  • Archived evidence/context.txt, evidence/artifacts.sha256, out/*, and fingerprints.
  • Statement that no real credential, sandbox approval, cloud service, or production target was used.
  • limitations.md: dynamic cardinality capped at three; no parallelism; no external side effect.

11. Cleanup and rollback

Download the evidence packet. Delete only the Chapter 11 checkpoint jobs/repository branches created for the lab. Do not delete shared agents, controller plugins, or unrelated Pipeline jobs. If you changed any job configuration while comparing Script Paths, restore the documented intended path.

12. What Chapter 11 adds

You can now use Scripted Pipeline intentionally rather than reflexively: dynamic Groovy controls orchestration, node controls agent/workspace allocation, Pipeline steps preserve Jenkins semantics, substantial work remains on agents, CPS state stays serializable, and Script Security remains a deliberate trust boundary.

Chapter 12 builds on this by controlling failure itself: retry, timeout, catchError, input, waitUntil, aborts, and side-effect-safe resilience.

Next lesson

Chapter 12 — Pipeline Steps, Error Handling, retry, timeout, catchError, input, waitUntil, and Resilient Flow Control

Use Pipeline control steps to handle failure and waiting without hiding errors, holding scarce agents, or repeating unsafe side effects.

Knowledge check

What is the justified Scripted value in the checkpoint?

Why must readFile be inside a workspace context?

What evidence should be preserved from the broken serialization run?

Why does converting the regex result to String fix the checkpoint?

What is the bridge to Chapter 12?

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, Script Security 1415.v9a_f9b_3a_c253d, and Pipeline: Nodes and Processes 1479.v56e587f413a_7. Pipeline: Groovy 4380 requires Jenkins 2.528.3 or newer. The mandatory lab uses a disposable lab-linux agent, a synthetic repository, no production credential, and no unsandboxed approval. Record your actually installed versions because plugin releases change independently from Jenkins core.

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.