Stages, parallelism & post
Structure a pipeline that reports clearly.
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.
pipeline {agent anystages {stage('Checkout') {steps { checkout scm }}stage('Build') {steps { sh 'make build' }}stage('Checks') {failFast trueparallel {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.
[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] junitRecording test results: 89 tests, 1 failed[Pipeline] mailSending email to: [email protected][Pipeline] cleanWs[WS-CLEANUP] Deleting project workspace /home/ci/workspace/payments-api[Pipeline] End of PipelineERROR: script returned exit code 2Finished: 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.
# -f follows the build to completion, -v streams the consolejava -jar jenkins-cli.jar -s https://jenkins.acme.internal/ \-auth ci:$JENKINS_API_TOKEN \build payments-api -f -v
Started payments-api #47Running as ci[Unit] 1 failed, 88 passed in 3.42sSending email to: [email protected]Finished: FAILURE$ echo $?1
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.