Chapter 10Lesson 02~135 minutes

Declarative Pipeline Syntax, agent, stages, steps, options, parameters, environment, tools, and post: Guided Hands-On Workflow and Core Operations

Construct and validate a disposable Declarative Pipeline step by step, add parameters/environment/tools/options/stage agents/post, inspect evidence, then introduce and repair a structural error.

Hands-onValidationStage agentsToolsParametersPost

Learning objectives

  • Create an SCM-backed Declarative Pipeline using a synthetic repository.
  • Add directives incrementally and explain which state each one changes.
  • Use top-level agent none with stage-level allocation to make executor ownership visible.
  • Capture build, node, workspace, tool, parameter, and stage evidence without exposing secrets.
  • Introduce a structural Declarative error, preserve its validation evidence, and repair only that layer.

1. Disposable topology and preflight

Reuse the authorized local controller and lab-linux agent pattern from Chapters 08–09. The repository is synthetic. The job name in this lesson is ch10-declarative-lab. No cloud account, production SCM organization, or real deployment target is required.

Before modifying anything: confirm the controller is Jenkins 2.568.3 LTS (or record your actual current LTS), the lab agent is online, the built-in node is not used for routine builds, Pipeline/Declarative plugins are installed, and Jenkins tools named jdk21-build and maven-3.9 either exist or are replaced consistently with your verified Chapter 08 names.

Record the exact tool binaries those names resolve to with java -version and mvn -version during the build. A Jenkins tool name is configuration identity, not proof of binary version.

2. Create a synthetic source repository

mkdir jenkins-ch10-demo && cd jenkins-ch10-demo
git init
git config user.name "Jenkins Chapter 10 Lab"
git config user.email "jenkins-ch10@example.invalid"
printf 'chapter10\n' > payload.txt
git add payload.txt
git commit -m 'ch10: add payload'
git rev-parse HEAD

Save the SHA. The Jenkinsfile will be committed into the same repository so the Pipeline definition and payload have one immutable source identity.

3. Increment 1 — validate the smallest Pipeline

Start deliberately small. Use agent none so agent allocation becomes visible in the next increment.

pipeline {
  agent none
  stages {
    stage('Model') {
      steps {
        echo 'Declarative model accepted.'
      }
    }
  }
}

This form is structurally valid, but the echo step does not require a workspace. Commit it. Create a Pipeline-from-SCM job and run it. Preserve the build number and exact Jenkinsfile SHA.

4. Increment 2 — add parameter and global environment policy

pipeline {
  agent none
  options {
    timestamps()
    disableConcurrentBuilds()
    buildDiscarder(logRotator(numToKeepStr: '20'))
  }
  parameters {
    choice(name: 'TARGET', choices: ['sandbox', 'qa'], description: 'Synthetic target')
    booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Execute test stage')
  }
  environment {
    APP_NAME = 'ch10-demo'
  }
  stages {
    stage('Model') {
      steps {
        echo "app=${env.APP_NAME} target=${params.TARGET} tests=${params.RUN_TESTS}"
      }
    }
  }
}

disableConcurrentBuilds() changes job/run scheduling behavior; it is not a shell mutex. buildDiscarder changes retained run history policy. The parameters become build inputs; the environment value becomes available to enclosed steps.

5. Increment 3 — allocate a stage agent and tools

Add a stage that needs a workspace. Keep the tools next to the stage-level agent so the ownership boundary is obvious.

stage('Build evidence') {
  agent { label 'lab-linux' }
  tools {
    jdk 'jdk21-build'
    maven 'maven-3.9'
  }
  options {
    timeout(time: 5, unit: 'MINUTES')
  }
  steps {
    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)"
        printf 'target=%s\n' "$TARGET"
        java -version 2>&1
        mvn -version
      } > evidence/build.txt
    '''
    archiveArtifacts artifacts: 'evidence/*', fingerprint: true
  }
}

The stage-level timeout is applied before entering the stage agent; therefore queue/agent-allocation time can count against that stage timeout. This differs from a top-level Pipeline timeout with a top-level agent, where agent allocation occurs before the timeout starts.

6. Increment 4 — conditionally run a test stage

stage('Test') {
  when {
    expression { return params.RUN_TESTS }
  }
  agent { label 'lab-linux' }
  steps {
    sh '''
      set -eu
      test "$TARGET" = sandbox -o "$TARGET" = qa
      printf 'tests-ok target=%s\n' "$TARGET" > test-result.txt
    '''
  }
  post {
    always {
      archiveArtifacts artifacts: 'test-result.txt', allowEmptyArchive: true
    }
  }
}

when controls whether the stage executes. A skipped stage is not a failed stage. The allowlist check still exists inside the executable step because build inputs are untrusted and conditions should not be the only safety boundary.

7. Increment 5 — wait without holding an executor

Add a purely synthetic approval stage. The stage-level input directive waits before acquiring its declared agent.

stage('Approve simulation') {
  input {
    message 'Run the bounded synthetic side effect?'
    ok 'Continue'
  }
  agent { label 'lab-linux' }
  steps {
    sh '''
      set -eu
      case "$TARGET" in
        sandbox|qa) ;;
        *) echo 'rejected target' >&2; exit 2 ;;
      esac
      printf 'simulated target=%s build=%s\n' "$TARGET" "$BUILD_NUMBER" > side-effect.txt
    '''
    archiveArtifacts artifacts: 'side-effect.txt', fingerprint: true
  }
}

The “side effect” is only a local text file. The lab teaches control flow without touching a production service.

8. Add Pipeline-level post

Because the Pipeline uses agent none, do not assume a global workspace in top-level post. Keep workspace-bound artifact archiving in stages that own an agent; use the top-level block for controller-level evidence such as final status text.

post {
  always {
    echo "finalResult=${currentBuild.currentResult} build=${env.BUILD_NUMBER}"
  }
  failure {
    echo 'Preserve the failing build URL and first error before rerun.'
  }
  cleanup {
    echo 'No external resources were created by this mandatory lab.'
  }
}

9. Validate before spending agent time

Use the Pipeline Syntax/Declarative validation capability available on your controller or simply run the SCM-backed lab after committing the Jenkinsfile. Record whether failure occurs before any agent allocation. A structural validation error should not be “fixed” by restarting an agent.

10. Deliberately break one directive

Create a new commit that moves parameters inside a stage:

stage('Broken') {
  parameters {
    string(name: 'ILLEGAL_HERE', defaultValue: 'x')
  }
  steps { echo 'This model should be rejected.' }
}

Trigger/validate that revision and preserve the exact validation message, build URL (if a run record is created), and source SHA. The expected causal layer is the Declarative model, not Git, queue capacity, or shell execution.

11. Repair only the structural layer

Move the parameter back to the top-level parameters directive, commit a new revision, and run again. Do not simultaneously change plugins, agents, credentials, or tools. A narrow repair keeps the comparison explainable.

12. Required evidence

  • Jenkins/Java/Pipeline/Declarative version baseline.
  • Job full name, build number/URL, cause, Jenkinsfile/source SHA.
  • Configured tool names plus observed java -version/mvn -version.
  • Parameter values that are safe to record; never secret values.
  • Node/label/workspace for agent-backed stages.
  • Stage outcomes including skipped/failed/success states.
  • Validation error from the intentionally broken revision and the repairing revision SHA.
  • Archived evidence files and fingerprints.

13. Small challenge: choose the layer

The Jenkinsfile validates, but maven 'maven-3.9' cannot resolve on the chosen agent. Should you modify Declarative grammar, restart the controller, or inspect tool configuration/agent compatibility?

The correct layer is tool configuration/execution context. Preserve the run evidence, inspect the configured tool name and agent OS/architecture, then correct the smallest mismatch.

14. Cleanup

Keep the evidence packet. Delete only the synthetic job/repository resources created for Chapter 10. Do not remove shared Pipeline plugins, global tools, credentials, or agents used by other labs.

Next lesson

Configuration, Design Choices, and Tradeoffs

Choose between top-level and stage agents, global versus local environment, Pipeline options versus step wrappers, and Declarative structure versus a script escape hatch.

Knowledge check

Why use agent none in this lab?

What is the causal layer of a misplaced parameters directive?

Why record actual java -version and mvn -version even when tool names are pinned?

Why is stage-level input useful before an expensive agent?

Why keep workspace-dependent archiving inside an agent-backed stage when top-level agent none is used?

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 aggregator 608.v67378e9d3db_1, Pipeline: Declarative 2.2293.v6e7193cec599, Credentials Binding 728.v902a_273b_8947, and optional Stage View 2.41. Declarative 2.2293 requires Jenkins 2.504.3 or newer. Labs reuse the Chapter 08 tool names jdk21-build and maven-3.9; verify what exact binaries those names resolve to on your disposable agent before running. Plugin versions and supported directives change independently, so capture the installed baseline rather than assuming these versions forever.

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.