CoursesJenkins foundations, done rightStages, parallelism & post

Stages, parallelism & post

Structure a pipeline that reports clearly.

Beginner12 min · lesson 6 of 16

A build runs for nine minutes, prints a thousand lines of log, and ends with one word: FAILURE. Where did it break? The compile? A test? The security scan? You are left scrolling. Stages fix that. A stage is a named phase of your pipeline (Checkout, Build, Test, Deploy), and Jenkins draws each one as a box in a row, so a wall of text turns into a status board you can read in a second. This lesson bolts three ideas together: stages that run in order, a parallel block that runs independent work at the same time, and a post block that reports the outcome whether the build ends green or red.

What a declarative pipeline actually looks like

A Jenkinsfile is a recipe card you keep in your repository, right next to the code it builds. Because it lives in Git, someone can review it, and you can see who changed the build and why. Declarative means you describe the shape of the build with a handful of keywords instead of writing a program. pipeline wraps the whole thing. agent any says run this on any free agent, an agent being a worker machine that actually types your commands into a shell. The controller (the central Jenkins server) hands out the work and collects the results; it never runs your build steps itself. stages holds an ordered list of stage blocks, and each stage holds steps, which are the real commands. sh 'make build' runs one shell command on the agent. Stages go top to bottom, and the first one that fails stops everything after it. Jenkins short-circuits on purpose, so you never burn four minutes packaging a build that never compiled. That ordering is a promise you can lean on: any stage can assume every stage above it passed.

Jenkinsfile
pipeline {
agent any
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Build') {
steps { sh 'make build' }
}
stage('Checks') {
failFast true
parallel {
stage('Lint') {
steps { sh 'make lint' }
}
stage('Unit') {
steps { sh 'make test-unit' }
}
stage('Scan') {
steps { sh 'make scan' }
}
}
}
}
post {
always { junit '**/test-results/*.xml' }
success { echo 'All checks green' }
unstable { echo 'Build completed but tests failed - see junit report' }
failure {
mail to: '[email protected]',
subject: "Build ${env.JOB_NAME} #${env.BUILD_NUMBER} FAILED",
body: "Console: ${env.BUILD_URL}console"
}
cleanup { cleanWs() }
}
}

parallel { }: run the independent work at the same time

Some checks never look at each other. A linter, a unit test suite and a dependency scan all read the same checked-out code, and none of them reads another one's output. Running them back to back is like washing one dish, drying it, putting it away, then starting on the next. Nest them in a parallel block inside a single wrapping stage and Jenkins starts every branch at the same moment, so the Checks stage costs you its slowest branch instead of the sum of all three. That matters more than it sounds. When feedback is fast, developers use CI (continuous integration, the habit of merging and testing small changes constantly). When it drags, they find ways around it. failFast true sits in the stage right beside the parallel block and changes what happens on a red result: the moment any branch fails, Jenkins kills the branches still running rather than letting them finish. Turn it on when one failure already means the build is dead and you want the machine freed straight away. Leave it off when you want the full picture in one run, every lint complaint and every scan finding, even though the tests already failed. Speed or a complete report. Pick one.

Two things catch people out here. First, parallel does not conjure capacity out of thin air. An executor is a slot on an agent, one lane that a single branch can occupy. Three branches on an agent with two executors means the third sits in a queue while the other two run, so the stage is only partly parallel. Give a heavy branch its own worker with agent { label 'linux' } inside that branch stage. Second, by default every branch shares one workspace directory, the folder on the agent where your code gets checked out. Two branches writing into build/ at the same time can scribble over each other's files, so split them onto separate agents or separate directories. There is a security line in here too. Every sh step runs on the agent, and an agent is a shared machine you should treat as untrusted. Reporting steps like mail and junit are driven from the controller instead. Bind a credential inside the one stage that needs it, never at the top, so a compromised branch cannot read another branch's secret. Jenkins masks bound secrets as **** in the console, and that holds even when parallel branches interleave their output.

post { }: the block that reports back no matter what

The stages do the work. The post block tells you how it went. post runs once every stage is done, and its conditions fire off the final build status. always runs every single time, which makes it the right home for publishing the JUnit report (the XML file your test runner writes) so the results survive even when the build dies. success runs only when the whole build is green. failure runs only when the build ended in status FAILURE. unstable runs when the build finished but something raised a flag, most often junit spotting failing tests, which sets the build to UNSTABLE, a status that is neither pass nor fail. cleanup runs last, after every other condition, which makes it the correct place for cleanWs() to wipe the workspace. Hold this model in your head: always and cleanup are unconditional, while success, unstable and failure are mutually exclusive, so exactly one of the three fires for any build that finishes. Wire your alerts to failure and unstable both, or a red test suite slips past in silence.

Reading the log: branches interleave, post fires on red

In a real console the parallel branches print at the same time, each line tagged with its branch name in brackets: [Lint], [Unit], [Scan]. The output comes out shuffled instead of in three tidy blocks, which looks alarming the first time you see it. Below, the Unit branch fails. failFast true is set, so Jenkins aborts the Scan branch halfway through, and then post takes over. Watch which conditions fire. always records the JUnit report, failure sends the mail, cleanup wipes the workspace. success and unstable are skipped, because the status is FAILURE.

Console output — payments-api #47
[Pipeline] stage
[Pipeline] { (Checks)
[Pipeline] parallel
[Pipeline] { (Branch: Lint)
[Pipeline] { (Branch: Unit)
[Pipeline] { (Branch: Scan)
[Lint] + make lint
[Unit] + make test-unit
[Scan] + make scan
[Lint] Checked 214 files, 0 issues
[Scan] Resolving 132 dependencies...
[Unit] FAILED tests/test_auth.py::test_login - assert 401 == 200
[Unit] 1 failed, 88 passed in 3.42s
[Unit] make: *** [test-unit] Error 2
[Pipeline] // parallel
[Scan] Aborted: failFast triggered by branch 'Unit'
[Pipeline] { (Declarative: Post Actions)
[Pipeline] junit
Recording test results: 89 tests, 1 failed
[Pipeline] mail
Sending email to: [email protected]
[Pipeline] cleanWs
[WS-CLEANUP] Deleting project workspace /home/ci/workspace/payments-api
[Pipeline] End of Pipeline
ERROR: script returned exit code 2
Finished: FAILURE

You do not need the web interface to start a build and watch it run. The Jenkins CLI (command-line interface) is a small client that talks to the controller over SSH or HTTP. It can trigger a job and stream the console straight back to your terminal, which is how you drive builds from a script. The handy part: the CLI process exits non-zero when the build fails, so it drops cleanly into other scripts.

trigger + follow the build (shell)
# -f follows the build to completion, -v streams the console
java -jar jenkins-cli.jar -s https://jenkins.acme.internal/ \
-auth ci:$JENKINS_API_TOKEN \
build payments-api -f -v
CLI response
Started payments-api #47
Running as ci
[Unit] 1 failed, 88 passed in 3.42s
Sending email to: [email protected]
Finished: FAILURE
$ echo $?
1
Diagram
Sequential spine: runs in order, first failure stops the rest
Checkout
checkout scm
Build
sh 'make build'
parallel { } inside stage('Checks'): all at once, failFast aborts siblings
Lint
make lint
Unit
make test-unit → fails
Scan
aborted by failFast
post { }: after the stages, picked by final status
always
junit report
failure
mail team
cleanup
cleanWs()
A failing test makes the build UNSTABLE, not FAILURE
When the junit step finds failing tests, it sets the build status to UNSTABLE, which is neither SUCCESS nor FAILURE. If your post block handles only failure, your notifier stays completely silent on a red test suite. The build turns yellow in the UI and no alert ever leaves the building. Always pair a failure block with an unstable block, or fold both into always with a status check, so a broken test cannot slip through unnoticed.
Quick check
01A test fails, junit records it, and the build ends UNSTABLE. Your post block has only always (junit), success and failure. Who hears about it?
Incorrect — No. A failing test recorded by junit sets the status to UNSTABLE, not FAILURE, and the failure condition only runs when the status is FAILURE.
Correct — always runs and files the report, but no notifier fires because you never handled the unstable status. That is the silent failure trap. Add an unstable block.
Incorrect — No. success runs only on status SUCCESS, and UNSTABLE is a separate status, so success is skipped.
Incorrect — No. success, unstable and failure are mutually exclusive, so at most one runs per build, picked by the final status.
02Inside the Checks stage, the Lint branch runs sh 'make lint', and the post block runs junit and mail. Which machine actually runs each one?
Correct — Every sh step runs on the agent, which you should treat as untrusted, while reporting steps like mail and junit are driven from the controller.
Incorrect — No. Reporting steps such as mail and junit are driven from the controller, not the agent.
Incorrect — No. Sequential and parallel stage steps alike run on an agent. The controller only schedules the work.
Incorrect — No. sh steps execute on the agent, which is exactly why the lesson treats the agent as untrusted.
03The Checks stage holds three parallel branches (Lint, Unit, Scan), the assigned agent has only two executors, and you set no per-branch agent and no failFast. What really happens?
Incorrect — No. Parallelism is capped by executors, the number of concurrent slots on the agent.
Correct — With only two executors the third branch queues. Give a heavy branch its own worker with agent { label ... } inside that branch to avoid it.
Incorrect — No. The extra branch queues. Having more branches than executors is not an error.
Incorrect — No. Jenkins does not offload build steps to the controller. The branch waits for an executor to free up.

Next you make that same Jenkinsfile behave differently depending on what set it off. Environment variables, build parameters and when conditions let one file treat a pull request, a nightly run and a production release as three separate jobs, without you keeping three copies of the pipeline in sync.

Try this

Work through “Reading the log: branches interleave, post fires on red” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: a failing test makes the build UNSTABLE, not FAILURE. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related