Jenkins Lesson 13 – Jenkinsfile Introduction | Dataplexa
Section II · Lesson 13

Jenkinsfile Introduction

You've seen a Jenkinsfile in action. Now let's go inside it — its structure, where it lives, how Jenkins finds it, and the patterns that make production Jenkinsfiles reliable and maintainable.

This lesson covers

Where the Jenkinsfile lives → How Jenkins finds it → The full anatomy of a Jenkinsfile → agent, environment, options, stages, post — all explained → A production-ready example

Lessons 11 and 12 gave you the why and the two syntax flavours. This lesson is about the what — what every directive in a real Jenkinsfile actually does, why it's there, and what a production-grade Jenkinsfile looks like before the team starts adding complexity.

Most Jenkinsfile tutorials show you a three-line example and call it done. Real Jenkinsfiles have ten to fifteen directives working together. By the end of this lesson you'll be able to read any Declarative Jenkinsfile and understand exactly what each section is doing.

Where the Jenkinsfile Lives

The Jenkinsfile lives in the root directory of your code repository — the same folder as your README.md, your package.json or build.gradle, your .gitignore. It sits alongside your application code, not inside Jenkins.

Typical repository structure

📁 checkout-service/

📄 Jenkinsfile ← lives here, root of the repo

📄 README.md

📄 build.gradle

📄 .gitignore

📁 src/

📁 tests/

📁 docker/

Three things to know about the Jenkinsfile name and location:

The name is always exactly Jenkinsfile — capital J, no file extension. Not jenkinsfile, not Jenkinsfile.groovy, not pipeline.jenkins. Just Jenkinsfile. Jenkins looks for this exact name by default.

📁

You can change the path in the job config — if your team stores Jenkinsfiles in a ci/ folder, go to the Pipeline job → Configure → Pipeline → Script Path and change it to ci/Jenkinsfile. Jenkins will find it there.

🌿

Every branch can have its own Jenkinsfile — in a Multibranch Pipeline, Jenkins reads the Jenkinsfile from whichever branch it's building. Your main Jenkinsfile can deploy to production. Your feature/* Jenkinsfile can skip the deploy stage entirely.

The Full Anatomy of a Declarative Jenkinsfile

Here are all the top-level directives available in a Declarative Jenkinsfile. You don't use all of them in every pipeline — but you need to know what each one does so you can reach for the right one when you need it.

agent

Tells Jenkins where to run the pipeline — on any available agent, a specific label, a Docker container, or a Kubernetes pod. Every Declarative pipeline must have an agent directive.

environment

Defines environment variables available to all stages. Set here once, used anywhere in the pipeline. Credentials can also be injected here using the credentials() helper.

options

Pipeline-level settings — build timeout, retry count, how many old builds to keep, whether to skip the default checkout. Configured once, applies to the whole pipeline.

parameters

Defines inputs a human (or another system) can pass to the pipeline at trigger time — strings, booleans, choice dropdowns. Covered in depth in Lesson 17.

triggers

Defines what automatically fires the pipeline — a cron schedule, SCM polling, or a webhook from GitHub. Without triggers, the pipeline only runs when manually started.

stages

The container that holds all your stage blocks. Every Declarative pipeline must have exactly one stages block. All the work happens inside here.

post

Runs after all stages — notifications, cleanup, artifact publishing. Use always, success, failure, unstable, or aborted conditions to control exactly when each action fires.

tools

Automatically installs and makes available a specific version of a build tool — Maven, Gradle, JDK, Node.js. Jenkins installs the tool on the agent before stages run.

A Production-Ready Jenkinsfile

The scenario:

You're the lead DevOps engineer at a fintech company. The payments team has been using a minimal Jenkinsfile with just a Test stage. They've now asked you to upgrade it to a production-grade pipeline that includes build timeout protection, environment variables, a proper multi-stage flow, and post-build notifications. This is what you'd write.

New terms in this code:

  • options { timeout() } — sets a maximum time the entire pipeline is allowed to run. If it exceeds this, Jenkins aborts it. Prevents runaway builds from blocking agents for hours.
  • options { buildDiscarder() } — tells Jenkins to keep only the last N builds. Same as the "Discard old builds" setting from Lesson 10, but defined in code instead of the UI.
  • environment { } — defines pipeline-wide environment variables. Every stage and step can read these variables automatically.
  • credentials('id') — fetches a stored Jenkins credential by its ID and injects it as an environment variable. The value is masked in all console output — nobody can read it from the logs.
  • when { branch 'main' } — a conditional directive that makes a stage only run when a specific condition is true. Here, the Deploy stage only runs when building the main branch — all other branches skip it automatically.
  • currentBuild.result — a Jenkins built-in variable that holds the result of the current build so far — SUCCESS, FAILURE, UNSTABLE, or null if still running.
  • currentBuild.displayName — the build number and name shown in the Jenkins UI. You can override it to add useful info like the version being built.
pipeline {

    // Run on any agent with the 'linux' label — don't use 'any' in production
    agent { label 'linux' }

    // Pipeline-level options — apply to every stage
    options {
        // Abort the entire pipeline if it runs for more than 30 minutes
        timeout(time: 30, unit: 'MINUTES')
        // Keep only the last 20 builds to save disk space
        buildDiscarder(logRotator(numToKeepStr: '20'))
        // Add timestamps to every line of console output
        timestamps()
    }

    // Pipeline-wide environment variables — available in every stage
    environment {
        APP_NAME    = 'payments-service'
        DEPLOY_ENV  = 'staging'
        // credentials() fetches the stored Jenkins credential with this ID
        // Jenkins masks this value in all console output automatically
        DOCKER_CREDS = credentials('docker-registry-credentials')
    }

    stages {

        stage('Checkout') {
            steps {
                // Pull the code from the SCM configured in the job
                checkout scm
                // Set a human-readable build name in the Jenkins UI
                script {
                    currentBuild.displayName = "#${BUILD_NUMBER} — ${APP_NAME}"
                }
            }
        }

        stage('Test') {
            steps {
                echo "Running tests for ${APP_NAME}"
                sh './gradlew clean test'
            }
            // post inside a stage only runs when that stage completes
            post {
                always {
                    // Publish JUnit test results so Jenkins can track trends
                    junit 'build/test-results/**/*.xml'
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                // DOCKER_CREDS_USR and DOCKER_CREDS_PSW are automatically
                // created by Jenkins when you use credentials() for a username/password type
                sh """
                    docker login -u ${DOCKER_CREDS_USR} -p ${DOCKER_CREDS_PSW} registry.acmecorp.com
                    docker build -t registry.acmecorp.com/${APP_NAME}:${BUILD_NUMBER} .
                    docker push registry.acmecorp.com/${APP_NAME}:${BUILD_NUMBER}
                """
            }
        }

        stage('Deploy') {
            // 'when' makes this stage conditional
            // This stage ONLY runs when building the main branch
            // All feature branches skip this stage automatically
            when {
                branch 'main'
            }
            steps {
                echo "Deploying ${APP_NAME} build ${BUILD_NUMBER} to ${DEPLOY_ENV}"
                sh "./deploy.sh ${DEPLOY_ENV} ${BUILD_NUMBER}"
            }
        }

    }

    // Post block runs after ALL stages complete
    post {
        success {
            echo "✅ ${APP_NAME} pipeline passed — build ${BUILD_NUMBER} deployed to ${DEPLOY_ENV}"
        }
        failure {
            echo "❌ ${APP_NAME} pipeline failed — build ${BUILD_NUMBER} did not deploy"
        }
        // 'always' runs regardless of result — good for cleanup
        always {
            // Clean the workspace after every build to save disk space
            cleanWs()
        }
    }

}

Where to practice: Add this Jenkinsfile to the repository you created in Lesson 11. You'll need a Docker daemon running on your agent for the Docker stage — if you don't have that yet, comment out the Build Docker Image stage and run the rest. Store a test credential in Jenkins under Manage Jenkins → Credentials with the ID docker-registry-credentials to test the credentials injection. Full Jenkinsfile reference at jenkins.io — Pipeline Syntax.

Started by user admin
[Pipeline] Start of Pipeline
[Pipeline] node (agent-linux-01)
[Pipeline] { (Checkout)
09:14:22 Checking out https://github.com/acmecorp/payments-service.git
09:14:24 > git checkout main
09:14:24 [payments-service] $ currentBuild.displayName set to "#42 — payments-service"
[Pipeline] { (Test)
09:14:25 Running tests for payments-service
09:14:25 + ./gradlew clean test
09:15:18 BUILD SUCCESSFUL in 53s
09:15:18 41 tests completed, 0 failed
09:15:18 Recording test results
[Pipeline] { (Build Docker Image)
09:15:19 + docker login -u deployer -p ****  registry.acmecorp.com
09:15:20 Login Succeeded
09:15:20 + docker build -t registry.acmecorp.com/payments-service:42 .
09:15:48 Successfully built a3f2b1c9d4e5
09:15:48 + docker push registry.acmecorp.com/payments-service:42
09:16:02 pushed: registry.acmecorp.com/payments-service:42
[Pipeline] { (Deploy)
09:16:03 Deploying payments-service build 42 to staging
09:16:03 + ./deploy.sh staging 42
09:16:31 Deployment complete.
[Pipeline] stage (post - success)
✅ payments-service pipeline passed — build 42 deployed to staging
[Pipeline] stage (post - always)
[Pipeline] cleanWs
Deleting project workspace... done
[Pipeline] End of Pipeline
Finished: SUCCESS

What just happened?

  • Timestamps on every line — the timestamps() option added 09:14:22-style prefixes to every console line. In a long build this is invaluable — you can see exactly which step took the most time without doing mental arithmetic.
  • #42 — payments-service — the currentBuild.displayName override changed the build label in the Jenkins UI from a plain "#42" to something descriptive. This sounds minor but makes the build history page far more readable.
  • -p **** — Jenkins automatically masked the Docker password in the console output. The four stars replace the actual credential value. This is the credentials() injection working correctly — the password was used but never exposed in any log.
  • 41 tests completed, 0 failed then Recording test results — the stage-level post { always { junit ... } } fired immediately after the Test stage completed, publishing the JUnit XML results to Jenkins. This lets Jenkins track test trends across builds and highlight flaky tests.
  • Deploy stage ran — because this was a build of the main branch, the when { branch 'main' } condition evaluated true and the stage executed. On a feature branch, this stage would be skipped entirely and you'd see [Pipeline] { (Deploy) skipped } in the output.
  • cleanWs() — the workspace was deleted after the build. The agent's disk won't accumulate leftover build files. This is the always post condition — it fired even though the build succeeded.

Patterns You'll See in Every Real Jenkinsfile

Always clean the workspace

Put cleanWs() in a post { always { } } block. Without this, every build leaves files on the agent. Over time the disk fills up and builds start failing with "No space left on device" errors at 2 AM.

Use environment variables for anything that changes

If you hardcode staging in three different shell commands, you'll miss one when you need to change it. Define it once in the environment { } block and reference it everywhere with ${DEPLOY_ENV}.

Set a timeout — always

Without a timeout, a hanging test or a network hiccup can block an agent executor for days. A 30-minute timeout on most pipelines means you find out about hangs before your next standup, not the next morning.

Publish test results in the stage-level post

Use post { always { junit '...' } } inside the Test stage rather than in the global post block. That way Jenkins publishes results even if the test stage fails — which is exactly when you need the test report most.

Gate deploys with when { branch 'main' }

Never deploy from a feature branch to production. The when directive is the clean way to enforce this without messy if/else logic scattered across stages.

Teacher's Note

Copy the production Jenkinsfile above into every new project as your starting point. Delete the stages you don't need yet. Add back when you do. It's easier to remove than to remember to add.

Practice Questions

1. Which Jenkinsfile directive makes a stage conditionally skip itself based on the branch name or other criteria?



2. What Jenkins Pipeline step removes all files from the agent's workspace after a build completes?



3. What helper function do you use inside the environment { } block to inject a stored Jenkins credential as an environment variable?



Quiz

1. What does timeout(time: 30, unit: 'MINUTES') in the options block do?


2. Where should the Jenkinsfile be placed in your code repository?


3. When you inject a credential using credentials() in the environment block, what does Jenkins do to protect it?


Up Next · Lesson 14

Pipeline Syntax

Every directive, every block, every option in Declarative syntax — the complete reference with real examples for each one.