Chapter 11Lesson 02~140 minutes

Scripted Pipeline, Groovy Control Flow, node Blocks, Dynamic Logic, and When to Use Scripted Pipelines: Guided Hands-On Workflow and Core Operations

Build a small SCM-backed Scripted Pipeline, branch and loop over synthetic data, allocate nodes deliberately, compare a Declarative equivalent, and inspect which work runs on the controller versus the agent.

Hands-onScripted JenkinsfileLoopsnodeAgent executionEvidence

Learning objectives

  • Create an SCM-backed Scripted Pipeline with a synthetic repository and bounded dynamic task list.
  • Use if/else and loops without turning controller Groovy into heavy application logic.
  • Allocate an agent only around workspace/tool operations and record node/workspace evidence.
  • Compare the Scripted flow with a Declarative equivalent and state why Scripted is or is not justified.
  • Preserve source, build, stage, node, workspace, artifact, and sandbox evidence.

1. Disposable lab topology

Reuse the local Jenkins LTS controller and lab-linux agent pattern from Chapters 08–10. Keep routine build execution off the built-in node. The mandatory path uses a synthetic local/public repository and no real credential.

Preflight: record the actual controller version, Java version, Pipeline: Groovy, Script Security, and Nodes/Processes plugin versions; confirm lab-linux is online; confirm the repository is synthetic; and verify that you can delete only the Chapter 11 lab resources afterward.

2. Create a tiny synthetic project

mkdir jenkins-ch11-scripted && cd jenkins-ch11-scripted
git init
git config user.name "Jenkins Chapter 11 Lab"
git config user.email "jenkins-ch11@example.invalid"
cat > lab-task <<'EOF'
#!/usr/bin/env sh
set -eu
case "$1" in
  lint)    printf 'lint=ok\n' ;;
  unit)    printf 'unit=ok\n' ;;
  package) mkdir -p out; printf 'payload=chapter11\n' > out/app.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: synthetic scripted project'
git rev-parse HEAD

The shell script owns task execution. The Jenkinsfile will own orchestration only.

3. Start with the smallest Scripted Jenkinsfile

stage('Plan') {
  echo "job=${env.JOB_NAME} build=${env.BUILD_NUMBER}"
}

node('lab-linux') {
  stage('Execute') {
    sh './lab-task lint'
  }
}

The first stage uses no workspace. The node step then queues agent-backed work. Run once and record the build number, URL, cause, node name, and workspace path.

4. Add bounded dynamic logic

Use a fixed allowlist and a synthetic requested list. The loop executes as CPS-transformed Groovy orchestration; each sh call launches real work on the agent.

def allowed = ['lint', 'unit', 'package'] as Set
def requested = ['lint', 'unit', 'package']
def selected = requested.findAll { allowed.contains(it) }

stage('Plan') {
  echo "selected=${selected.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
    '''
  }

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

  stage('Retain evidence') {
    sh 'sha256sum out/app.txt > evidence/app.sha256'
    archiveArtifacts artifacts: 'evidence/*,out/*', fingerprint: true
  }
}

The task names are fixed synthetic values here. If they come from a parameter, SCM file, API, or webhook, validate them before they influence stage names, file paths, commands, URLs, or external targets.

5. Prove where code executes

Statement Execution owner Evidence
requested.findAll { ... } Pipeline Groovy/CPS on controller No agent process; visible only through resulting plan/log/state
node('lab-linux') Jenkins queue/scheduler Queue item, selected node, executor
checkout scm Pipeline step using SCM integration in node context Console log, workspace Git metadata, source SHA
sh './lab-task unit' External process on agent Agent process output and workspace changes
archiveArtifacts Pipeline step retaining build evidence Archived files/fingerprints in build record

6. Add a simple branch without hiding intent

def shouldPackage = selected.contains('package')

if (shouldPackage) {
  echo 'Package task is in the validated plan.'
} else {
  echo 'No package task requested.'
}

Do not place dozens of unrelated decisions in one opaque Groovy function. Keep decision evidence close to the stages it controls.

7. Compare one large node with targeted allocations

For this tiny lab, one node block is reasonable because checkout, checks, and packaging are short and share files. A long approval or controller-only planning phase should stay outside the node. If a later phase requires a different trust/tool boundary, use a separate node allocation and transfer only the required evidence.

stage('Plan outside node') {
  echo "taskCount=${selected.size()}"
}
node('lab-linux') {
  stage('Agent work') {
    checkout scm
    sh './lab-task lint'
  }
}

8. Compare a Declarative equivalent

pipeline {
  agent { label 'lab-linux' }
  stages {
    stage('Lint') {
      steps { sh './lab-task lint' }
    }
    stage('Unit') {
      steps { sh './lab-task unit' }
    }
    stage('Package') {
      steps { sh './lab-task package' }
    }
  }
}

If the task list is actually fixed, the Declarative version is clearer. Scripted earns its cost only when the list or orchestration shape truly must be dynamic after validation.

9. Inspect sandbox state without weakening it

Do not add internal Jenkins API calls. Confirm that the job runs in its normal sandboxed Pipeline context. If an existing experiment has a rejected signature, capture the rejection text and remove/rewrite the unsupported call. Do not approve it for this lab.

10. Challenge — choose the correct layer

Requirement: the build receives a synthetic comma-separated TASKS parameter and must run only lint, unit, or package. Where should the safety control live?

Expected design: validate and normalize the parameter in bounded Pipeline logic, reject anything outside the allowlist before entering an agent-side command, then pass only validated task names to the agent process. Do not “fix” the problem with shell eval, broad quoting assumptions, or administrator approval.

11. Cleanup

Download the archived evidence first. Delete only the Chapter 11 synthetic job/repository resources. Do not remove shared Pipeline plugins, shared agents, or unrelated credentials/configuration.

Next lesson

Configuration, Design Choices, and Tradeoffs

Decide when Scripted is worth its flexibility, where node boundaries belong, and when dynamic stage generation hurts readability more than it helps.

Knowledge check

Which part of the lab should perform the actual synthetic task?

Why keep planning outside node when it needs no workspace?

What proves the exact source revision used?

If the task list is fixed, which style is normally clearer?

How should a user-provided task name reach a shell?

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.