Laptop to Production

The Jenkinsfile

A pipeline written as code, line by line.

Time
60 minutes
Level
Intermediate

Part 1: The problem

QuickCart, a Friday deploy

QuickCart’s deploy job lived in the Jenkins web page. On Thursday, Priya opened its settings and added a line to the build script that cleared a cache before deploying. She typed a folder name wrong.

On Friday afternoon, the deploy failed halfway. The job’s settings showed only the current version. Nobody could see what it had been before, who had changed it, or why. The stg and prod jobs had been copied from each other two years earlier and had drifted apart since, so the working stg job was no help either.

Priya fixed it by guessing. It took an hour, during which nothing could be deployed.

A Jenkinsfile fixes this by putting the pipeline in the repository, next to the code.

Part 2: The idea

A Jenkinsfile is a text file called Jenkinsfile, at the root of the repository. It describes the pipeline: what runs, in what order, on which agent, and what happens when something fails. Jenkins reads it from the same commit it’s building.

That one change gives the pipeline everything code already has:

Pipeline in the web pagePipeline in a Jenkinsfile
Changed by editing a text boxChanged by a pull request, reviewed like code
One version, the current oneFull history in git: who, what, when, why
A change is live for everyone at onceA branch can try a new pipeline without affecting main
Copied between jobs, then driftsOne file, used by every branch
Lost with the Jenkins serverIn git, with the code
Key point: The pipeline lives in the repository, next to the code it builds, and changes the same way: by pull request.

There are two ways to write one. Declarative starts with pipeline { and has a fixed structure that Jenkins checks before it runs anything. Scripted starts with node { and is plain Groovy code. This course uses declarative: it’s easier to read, and mistakes are caught before a build starts. When you need a little real code, a script { } block inside a step lets you write some.

The smallest useful Jenkinsfile

Jenkinsfile
pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                sh './test.sh'
            }
        }
    }
}
LineWhat it means
pipeline { }Everything is inside this block. It’s what makes it declarative
agent anyRun on any agent with a free executor
stages { }The list of stages, run from top to bottom
stage('Test') { }One named stage. The name is what you see on the build page
steps { }What the stage actually does, one step after another
sh './test.sh'Run a shell command on the agent. If it exits with anything but 0, the stage fails

agent: where it runs

You writeIt meansUse it when
agent anyAny agent with a free executorEvery agent has the tools you need
agent noneNo agent for the pipeline as a whole. Each stage says its ownSome stages wait, like an approval, and shouldn’t hold an executor while they do
agent { label 'linux' }Any agent with that labelSome agents have tools others don’t
agent { docker { image 'node:22-alpine' } }Start that container on an agent and run the steps inside it. Needs the Docker Pipeline plugin and Docker on the agentEach project needs different tools, and you don’t want to install them all on every agent

agent can also go inside a single stage. Then that stage runs there, whatever the top says.

stages, stage and steps

stages holds one or more stage blocks. Each stage has steps. Common steps:

StepWhat it does
sh 'command'Run a shell command on the agent
echo 'text'Print a line in the build log. Jenkins’ own step, no shell involved
writeFile file: 'x.txt', text: 'hello'Write a file in the workspace
archiveArtifacts artifacts: 'out/*.txt'Keep files from the build, downloadable from the build page
junit 'reports/*.xml'Read test results, so Jenkins shows which tests failed
retry(3) { }Try the steps inside up to 3 times
timeout(time: 2, unit: 'MINUTES') { }Stop the steps inside if they run too long
script { }A small piece of scripted Groovy, for logic declarative can’t express

The folder a build runs in on the agent is called the workspace. Jenkins checks your code out into it before the first stage.

environment: variables for every step

Jenkinsfile
pipeline {
    agent any
    environment {
        APP_NAME = 'quickcart-orders'
        REGION   = 'us-east-1'
    }
    stages {
        stage('Show') {
            steps {
                sh 'echo "Building $APP_NAME for $REGION"'
            }
        }
    }
}

Every name in environment becomes an environment variable for every step. Put it inside a stage instead, and it applies only there. Jenkins adds its own too:

VariableHolds
BUILD_NUMBERThis build’s number: 1, 2, 3
BRANCH_NAMEThe branch being built, in a multibranch job: main, feature/coupons, PR-42
CHANGE_IDThe pull request’s number, only on a pull request build
GIT_COMMITThe full commit hash being built
WORKSPACEThe folder the build runs in

Quotes: who fills in the $

This trips everyone up once. A Jenkinsfile is Groovy, and Groovy and the shell both use $ for variables.

You writeWho replaces $NAMEUse it for
sh 'echo "$APP_NAME"'The shell, from its environment. Groovy leaves single-quoted text aloneEnvironment variables and secrets. The safe default
echo "Deploying ${params.TARGET}"Groovy, before the step runsGroovy values like params and env, in Jenkins steps like echo
sh "echo ${SECRET}"Groovy, so the secret becomes part of the command textNever. Jenkins warns you, because the secret can leak

parameters: questions asked when the build starts

Jenkinsfile
pipeline {
    agent any
    parameters {
        choice(name: 'TARGET', choices: ['dev', 'stg', 'prod'], description: 'Where to deploy')
        booleanParam(name: 'RUN_SLOW_TESTS', defaultValue: false, description: 'Also run the slow tests')
        string(name: 'RELEASE_NOTE', defaultValue: 'Routine release', description: 'One line about this release')
    }
    stages {
        stage('Show') {
            steps {
                echo "Target: ${params.TARGET}, slow tests: ${params.RUN_SLOW_TESTS}"
            }
        }
    }
}
TypeShows asYou read it as
choiceA drop-down. The first choice is the defaultparams.TARGET
booleanParamA tick boxparams.RUN_SLOW_TESTS, true or false
stringA one-line text boxparams.RELEASE_NOTE

The job’s button changes from Build Now to Build with Parameters, but only after Jenkins has read the Jenkinsfile once. You’ll see that in the lab.

options: rules for the whole run

OptionWhat it doesIf you leave it out
timeout(time: 15, unit: 'MINUTES')Abort the build if it runs longerA hung step can hold an executor forever
timestamps()A time on every log line. Needs the Timestamper pluginYou can’t tell which step was slow
disableConcurrentBuilds()Only one build of this branch at a time; the next waitsTwo deploys of the same thing can run at once and collide
buildDiscarder(logRotator(numToKeepStr: '20'))Keep only the last 20 buildsOld builds pile up until the controller’s disk is full
ansiColor('xterm')Show colour codes as colours. Needs the AnsiColor pluginTools that print colour show codes like [32m
retry(2)Run the whole pipeline again if it failsA failure is final, which is usually what you want
skipDefaultCheckout()Don’t check the code out automaticallyCode is checked out before the first stage

options can also go inside a stage. A timeout there limits only that stage, and if the stage has an input, it limits the wait too.

when: should this stage run?

A stage with a when block runs only if its condition is true. If it’s false, the stage is skipped, which isn’t a failure.

ConditionTrue when
branch 'main'This is a build of the main branch. Also takes patterns like 'release/*'
changeRequest()This is a build of a pull request
changeRequest target: 'main'This is a pull request into main
tag 'v*'This build is for a git tag matching the pattern
expression { params.TARGET == 'prod' }The Groovy code inside returns true
environment name: 'REGION', value: 'us-east-1'That environment variable has that value
not { }, allOf { }, anyOf { }Combine conditions

branch, changeRequest and tag need a multibranch job, because that’s what tells Jenkins which branch or pull request it’s building. In a plain Pipeline job they’re always false. Continuous integration sets up a multibranch job.

Two settings change when the check happens. beforeAgent true checks before an agent is found, so a skipped stage never waits for one. beforeInput true checks before asking for approval, so nobody is asked about a stage that would be skipped anyway.

input: wait for a person

Jenkinsfile
pipeline {
    agent none
    stages {
        stage('Build') {
            agent any
            steps {
                echo 'Building'
            }
        }
        stage('Deploy to prod') {
            options {
                timeout(time: 30, unit: 'MINUTES')
            }
            input {
                message 'Deploy to production?'
                ok 'Deploy'
                submitter 'release-managers'
            }
            agent any
            steps {
                echo 'Deploying'
            }
        }
    }
}
LineWhat it means
agent noneNo executor for the pipeline as a whole
agent any in BuildThis stage takes an executor, and gives it back when it ends
options → timeout(time: 30, …)If nobody answers in 30 minutes, the build is aborted
input → messageThe question shown on the build page
ok 'Deploy'The label on the button that says yes. There’s always an Abort button too
submitter 'release-managers'Only these users or groups may answer. Leave it out and anyone who can run the job can
agent any in Deploy to prodThe executor is taken only after the approval

Written this way, a build waiting for approval over a weekend holds no executor. With agent any at the top, it would hold one the whole time.

parallel: stages side by side

Jenkinsfile
pipeline {
    agent any
    stages {
        stage('Checks') {
            failFast true
            parallel {
                stage('Unit tests') {
                    steps {
                        sh 'echo "Unit tests passed"'
                    }
                }
                stage('Lint') {
                    steps {
                        sh 'echo "Lint passed"'
                    }
                }
            }
        }
    }
}

parallel runs the stages inside it at the same time, each with its own executor if it has its own agent. failFast true stops the others as soon as one fails, so you don’t wait for work that no longer matters.

post: what happens afterwards

post runs after the stages, whatever happened. Each block inside runs only if its condition matches.

BlockRuns when
alwaysEvery time. Save test reports here
successThe build passed
failureThe build failed. Tell the team here
unstableTests failed but the build carried on. junit marks a build unstable when tests fail
abortedSomeone, or a timeout, stopped it
changedThe result is different from the last build’s
fixedThis build passed, and the last one didn’t
cleanupLast of all, after every other post block. Delete the workspace here

They run in a fixed order: always first, then the ones that match the result, then cleanup. post can also go inside a stage, for things that belong to that stage.

Part 3: See it

Below is a short Jenkinsfile. Pick what the build is for, then step through it. Try a pull request, then main, then make the tests fail.

This build is for
Tests
pipeline {
  agent any
  stages {
    stage('Test') {
      steps { sh './test.sh' }
    }
    stage('Plan') {
      when { changeRequest() }
      steps { sh 'terraform plan' }
    }
    stage('Build image') {
      when { branch 'main' }
      steps { sh './build-image.sh' }
    }
    stage('Deploy to prod') {
      when { branch 'main' }
      input { message 'Deploy to prod?' }
      steps { sh './deploy.sh prod' }
    }
  }
  post {
    always  { junit 'reports/*.xml' }
    success { echo 'Shipped' }
    failure { echo 'Tell the team' }
    cleanup { deleteDir() }
  }
}

BRANCH_NAME = feature/coupons

Stages

  1. Testnot yet
  2. Plannot yet
  3. Build imagenot yet
  4. Deploy to prodnot yet

post

  1. alwaysnot yet
  2. successnot yet
  3. failurenot yet
  4. cleanupnot yet
ReadyA build of feature/coupons is about to start. Choose Run or Next step.

What to notice: the same file gives three different pipelines. A feature branch only tests. A pull request also plans. Only main builds and deploys. And when a stage fails, the stages after it are skipped, but post still runs.

Part 4: Do it

Free — Jenkins runs on your computer. Nothing in AWS is used.

You’ll run Jenkins on your computer, create a pipeline, and see parameters, when, parallel, input, a timeout and post do their jobs. About 40 minutes. The next three lessons use this same Jenkins, so keep it when you finish.

On your computer, Jenkins’ controller and its only agent are the same machine, and builds run on the controller. That’s fine for learning on your own. Jenkins from zero explains why a shared Jenkins must never work this way.

  1. Install and start Jenkins

    Terminal
    brew install jenkins-lts
    PartWhat it means
    brew installinstall software with Homebrew
    jenkins-ltsthe long-term support version of Jenkins. Homebrew installs the Java it needs too

    Start it in a terminal window of its own:

    Terminal
    jenkins-lts --httpListenAddress=127.0.0.1 --httpPort=8080
    PartWhat it means
    jenkins-ltsstart Jenkins in this terminal. It keeps running until you press Ctrl+C
    --httpListenAddress=127.0.0.1only accept connections from this computer, not from other machines on your network
    --httpPort=8080the port for the web page

    Among many lines of log, the first start prints:

    You should see
    *************************************************************
    *************************************************************
    *************************************************************
    
    Jenkins initial setup is required. An admin user has been created and a password generated.
    Please use the following password to proceed to installation:
    
    3b9e5c07a1d24f6e8c2b0d9a7f41e635
    
    This may also be found at: /home/you/.jenkins/secrets/initialAdminPassword
    
    *************************************************************
    *************************************************************
    *************************************************************

    Then, when it’s ready:

    You should see
    Jenkins is fully up and running

    Your password and home folder are different. Copy the password. Leave this terminal open; closing it stops Jenkins. Jenkins keeps everything in ~/.jenkins, the JENKINS_HOME on your computer.

  2. Finish the setup

    Open http://localhost:8080 and go through the setup pages:

    PageDo this
    Unlock JenkinsPaste the password. Choose Continue
    Customize JenkinsInstall suggested plugins. They include Pipeline, Git, Credentials Binding and Timestamper
    Create First Admin UserA username and a password you’ll remember
    Instance ConfigurationLeave http://localhost:8080/. Choose Save and Finish, then Start using Jenkins
  3. Create the pipeline

    Choose New Item.

    FieldEnter
    Enter an item namequickcart-pipeline
    TypePipeline

    Choose OK, scroll to Pipeline, leave Definition as Pipeline script, and paste this into Script:

    Jenkinsfile
    pipeline {
        agent any
    
        parameters {
            choice(name: 'TARGET', choices: ['dev', 'stg', 'prod'], description: 'Where to deploy')
            booleanParam(name: 'RUN_SLOW_TESTS', defaultValue: false, description: 'Also run the slow tests')
            string(name: 'RELEASE_NOTE', defaultValue: 'Routine release', description: 'One line about this release')
        }
    
        environment {
            APP_NAME = 'quickcart-orders'
            VERSION  = "1.0.${env.BUILD_NUMBER}"
        }
    
        options {
            timeout(time: 15, unit: 'MINUTES')
            timestamps()
            disableConcurrentBuilds()
            buildDiscarder(logRotator(numToKeepStr: '20'))
        }
    
        stages {
            stage('Build') {
                steps {
                    echo "Building ${env.APP_NAME} version ${env.VERSION}"
                    writeFile file: 'out/version.txt', text: "${env.APP_NAME} ${env.VERSION}\n"
                }
            }
    
            stage('Tests') {
                parallel {
                    stage('Unit tests') {
                        steps {
                            sh 'echo "Unit tests passed for $APP_NAME"'
                        }
                    }
                    stage('Slow tests') {
                        when {
                            expression { params.RUN_SLOW_TESTS }
                        }
                        steps {
                            sh 'sleep 5'
                            echo 'Slow tests passed'
                        }
                    }
                }
            }
    
            stage('Only on main') {
                when {
                    branch 'main'
                }
                steps {
                    echo 'This only runs in a multibranch job, on main'
                }
            }
    
            stage('Deploy') {
                when {
                    expression { params.TARGET != 'prod' }
                }
                steps {
                    echo "Deploying ${env.VERSION} to ${params.TARGET}"
                }
            }
    
            stage('Deploy to prod') {
                when {
                    beforeInput true
                    expression { params.TARGET == 'prod' }
                }
                options {
                    timeout(time: 5, unit: 'MINUTES')
                }
                input {
                    message 'Deploy to production?'
                    ok 'Deploy'
                }
                steps {
                    echo "Deploying ${env.VERSION} to prod: ${params.RELEASE_NOTE}"
                }
            }
        }
    
        post {
            always {
                archiveArtifacts artifacts: 'out/version.txt'
            }
            success {
                echo 'The pipeline passed'
            }
            failure {
                echo 'The pipeline failed: tell the team'
            }
            cleanup {
                deleteDir()
            }
        }
    }

    Every directive is explained in the idea. The parts that are new here:

    LineWhat it means
    VERSION = "1.0.${env.BUILD_NUMBER}"Double quotes, so Groovy fills in the build number: build 3 is version 1.0.3
    writeFile file: 'out/version.txt', …Make a small file, standing in for what a real build produces
    stage('Tests') → parallelUnit tests and slow tests run side by side
    expression { params.RUN_SLOW_TESTS }The slow tests run only if the box was ticked
    stage('Only on main')Here to show that branch is always false in a plain Pipeline job
    expression { params.TARGET != 'prod' }dev and stg deploy straight away
    beforeInput trueCheck the condition before asking, so dev builds never ask for approval
    Stage options → timeout(time: 5, …)Nobody answers in 5 minutes, the build is aborted
    archiveArtifacts in post → alwaysKeep version.txt, even if a later stage failed
    deleteDir() in post → cleanupEmpty the workspace at the very end

    Choose Save.

  4. Run it once, and meet the parameters

    Choose Build Now. When it finishes, open build #1 and choose Console Output. Among the lines you’ll see:

    The console output includes
    Building quickcart-orders version 1.0.1
    Unit tests passed for quickcart-orders
    Stage "Slow tests" skipped due to when conditional
    Stage "Only on main" skipped due to when conditional
    Deploying 1.0.1 to dev
    The pipeline passed
    Finished: SUCCESS

    Every line also starts with a time, from timestamps(). Go back to the job page.

    You should see

    The button now says Build with Parameters. Before build #1, Jenkins didn’t know the Jenkinsfile had parameters, because it hadn’t read it yet.

  5. Deploy to prod, with approval

    Choose Build with Parameters:

    FieldEnter
    TARGETprod
    RUN_SLOW_TESTSticked
    RELEASE_NOTECoupons at checkout

    Choose Build. Open the running build’s Console Output.

    You should see

    The slow tests run next to the unit tests this time, and Deploy is skipped. Then the build stops and waits, with the question Deploy to production? and two links: Deploy and Abort.

    Choose Deploy.

    The console output includes
    Deploying 1.0.2 to prod: Coupons at checkout
    The pipeline passed
    Finished: SUCCESS

    Run it once more with prod, and this time don’t answer. After 5 minutes:

    The console output ends with
    Cancelling nested steps due to timeout
    Finished: ABORTED

    Nobody forgot a build that waits forever: the timeout ended it for you.

  6. Make a stage fail

    Choose Configure, and change the Unit tests step so it fails:

    Jenkinsfile
    pipeline {
        agent any
        stages {
            stage('Unit tests') {
                steps {
                    sh 'echo "1 test failed" && exit 1'
                }
            }
            stage('Deploy') {
                steps {
                    echo 'Deploying'
                }
            }
        }
        post {
            success {
                echo 'The pipeline passed'
            }
            failure {
                echo 'The pipeline failed: tell the team'
            }
        }
    }

    This shorter pipeline replaces the long one, so the failure is easy to see.

    PartWhat it means
    echo "1 test failed"print a line, as a test tool would
    &&run the next command only if this one succeeded
    exit 1end the shell with code 1, which means failure. Jenkins fails the stage

    Choose Save, then Build Now.

    The console output includes
    1 test failed
    Stage "Deploy" skipped due to earlier failure(s)
    The pipeline failed: tell the team
    ERROR: script returned exit code 1
    Finished: FAILURE

    Deploy never ran. failure did; success didn’t.

  7. Clean-up step: Clean up

    Keep Jenkins and ~/.jenkins: the next three lessons use them. To stop Jenkins for now, press Ctrl+C in its terminal. Start it again later with the same jenkins-lts command; your jobs and settings are still there.

    When you’ve finished the course and want it gone:

    Terminal
    brew uninstall jenkins-lts
    PartWhat it means
    brew uninstallremove software Homebrew installed
    jenkins-ltsJenkins. Your ~/.jenkins folder stays until you delete it

    Then delete the .jenkins folder in your home folder, which holds every job and setting:

    Terminal
    rm -rf ~/.jenkins
    PartWhat it means
    rmremove files
    -rfthe whole folder and everything inside it, without asking about each file. There’s no undo, so check the name before pressing Enter
    ~/.jenkinsthe .jenkins folder in your home folder. The dot at the start makes it hidden

Part 5: Check yourself

A stage has when { branch 'main' }. In QuickCart's multibranch job, which builds run it?

The Unit tests stage fails. What happens to the stages after it, and to post?

A pipeline has agent any at the top and an input step that waits for approval over a weekend. What's the problem?

You add a parameters block to a Jenkinsfile and choose Build Now. The build ignores your choice of TARGET. Why?

Which line prints the value of the environment variable APP_NAME correctly and safely inside sh?

Part 6: Cheat sheet

On one card
DirectiveRemember
pipeline { }Declarative. Checked before it runs
agentany, none, { label 'x' }, { docker { image 'x' } }. Per stage too
stages / stage / stepsTop to bottom. The first failure skips the rest
environmentVariables for every step. Read them in sh with single quotes
parameterschoice, booleanParam, string. Read as params.NAME. Active after one run
optionstimeout, timestamps(), disableConcurrentBuilds(), buildDiscarder(logRotator(numToKeepStr: '20'))
whenbranch, changeRequest(), tag, expression { }. beforeAgent, beforeInput
inputmessage, ok, submitter. Put a timeout on the stage. Use agent none at the top
parallelSide by side. failFast true
postalways, success, failure, unstable, aborted, changed, fixed, then cleanup last
Quotes'…' the shell fills in $VAR. "…" Groovy fills in ${…}. Secrets: single quotes only
ReplayTry an edited pipeline once, without saving it

Finished the build and the quiz? Mark it done to fill in its stop on the route map.