Skip to main content
Jenkins · lesson 1 of 5
Jenkins beginner Lesson 1 of 5

Also part of: DevOps

Jenkins CI: Pipeline Basics

Learn what Jenkins is and how to use declarative pipelines: stages, agents, steps, and basic CI patterns.

By SkillByExample · Updated

Jenkins is a CI server that automates build/test/release workflows. Even if you use it less today, understanding Jenkins pipeline concepts helps you reason about CI systems generally.

Theory first: pipelines as executable delivery design

A CI pipeline is an executable model of your delivery process. Each stage encodes a quality boundary (build, test, security, deploy), and failures represent policy violations—not just broken scripts.

This framing keeps Jenkins practical: you are designing trustworthy flow control for change, where feedback speed and reproducibility matter as much as successful builds.

Learning outcomes

By the end you can:

  • understand Jenkins pipeline stages
  • run commands in steps
  • use agents/nodes to control execution environment

1) Pipeline overview

A Jenkins pipeline is a script describing what to run and when. Today, declarative pipelines are most common.

2) Minimal declarative pipeline

Jenkinsfile:

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }

    stage('Install') {
      steps {
        sh 'npm ci'
      }
    }

    stage('Test') {
      steps {
        sh 'npm test'
      }
    }
  }
}

3) Common stage patterns

Lint + test

stage('Lint') {
  steps { sh 'npm run lint' }
}

stage('Test') {
  steps { sh 'npm test' }
}

Build + archive

stage('Build') {
  steps {
    sh 'npm run build'
    archiveArtifacts artifacts: 'dist/**', fingerprint: true
  }
}

4) Choosing an execution environment

Declarative pipeline:

agent {
  docker {
    image 'node:20-alpine'
    args '-u node'
  }
}

Or run on specific labeled agents:

agent { label 'linux-x64' }

5) Secrets and credentials (conceptual)

Jenkins uses a credentials store so you don’t hardcode secrets in the Jenkinsfile. Common usage pattern is withCredentials.

Example (concept):

withCredentials([string(credentialsId: 'API_TOKEN', variable: 'API_TOKEN')]) {
  sh 'curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com'
}

Practice

These solutions ran on a jenkins/jenkins:lts container with the Pipeline plugins installed and a secret-text credential with the ID API_TOKEN. Each task is a Pipeline job (New Item → Pipeline) with the Jenkinsfile pasted into its Script box. Each job was started with jenkins-cli.jar, whose build -s -v waits for the build and streams its console log. Lines starting [Pipeline] are Jenkins tracing each step it runs.

  1. Paste the “Minimal declarative pipeline” exactly as written into a new Pipeline job called minimal and run it. Which stage fails, and why?

    Solution
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build minimal -s -v | grep -v '^\[Pipeline\]'
    Started minimal #1
    Started from command line by anonymous
    Running on Jenkins in /var/jenkins_home/workspace/minimal
    Stage "Install" skipped due to earlier failure(s)
    Stage "Test" skipped due to earlier failure(s)
    ERROR: ‘checkout scm’ is only available when using “Multibranch Pipeline” or “Pipeline script from SCM”
    Finished: FAILURE
    Completed minimal #1 : FAILURE

    checkout scm means “check out the repository this Jenkinsfile came from”. A pasted script didn’t come from a repository, so there is nothing to check out. In a real project the Jenkinsfile lives in the repo and the job is “Pipeline script from SCM”. For the remaining tasks, leave the Checkout stage out.

  2. Build a two-stage pipeline from the “Lint + test” pattern, using sh commands that need no project. Make Lint fail with sh 'echo "lint: 1 problem found"; exit 1' and Test pass with sh 'echo "tests passed"'. Does Test run?

    Solution
    pipeline {
      agent any
    
      stages {
        stage('Lint') {
          steps { sh 'echo "lint: 1 problem found"; exit 1' }
        }
    
        stage('Test') {
          steps { sh 'echo "tests passed"' }
        }
      }
    }
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build lint-fails -s -v
    Started lint-fails #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/lint-fails
    [Pipeline] {
    [Pipeline] stage
    [Pipeline] { (Lint)
    [Pipeline] sh
    + echo lint: 1 problem found
    lint: 1 problem found
    + exit 1
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] stage
    [Pipeline] { (Test)
    Stage "Test" skipped due to earlier failure(s)
    [Pipeline] getContext
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] End of Pipeline
    ERROR: script returned exit code 1
    Finished: FAILURE
    Completed lint-fails #1 : FAILURE

    A non-zero exit from sh fails the stage, and every later stage is skipped. The order of stages is a policy: a lint failure here means the tests never get a turn. “tests passed” never prints.

  3. Use the “Build + archive” pattern with sh 'mkdir -p dist && echo "app v1" > dist/app.txt && echo "body{}" > dist/app.css' in place of npm run build. Check what got archived. Then change the mkdir and redirect to write into build/ instead of dist/, keep the archiveArtifacts line unchanged, and run it again.

    Solution
    pipeline {
      agent any
    
      stages {
        stage('Build') {
          steps {
            sh 'mkdir -p dist && echo "app v1" > dist/app.txt && echo "body{}" > dist/app.css'
            archiveArtifacts artifacts: 'dist/**', fingerprint: true
          }
        }
      }
    }
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build build-archive -s -v
    Started build-archive #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/build-archive
    [Pipeline] {
    [Pipeline] stage
    [Pipeline] { (Build)
    [Pipeline] sh
    + mkdir -p dist
    + echo app v1
    + echo body{}
    [Pipeline] archiveArtifacts
    Archiving artifacts
    Recording fingerprints
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] End of Pipeline
    Finished: SUCCESS
    Completed build-archive #1 : SUCCESS
    $ curl -sg "http://localhost:8080/job/build-archive/1/api/json?tree=artifacts[relativePath]"
    {"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","artifacts":[{"relativePath":"dist/app.css"},{"relativePath":"dist/app.txt"}]}

    With the build writing to build/ instead (sh 'mkdir -p build && echo "app v1" > build/app.txt'):

    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build archive-miss -s -v
    Started archive-miss #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/archive-miss
    [Pipeline] {
    [Pipeline] stage
    [Pipeline] { (Build)
    [Pipeline] sh
    + mkdir -p build
    + echo app v1
    [Pipeline] archiveArtifacts
    Archiving artifacts
    ‘dist/**’ doesn’t match anything, but ‘**’ does. Perhaps that’s what you mean?
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] End of Pipeline
    ERROR: No artifacts found that match the file pattern "dist/**". Configuration error?
    Finished: FAILURE
    Completed archive-miss #1 : FAILURE

    Archiving nothing is treated as a broken build, not an empty success. That catches a build script that quietly stopped producing output.

  4. Change the pipeline’s agent to agent { label 'windows' } on a Jenkins that has no node with that label. Start it without -s, read the console after a few seconds, and then stop the build.

    Solution
    $ curl -s http://localhost:8080/job/label-windows/1/consoleText
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Still waiting to schedule task
    ‘Jenkins’ doesn’t have label ‘windows’
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ stop-builds label-windows
    Build '#1' stopped for job 'label-windows'
    $ curl -s http://localhost:8080/job/label-windows/1/consoleText | tail -2
    org.jenkinsci.plugins.workflow.actions.ErrorAction$ErrorId: 0b047bd2-4362-4657-9a09-2bda2a04e076
    Finished: ABORTED

    An unmatched label is not an error. The build simply waits in the queue for a node that may never come. A typo in a label shows up as a build stuck on “Still waiting to schedule task”, not as a red failure.

  5. Put the withCredentials example in a stage. Replace the curl with sh 'echo "single quotes: token is $API_TOKEN"'. Add a second step that uses Groovy interpolation: sh "echo \"double quotes: token is ${API_TOKEN}\"".

    Solution
    pipeline {
      agent any
    
      stages {
        stage('Call API') {
          steps {
            withCredentials([string(credentialsId: 'API_TOKEN', variable: 'API_TOKEN')]) {
              sh 'echo "single quotes: token is $API_TOKEN"'
              sh "echo \"double quotes: token is ${API_TOKEN}\""
            }
          }
        }
      }
    }
    $ java -jar jenkins-cli.jar -s http://localhost:8080/ build call-api -s -v
    Started call-api #1
    Started from command line by anonymous
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Running on Jenkins in /var/jenkins_home/workspace/call-api
    [Pipeline] {
    [Pipeline] stage
    [Pipeline] { (Call API)
    [Pipeline] withCredentials
    Masking supported pattern matches of $API_TOKEN
    [Pipeline] {
    [Pipeline] sh
    + echo single quotes: token is ****
    single quotes: token is ****
    [Pipeline] sh
    Warning: A secret was passed to "sh" using Groovy String interpolation, which is insecure.
    		 Affected argument(s) used the following variable(s): [API_TOKEN]
    		 See https://jenkins.io/redirect/groovy-string-interpolation for details.
    + echo double quotes: token is ****
    double quotes: token is ****
    [Pipeline] }
    [Pipeline] // withCredentials
    [Pipeline] }
    [Pipeline] // stage
    [Pipeline] }
    [Pipeline] // node
    [Pipeline] End of Pipeline
    Finished: SUCCESS
    Completed call-api #1 : SUCCESS

    Both lines are masked in the log, but only the first is safe. With single quotes the shell reads $API_TOKEN from its environment. With double quotes Groovy pastes the secret into the command text before sh runs, and Jenkins warns about exactly that.

Next steps

Next tutorials:

  • Jenkins credentials + artifacts
  • Multi-stage releases
  • Pair with monitoring: alerts on pipeline failures

Frequently Asked Questions

Is Jenkins obsolete now that GitHub Actions exists?
No—Jenkins is still common in enterprises with existing infrastructure, custom agents, and plugin ecosystems. The core CI/pipeline concepts transfer.
What’s the difference between an agent and a node?
In Jenkins Pipeline, an agent (declarative) chooses where stages run. Under the hood it maps to nodes/executors.