Jenkins post Blocks Explained: always, success, failure, unstable, and cleanup

Why post Is More Reliable Than a Final Stage

A stage named Cleanup looks tidy, but Jenkins never reaches it if Test fails first. A Declarative post block is tied to stage or Pipeline completion, so it is the correct home for result-dependent publishing, notification, and cleanup.

pipeline {
  agent { label 'linux' }
  stages {
    stage('Test') {
      steps { sh './gradlew test' }
      post {
        always { junit 'build/test-results/**/*.xml' }
      }
    }
  }
  post {
    always { cleanWs() }
    failure { echo 'Pipeline failed' }
  }
}

Try these lifecycle rules in the Jenkins Warmup post-actions exercise and Leave a Clean Workspace.

---

The Conditions That Matter

ConditionRuns when
alwaysThe stage or Pipeline finishes with any result, including abort
successResult is successful
failureResult is failed
unstableResult is unstable, often because tests failed or code marked it so
unsuccessfulResult is anything other than success
abortedThe run was aborted, timed out, or superseded
changedResult differs from the previous completed run
fixedCurrent run succeeds after the previous run failed or was unstable
regressionResult is worse than the previous completed run
cleanupRuns after other post conditions, regardless of result

failure and unstable are not interchangeable. A failed sh step normally makes the build fail. JUnit can make a build unstable when test cases fail while still publishing the report. Security scanners are often deliberately mapped to unstable during rollout. Notification routing should preserve that distinction.

---

Stage-Level and Pipeline-Level Responsibilities

Use stage-level post for output owned by that stage:

stage('Integration Test') {
  steps {
    sh './ci/integration-test.sh'
  }
  post {
    always {
      junit testResults: 'reports/integration/*.xml'
      archiveArtifacts artifacts: 'reports/integration/logs/**',
                       allowEmptyArchive: true
    }
  }
}

Use top-level post for the whole run: workspace cleanup, one delivery notification, audit metadata, and build-wide status. If every stage sends a Slack message, a single failure can generate a storm.

---

A Production Pattern

post {
  success {
    echo "Released build $BUILD_NUMBER"
  }
  regression {
    mail to: 'delivery-oncall@example.com',
         subject: "Regression: $JOB_NAME #$BUILD_NUMBER",
         body: "Inspect $BUILD_URL"
  }
  fixed {
    echo 'The pipeline has recovered'
  }
  aborted {
    echo 'Build aborted or timed out; no release was promoted'
  }
  cleanup {
    cleanWs(deleteDirs: true)
  }
}

Environment variables inside a single-quoted Groovy string are not automatically expanded by Groovy; many Jenkins steps accept a Groovy string, while sh delegates expansion to the shell. Be conscious of which interpreter owns each dollar sign, especially around secrets.

Keep notification code resilient. If a webhook outage throws from post, it can distract from the original failure. Wrap non-critical notification calls with an explicit policy, log their failure, and never let a “success” notification run before artifact promotion has actually succeeded.

---

Result Semantics Before post

stage('Optional Audit') {
  steps {
    catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
      sh './ci/advisory-audit.sh'
    }
  }
  post {
    unstable { echo 'Audit findings need review' }
  }
}

catchError makes the intended result visible. Compare that with sh './audit.sh || true': Jenkins sees exit code zero and cannot distinguish “clean” from “failed but ignored.” If a quality gate is mandatory, do not catch it at all. If it is advisory, mark the stage unstable and preserve the evidence.

---

Common Mistakes

A useful ordering is: collect evidence in stage-level always; archive or notify based on final result; clean last in cleanup. Practice result policy in Contain a Non-Critical Failure and report publication in Publish Test Results.