In a Jenkins Declarative Pipeline, when do a stage's post conditions always, success, failure and unstable each run, and which condition runs last?
answer
- the stage's guaranteed epilogue
- runs even when steps threw
- condition names map to build results
- always runs, but it is not last
- cleanup is evaluated after everything else
basics
~20 sA stage's post block runs after its steps finish, however they finished. always runs every time; success, failure and unstable run only when the stage ended with that result; cleanup runs last, after every other condition has been evaluated.
solid answer
~40 s`post` is the stage's guaranteed epilogue: it runs whether the steps succeeded, failed or threw, which is why archiving, test publishing and notifications live there rather than at the end of `steps`. The condition blocks are `always`, `changed`, `fixed`, `regression`, `aborted`, `failure`, `success`, `unstable`, `unsuccessful` and `cleanup`. Several can fire in one run — a failing stage triggers `always`, `failure`, `unsuccessful` and `cleanup`. The two order-sensitive ones are worth naming: `always` runs regardless of result but is *not* last, while `cleanup` is defined to run after every other condition has been evaluated, which is why workspace deletion belongs there and not in `always`. `changed`, `fixed` and `regression` compare against the previous run of the same job, so they only make sense on a job with history.
code
groovy · 12 linesstage('Integration tests') {
steps {
sh 'docker compose up -d'
sh 'make integration'
}
post {
always { junit 'reports/**/*.xml' }
failure { archiveArtifacts artifacts: 'logs/**', allowEmptyArchive: true }
fixed { echo 'integration tests are green again' }
cleanup { sh 'docker compose down -v'; deleteDir() }
}
}go deeper
Know that post attaches to a stage and runs after its steps whatever happened, and that always fires on every result while success and failure fire only on theirs.
Name the full condition set and explain that several fire in one run, that cleanup is evaluated after all the others, and that changed, fixed and regression compare against the previous run.
Demonstrate the debugging instinct: when an expected notification or archive is missing, work out which result the stage actually ended with and whether something downgraded a failure before post was evaluated.
Own the convention across the team's pipelines — what reporting is mandatory on every stage, where destructive teardown lives, and how notification noise is bounded by using fixed and regression instead of firing on every result.
## What `post` guarantees Steps in a Declarative stage run until one of them throws. When that happens the rest of `steps` is skipped — so anything you put at the bottom of `steps` (publish the test report, notify Slack, tear down the test database) silently does not happen on exactly the runs where you most needed it. `post` exists to fix that: it is attached to the stage, and Jenkins evaluates it after the stage body completes, successfully or not. ```groovy stage('Test') { steps { sh 'make test' } post { always { junit 'reports/**/*.xml' } failure { echo 'tests failed — see the report above' } cleanup { deleteDir() } } } ``` Here `junit` runs even when `make test` exits non-zero, which is the whole point: the report is most valuable on the failing run. ## The condition set - `always` — runs regardless of the stage's result. - `success` — the stage completed with SUCCESS. - `failure` — the stage completed with FAILURE. - `unstable` — the stage completed with UNSTABLE (typically because a publisher such as `junit` recorded test failures, or a step called `unstable`). - `aborted` — the run was aborted, for example by a user or an expired `timeout`. - `unsuccessful` — anything that is not SUCCESS; a convenient catch-all for failure, unstable and aborted. - `changed` — the result differs from the previous run of this job. - `fixed` — this run succeeded and the previous run was failed or unstable. - `regression` — this run is worse than the previous one (previous was successful, this one is not). - `cleanup` — runs after all other post conditions have been evaluated, whatever the result. More than one block fires per run: a stage that goes from green to red triggers `always`, `failure`, `unsuccessful`, `changed`, `regression` and `cleanup`. ## Why `cleanup` exists when `always` already runs This is the crux of the question. `always` runs unconditionally, but it is only one condition among several; the result-specific blocks may run after it. If you delete the workspace in `always`, a `failure` block that tries to read a log file afterwards finds nothing. `cleanup` is defined to run last, after every other condition has been evaluated, so it is the correct home for destructive teardown: `deleteDir()`, removing a namespace, stopping a container. The rule of thumb: `always` is for things that must happen on every result, `cleanup` is for things that must happen *after everything else*. ## History-relative conditions `changed`, `fixed` and `regression` are evaluated against the previous run of the same job (the same branch, for a multibranch pipeline). They are how teams send "build is back to green" and "you broke main" notifications without spamming a channel on every run. On a brand-new job, or the first run of a new branch, there is no previous result to compare with, so do not build critical behaviour on them. ## Stage-level versus pipeline-level scope A stage's `post` sees that stage's result and runs while the stage's agent and workspace are still available — that is why per-stage artifact archiving works. Blocks written at the top level of the pipeline instead of on a stage run once at the end of the whole run and reflect the overall build result; put per-stage teardown on the stage so it happens close to the work it cleans up. ## The trap worth naming A stage result and the build result are not the same thing. A `post { failure { ... } }` block on a stage fires on that stage's own FAILURE, and if something downgraded the failure — for example the steps were wrapped in a `catchError` that set the build to UNSTABLE — the stage may end UNSTABLE, so `unstable` fires and `failure` does not. When a notification you expected did not arrive, checking which result the stage actually ended with is the first diagnostic step.
- Why put deleteDir() in cleanup rather than in always?Because `always` is only one of several conditions, and result-specific blocks such as `failure` may still be evaluated after it. Deleting the workspace in `always` can pull the log and report files out from under a `failure` block that wanted to archive them. `cleanup` is defined to run after every other condition, so destructive teardown there is safe.
- A stage's post failure block never fires even though the tests clearly failed. What would you check first?What result the stage actually ended with. If the steps were wrapped in something that downgraded the failure — a `catchError` setting UNSTABLE, or a publisher that only marks the build unstable on test failures — the stage ends UNSTABLE, so `unstable` and `unsuccessful` fire while `failure` does not. Check the stage result in the build's stage view, not the console text.
- How do changed, fixed and regression differ from the other post conditions?They are relative to the previous run of the same job or branch rather than to this run alone. `changed` fires when the result differs from last time, `fixed` when this run succeeded after a failed or unstable one, and `regression` when this run is worse than the previous. They are the basis of "back to green" and "you broke it" notifications, and they mean nothing on a job's first run.
saying these in an interview costs you the question
- Saying post is skipped when a step fails
- Thinking always is the last condition to run
- Deleting the workspace in always, then archiving in failure
- Assuming only one post condition can fire per run
- Confusing the stage's result with the overall build result