Jenkins · lesson 4 of 5
Jenkins Shared Libraries
Eliminate copy-pasted Jenkinsfiles by extracting common pipeline logic into a Shared Library—versioned, tested, and available to all team pipelines.
By SkillByExample · Updated
As your team grows, you end up copy-pasting the same Jenkinsfile stages across dozens of repos.
Shared Libraries let you write pipeline steps once, version them, and use them everywhere.
Learning outcomes
By the end you can:
- create a Shared Library repository with the correct structure
- write and use global vars (steps)
- load a library in a Jenkinsfile
- use implicit and explicit library loading
1) Why Shared Libraries?
Without shared libraries:
- Every repo has its own Jenkinsfile with duplicated Docker build/push logic
- Fixing a bug means updating every repo
- No central version control or testing for CI logic
With shared libraries:
- One library repo contains reusable pipeline steps
- Pipelines import the library—one fix propagates everywhere
- The library itself is versioned, testable, and code-reviewed
2) Shared Library directory structure
jenkins-shared-library/
├── vars/
│ ├── buildDockerImage.groovy # global step: buildDockerImage(...)
│ ├── deployToKubernetes.groovy # global step: deployToKubernetes(...)
│ └── notifySlack.groovy # global step: notifySlack(...)
├── src/
│ └── org/
│ └── example/
│ └── PipelineUtils.groovy # Groovy class
├── resources/
│ └── org/example/
│ └── deploy.sh # scripts or templates
└── README.md
3) Writing a global step (vars/)
Each file in vars/ defines a call() method that becomes the step name.
// vars/buildDockerImage.groovy
def call(Map config = [:]) {
// Provide defaults
def registry = config.registry ?: 'registry.example.com'
def imageName = config.imageName ?: error('imageName is required')
def tag = config.tag ?: env.BUILD_NUMBER
def fullImage = "${registry}/${imageName}:${tag}"
echo "Building Docker image: ${fullImage}"
sh "docker build -t ${fullImage} ."
sh "docker push ${fullImage}"
return fullImage
}
// vars/notifySlack.groovy
def call(String message, String color = 'good') {
slackSend(
channel: '#deployments',
color: color,
message: "${message} — ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
)
}
4) Register the library in Jenkins
- Go to Manage Jenkins → Configure System → Global Pipeline Libraries
- Click Add:
- Name:
my-shared-lib - Default version:
main - Retrieval method: Modern SCM → GitHub → your repo URL
- Name:
- Tick Load implicitly (optional, auto-loads in all pipelines) or leave unchecked (explicit
@Library)
5) Using the library in a Jenkinsfile
Explicit loading
@Library('my-shared-lib@main') _ // underscore imports all vars/
pipeline {
agent any
stages {
stage('Build Image') {
steps {
script {
def image = buildDockerImage(
registry: 'registry.example.com',
imageName: 'myapp',
tag: env.BUILD_NUMBER
)
env.DOCKER_IMAGE = image
}
}
}
stage('Deploy') {
when { branch 'main' }
steps {
deployToKubernetes(
image: env.DOCKER_IMAGE,
namespace: 'production',
deployment: 'myapp'
)
}
}
}
post {
success { notifySlack("✅ Deploy succeeded") }
failure { notifySlack("❌ Deploy failed", 'danger') }
}
}
6) Kubernetes deploy step example
// vars/deployToKubernetes.groovy
def call(Map config = [:]) {
def image = config.image ?: error('image is required')
def namespace = config.namespace ?: 'default'
def deployment = config.deployment ?: error('deployment is required')
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
sh """
kubectl set image deployment/${deployment} \
${deployment}=${image} \
--namespace=${namespace}
kubectl rollout status deployment/${deployment} \
--namespace=${namespace} \
--timeout=120s
"""
}
}
7) Versioning the library
Pin to a specific git tag for stability:
@Library('my-shared-lib@v1.2.0') _
Or always use latest from main (risky for production):
@Library('my-shared-lib@main') _
8) Testing Shared Library code
Use jenkins-spock or plain Groovy unit tests.
Minimum: write a test playbook pipeline that calls each step with representative inputs and verify it runs end-to-end in a test Jenkins instance.
Practice
These solutions ran on a jenkins/jenkins:lts container with the Pipeline and Git plugins.
The library is a local git repository at file:///repos/lib, registered as
my-shared-lib with default version main and retrieval method Modern SCM → Git. Reading
a local file:// repository needs Jenkins started with
-Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true; a hosted repo does not. Each
Jenkinsfile is a Pipeline job’s pasted script. Library loading adds about 30 lines of git
output to each log, so the logs below are filtered with
F='^(Loading library|Checking out|Commit message|Building|\[v2\]|\+ |returned|ERROR|Stage |Finished)'.
-
Create the library repo with
vars/buildDockerImage.groovyfrom section 3. Change its twoshlines tosh "echo docker build -t ${fullImage} ."andsh "echo docker push ${fullImage}", so no Docker daemon is needed. Also add this Groovy class atsrc/org/example/PipelineUtils.groovy, which task 4 uses:package org.example class PipelineUtils { static String imageTag(String version, String buildNumber) { return "${version}-b${buildNumber}" } }Commit both files, tag that commit
v1.0.0, and call the step with onlyimageName: 'myapp', printing what it returns.Solution
@Library('my-shared-lib@v1.0.0') _ pipeline { agent any stages { stage('Build Image') { steps { script { def image = buildDockerImage(imageName: 'myapp') echo "returned: ${image}" } } } } }$ curl -s http://localhost:8080/job/lib-pinned/1/consoleText | grep -E "$F" Loading library my-shared-lib@v1.0.0 Checking out Revision 0c7f8ae8bd126a4632f4d98c62e4181b86bfd850 (v1.0.0) Commit message: "buildDockerImage v1" Building Docker image: registry.example.com/myapp:1 + echo docker build -t registry.example.com/myapp:1 . + echo docker push registry.example.com/myapp:1 returned: registry.example.com/myapp:1 Finished: SUCCESSThe library is checked out before the pipeline starts. The two missing arguments fell back to their
?:defaults: the default registry, andenv.BUILD_NUMBER(1) as the tag.return fullImagehands the string back to the callingscriptblock. -
Call the step with
registry: 'ghcr.io/acme'but noimageName, and add aDeploystage after it withsteps { echo "deploying" }.Solution
$ curl -s http://localhost:8080/job/lib-noname/1/consoleText | grep -E "$F" Loading library my-shared-lib@v1.0.0 Checking out Revision 0c7f8ae8bd126a4632f4d98c62e4181b86bfd850 (v1.0.0) Commit message: "buildDockerImage v1" Stage "Deploy" skipped due to earlier failure(s) ERROR: imageName is required Finished: FAILUREconfig.imageName ?: error(...)only evaluates the right-hand side when the left is missing, anderrorfails the build there. Nothing after it in the step ran, not even the “Building Docker image” echo, and Deploy was skipped. A required argument checked at the top of a step fails before any side effects. -
On
main, after the tag, change the step’s log line toecho "[v2] Building Docker image: ${fullImage}"and commit. Build task 1’s job (lib-pinned) again, unchanged, then run the same Jenkinsfile with@Library('my-shared-lib@main') _as a new job,lib-main.Solution
$ curl -s http://localhost:8080/job/lib-pinned/2/consoleText | grep -E "$F" Loading library my-shared-lib@v1.0.0 Checking out Revision 0c7f8ae8bd126a4632f4d98c62e4181b86bfd850 (v1.0.0) Commit message: "buildDockerImage v1" Building Docker image: registry.example.com/myapp:2 + echo docker build -t registry.example.com/myapp:2 . + echo docker push registry.example.com/myapp:2 returned: registry.example.com/myapp:2 Finished: SUCCESS $ curl -s http://localhost:8080/job/lib-main/1/consoleText | grep -E "$F" Loading library my-shared-lib@main Checking out Revision ff7e6c92d6924b4d1d2b88ab2fdadf6fcec466c9 (main) Commit message: "buildDockerImage: prefix log with [v2]" [v2] Building Docker image: registry.example.com/myapp:1 + echo docker build -t registry.example.com/myapp:1 . + echo docker push registry.example.com/myapp:1 returned: registry.example.com/myapp:1 Finished: SUCCESSBoth builds ran after the
[v2]commit existed, butlib-pinned#2 still checked out the tagged revision. Only its build number changed, so the default tag is now2. Pinning means a library change reaches a pipeline only when that pipeline’s@Libraryline is bumped.@mainpicks up every push immediately. -
Still pinned to
v1.0.0, import thePipelineUtilsclass you committed in task 1. Use it to build thetagyou pass tobuildDockerImage.Solution
@Library('my-shared-lib@v1.0.0') _ import org.example.PipelineUtils pipeline { agent any stages { stage('Build Image') { steps { script { def tag = PipelineUtils.imageTag('2.4.0', env.BUILD_NUMBER) def image = buildDockerImage(imageName: 'myapp', tag: tag) echo "returned: ${image}" } } } } }$ curl -s http://localhost:8080/job/lib-src/1/consoleText | grep -E "$F" Loading library my-shared-lib@v1.0.0 Checking out Revision 0c7f8ae8bd126a4632f4d98c62e4181b86bfd850 (v1.0.0) Commit message: "buildDockerImage v1" Building Docker image: registry.example.com/myapp:2.4.0-b1 + echo docker build -t registry.example.com/myapp:2.4.0-b1 . + echo docker push registry.example.com/myapp:2.4.0-b1 returned: registry.example.com/myapp:2.4.0-b1 Finished: SUCCESSvars/steps are available as soon as the library loads. Asrc/class must be imported by its package name, exactly like a normal Groovy class. Both came from the same@Libraryline and the same tagged revision. -
Delete the
_after@Library('my-shared-lib@v1.0.0')in task 1’s Jenkinsfile and run it.Solution
$ curl -s http://localhost:8080/job/lib-nounderscore/1/consoleText | head -7 Started from command line by anonymous org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: WorkflowScript: 3: unexpected token: pipeline @ line 3, column 1. pipeline { ^ 1 errorAn annotation must be attached to something, and
_is the placeholder it attaches to. Without it Groovy can’t parse the file, so the build fails at compile time, before any library is even loaded (there is no “Loading library” line).
Next steps
- Pair with Ansible: trigger playbooks from shared library steps
- Jenkins Configuration as Code (JCasC) for managing Jenkins itself via YAML
- GitOps pattern: replace Jenkins pushes with ArgoCD pull-based deploys