Chapter 12Lesson 02~150 minutes

Pipeline Steps, Error Handling, retry, timeout, catchError, input, waitUntil, and Resilient Flow Control: Guided Hands-On Workflow and Core Operations

Build a disposable Pipeline that safely retries a flaky read-only check, bounds polling, records a human approval, downgrades a synthetic verification failure without hiding it, and executes an idempotently guarded local side effect.

Hands-onretrytimeoutcatchErrorinputwaitUntilIdempotency

Learning objectives

  • Create a synthetic resilient Pipeline with explicit evidence for each attempt and state transition.
  • Use retry only around a read-only flaky operation and prove the number of attempts.
  • Bound waitUntil and human input with timeouts while avoiding unnecessary agent occupancy.
  • Use catchError to continue evidence collection without converting a critical failure to green.
  • Guard a simulated side effect with an immutable operation ID and independently verify the resulting target state.

1. Disposable lab and evidence baseline

Reuse the local controller and lab-linux agent pattern from earlier chapters. The lab uses only local files under a synthetic workspace/target directory. No package registry, cloud account, production environment, or real credential is required.

Preflight: record Jenkins/Java, Pipeline: Basic Steps, and Pipeline: Input Step versions; confirm the lab agent is online; confirm the synthetic repository is disposable; and decide exactly which local target directory belongs to this chapter.
mkdir jenkins-ch12-resilience && cd jenkins-ch12-resilience
git init
git config user.name "Jenkins Chapter 12 Lab"
git config user.email "jenkins-ch12@example.invalid"
mkdir -p scripts
cat > scripts/flaky-read.sh <<'EOF'
#!/usr/bin/env sh
set -eu
mkdir -p .lab-state
count_file=.lab-state/read-attempts
count=0
[ -f "$count_file" ] && count=$(cat "$count_file")
count=$((count + 1))
printf '%s\n' "$count" > "$count_file"
printf 'read_attempt=%s\n' "$count"
[ "$count" -ge 2 ]
EOF
chmod +x scripts/flaky-read.sh
git add . && git commit -m 'ch12: synthetic resilience lab'
git rev-parse HEAD

The script intentionally fails on its first observation and succeeds on the second. It does not mutate an external business target, so retry is safe for the exercise.

2. Build the Pipeline incrementally

pipeline {
  agent none
  options {
    timestamps()
  }
  stages {
    stage('Observe baseline') {
      agent { label 'lab-linux' }
      steps {
        checkout scm
        sh '''
          set -eu
          rm -rf .lab-state evidence synthetic-target
          mkdir -p evidence synthetic-target
          {
            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
        '''
      }
    }
  }
}

agent none ensures later waits can occur without holding a global executor. The first stage allocates a workspace only for checkout and evidence.

3. Retry only the synthetic read-only check

stage('Read-only retry') {
  agent { label 'lab-linux' }
  steps {
    retry(3) {
      sh './scripts/flaky-read.sh'
    }
    sh 'cp .lab-state/read-attempts evidence/read-attempts.txt'
  }
}

Expected evidence: attempt 1 fails, attempt 2 succeeds, attempt 3 is never needed. The final artifact must contain 2. If the body performed publication or deployment instead, this retry boundary would be unjustified without an idempotency contract.

4. Bound a polling wait

Create a synthetic readiness flag from a short background process on the agent, then poll for it under a finite timeout.

stage('Bounded wait') {
  agent { label 'lab-linux' }
  steps {
    sh '(sleep 3; touch synthetic-ready.flag) >/dev/null 2>&1 &'
    timeout(time: 30, unit: 'SECONDS') {
      waitUntil(initialRecurrencePeriod: 500, quiet: true) {
        return fileExists('synthetic-ready.flag')
      }
    }
    sh 'printf "ready=true\n" > evidence/readiness.txt'
  }
}

This demonstrates the semantics, not an ideal production integration. For a real service, event-driven callbacks are often preferable to controller-driven polling.

5. Wait for approval without holding an executor

stage('Approval') {
  steps {
    script {
      timeout(time: 10, unit: 'MINUTES') {
        env.LAB_APPROVER = input(
          message: 'Proceed with local synthetic target change?',
          ok: 'Proceed',
          submitter: 'lab-approver',
          submitterParameter: 'APPROVER'
        )
      }
      echo "approval recorded for this disposable lab"
    }
  }
}

No stage agent is declared, so Jenkins does not reserve lab-linux just to wait for a person. Do not print secrets; the approver identity itself is audit evidence and may be written later to the evidence packet.

6. Guard the simulated side effect

stage('Guarded synthetic side effect') {
  agent { label 'lab-linux' }
  steps {
    sh '''
      set -eu
      op_id="chapter12-${BUILD_NUMBER}"
      marker="synthetic-target/${op_id}.done"
      if [ -f "$marker" ]; then
        printf 'already_applied=%s\n' "$op_id" | tee evidence/side-effect.txt
      else
        printf 'applied_by_build=%s\n' "$BUILD_NUMBER" > "$marker"
        printf 'applied=%s\n' "$op_id" | tee evidence/side-effect.txt
      fi
      sha256sum "$marker" > evidence/side-effect.sha256
    '''
    writeFile file: 'evidence/approver.txt', text: "${env.LAB_APPROVER}\n"
  }
}

The workspace marker is only a teaching substitute for a target-side idempotency key. In production, the target system should own durable operation identity.

7. Preserve a non-critical verification failure

stage('Synthetic optional verification') {
  agent { label 'lab-linux' }
  steps {
    catchError(
      buildResult: 'UNSTABLE',
      stageResult: 'UNSTABLE',
      catchInterruptions: false,
      message: 'Optional synthetic verifier failed'
    ) {
      sh 'test -f deliberately-absent-report.txt'
    }
    sh 'printf "verification=unstable_expected\n" > evidence/verification.txt'
  }
}

This stage teaches result semantics. It is explicitly optional; a release-signing or security gate should not be downgraded merely to keep the Pipeline moving.

8. Retain the evidence

stage('Retain evidence') {
  agent { label 'lab-linux' }
  steps {
    sh 'find evidence -maxdepth 1 -type f -print | sort > evidence/manifest.txt'
    archiveArtifacts artifacts: 'evidence/*,synthetic-target/*', fingerprint: true
  }
}

Expected final run result is UNSTABLE because the optional verifier was downgraded. A green result would be wrong evidence.

9. Challenge — choose the correct resilience layer

A synthetic HTTP status query intermittently returns 503, while the following action would create a unique release record. Where should retry go?

Expected design: retry only the read-only status query. Before creating the release, obtain a deterministic operation ID and check target state. Never wrap the entire query-plus-create transaction in a blind retry.

10. Cleanup

Download the evidence first. Delete only Chapter 12 lab jobs/repository resources and the synthetic target directory. Preserve shared agents/plugins and unrelated Pipeline state.

Next lesson

Configuration, Design Choices, and Tradeoffs

Decide when to retry, fail fast, downgrade an error, place timeout boundaries, reserve agents, poll, or wait for human approval.

Knowledge check

Why is the flaky read step safe to retry?

What should the final build result be after the optional verifier?

Why is approval in an agent none stage?

What proves the retry count?

What would make a real side effect retryable?

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: Basic Steps 1098.v808b_fd7f8cf4, and Pipeline: Input Step 560.v56198a_642157. The mandatory path is local/disposable, uses synthetic state and fake identities, performs no production deployment/publication, and does not require commercial services. Always record the versions actually installed on your controller because plugins release 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.