Declarative Best Practices: Timeouts, Retries, Post

Wrap flaky work in retries, bound every pipeline with a timeout, and always clean up in post so a stuck build never holds an agent hostage.

The difference between a hobby pipeline and a production one is usually not the happy path — it is how it behaves when things go wrong. Three directives do most of the heavy lifting: timeout, retry, and post.

Bound everything with a timeout

A network hang or a wedged test runner can block an executor indefinitely. Set a top-level options { timeout(...) } so a build gives up instead of squatting on an agent forever. Add tighter stage-level timeouts around anything that talks to the network.

Retry only what is safe to retry

Retrying a flaky npm ci or a Docker pull is reasonable — it is idempotent. Retrying a deploy that already half-applied is dangerous. Use retry(n) narrowly, and combine it with timeout so a retry loop cannot run forever. The waitUntil and retry combo is handy for polling readiness.

Always clean up in post

The post block runs regardless of outcome. Put cleanWs(), artifact archiving, and test-report publishing in always; put alerts in failure and unstable. This is what keeps a red build from leaving a dirty workspace that breaks the next one.

Example

Example · bash
pipeline {
    agent any
    options {
        timeout(time: 45, unit: 'MINUTES')   // hard ceiling for the whole run
        buildDiscarder(logRotator(numToKeepStr: '30'))
        disableConcurrentBuilds()
    }
    stages {
        stage('Install') {
            steps {
                // network step: bound it AND retry transient failures
                retry(3) {
                    timeout(time: 5, unit: 'MINUTES') {
                        sh 'npm ci'
                    }
                }
            }
        }
        stage('Test') {
            steps { sh 'npm test -- --ci' }
        }
    }
    post {
        always {
            junit testResults: 'reports/*.xml', allowEmptyResults: true
            cleanWs()
        }
        failure {
            echo 'Build failed — see notifications lesson to wire up alerts.'
        }
    }
}

When to use it

  • A pipeline wraps a flaky HTTP health-check step in retry(3) so a single transient timeout does not fail the entire build.
  • A timeout(time: 45, unit: 'MINUTES') option at the pipeline level prevents a hung integration test from holding an agent overnight.
  • A post { always { cleanWs() } } block guarantees the workspace is wiped even when an earlier stage throws an exception.

More examples

Pipeline-level timeout and retry

Combines a 45-minute timeout, 2 retries on failure, log rotation, and guaranteed workspace cleanup.

Example · groovy
pipeline {
  agent any
  options {
    timeout(time: 45, unit: 'MINUTES')
    retry(2)
    buildDiscarder(logRotator(numToKeepStr: '30'))
  }
  stages {
    stage('Build') { steps { sh 'mvn package' } }
  }
  post {
    always { cleanWs() }
  }
}

Stage-level retry for flaky step

Retries a flaky integration test stage up to 3 times and always publishes results regardless of outcome.

Example · groovy
stage('Integration Tests') {
  options { retry(3) }
  steps {
    sh './run-integration.sh'
  }
  post {
    always { junit 'reports/**/*.xml' }
  }
}

Post block with all conditions

Uses all five meaningful post conditions to handle cleanup, archiving, and state-change notifications.

Example · groovy
post {
  always   { cleanWs() }
  success  { archiveArtifacts 'dist/**' }
  failure  { slackSend channel: '#builds', color: 'danger',
                       message: "FAILED: ${JOB_NAME}" }
  fixed    { slackSend channel: '#builds', color: 'good',
                       message: "RECOVERED: ${JOB_NAME}" }
  unstable { echo 'Tests unstable — check results' }
}

Discussion

  • Be the first to comment on this lesson.