When to Use a Jenkins Shared Library

A Jenkins shared library solves a specific organizational problem: pipeline code that has been copied between repositories until no one is sure which version is canonical. The moment you find yourself editing the same Docker build block in four different Jenkinsfiles after a registry URL change, the shared library pattern becomes the right tool.
The Jenkins shared library mechanism is purpose-built for this situation. Rather than treating pipeline logic as application-specific configuration, it promotes that logic to a versioned, testable artifact consumed by every team. The value compounds quickly — a security fix to a credential-handling step lands everywhere on the next pipeline run, not after a ticket-driven copy-paste sprint across thirty repositories.
Concrete scenarios where this pays off:
- Multi-team Docker build pipelines sharing identical build, tag, and push conventions but different registries or credential IDs.
- Deployment pipelines that follow the same Helm or kubectl pattern but target different clusters per environment.
- Compliance gates — SAST scans, license checks, or approval stages that must be consistent across all service pipelines by policy.
- Onboarding new services where a team should write a minimal Jenkinsfile and inherit all CI/CD behavior without re-inventing it.
If your Jenkins instance serves a single team with two or three pipelines that rarely change, the overhead of a separate library repository is not justified. Once you cross roughly five repositories with shared pipeline patterns, the library structure starts returning value immediately.
Setting Up the Library Repository
The library lives in its own Git repository. Jenkins imposes a specific directory layout at the root — deviating from it produces silent failures that are frustrating to debug.
Required structure:
my-shared-lib/
├── vars/
│ ├── buildAndPush.groovy
│ └── deployHelm.groovy
├── src/
│ └── org/
│ └── example/
│ └── Utils.groovy
├── resources/
│ └── scripts/
│ └── deploy.sh
└── README.md
Each directory has a distinct role:
vars/— Global variables and callable pipeline steps. Each.groovyfile here becomes a step name in any consuming Jenkinsfile. Files must use camelCase naming (e.g.,buildAndPush.groovy).src/— Helper classes following standard Java package conventions. These are not auto-imported; consuming code must explicitly import them.resources/— Non-Groovy assets: shell scripts, JSON templates, configuration stubs. Loaded at runtime vialibraryResource().
Once the repository exists and is pushed to your Git host, register it in Jenkins under Manage Jenkins → Configure System → Global Pipeline Libraries. Key fields to set:
- Name — the identifier used in
@Library()annotations (e.g.,kuryzhev-shared-lib). - Default version — a branch name, tag, or commit SHA. This is the fallback when a Jenkinsfile does not pin a version explicitly.
- Retrieval method — Modern SCM with your Git credentials configured.
- implicit — Setting this to
trueauto-loads the library into every pipeline without requiring an explicit@Libraryannotation. Useful for compliance steps that must always be present; risky if you want teams to opt in deliberately.
For detailed Jenkins configuration options, refer to the official Jenkins Shared Libraries documentation.
Configuration: Defining Steps and Helper Classes
The most important convention in vars/ is the call() method. When Jenkins resolves a step name from a vars/ file, it invokes call() on that object. Defining any other method name as your entry point and omitting call() is one of the most common setup errors — the step appears to load but does nothing when invoked.
Below is the complete buildAndPush.groovy step used across our Docker pipelines. It accepts a configuration map, validates required keys, and returns the full image reference so downstream stages can reference it without re-constructing the string.
// vars/buildAndPush.groovy
// Reusable step: build Docker image and push to registry
def call(Map config = [:]) {
// Required config keys: imageName, registry, credentialsId
// Optional: dockerfile (default 'Dockerfile'), tag (default BUILD_NUMBER)
def imageName = config.imageName ?: error('imageName is required')
def registry = config.registry ?: error('registry is required')
def credId = config.credentialsId ?: error('credentialsId is required')
def dockerfile = config.dockerfile ?: 'Dockerfile'
def imageTag = config.tag ?: env.BUILD_NUMBER
def fullImage = "${registry}/${imageName}:${imageTag}"
stage('Build Docker Image') {
sh """
docker build \
-f ${dockerfile} \
-t ${fullImage} \
.
"""
}
stage('Push Docker Image') {
withCredentials([usernamePassword(
credentialsId: credId,
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'
)]) {
sh """
echo "\$REG_PASS" | docker login ${registry} \
-u "\$REG_USER" --password-stdin
docker push ${fullImage}
docker logout ${registry}
"""
}
}
// Return full image reference so downstream stages can consume it
return fullImage
}
For logic that does not belong in a pipeline step — utility parsing, HTTP calls, structured data transformations — place it in src/ as a proper Groovy class. The package path must match the directory path exactly.
// src/org/example/Utils.groovy
package org.example
class Utils implements Serializable {
static String buildImageTag(String branchName, String buildNumber) {
// Normalize branch name: replace slashes with dashes
def sanitized = branchName.replaceAll('/', '-').toLowerCase()
return "${sanitized}-${buildNumber}"
}
}
Note the implements Serializable declaration — this is not optional for classes used inside CPS-transformed pipeline code. We will return to why in the common mistakes section.
For shell scripts or templates stored in resources/, load them with libraryResource and write the content to disk before executing. The method returns a plain string — it does not create a file automatically.
// vars/runDeployScript.groovy
def call(String environment) {
def scriptContent = libraryResource('scripts/deploy.sh')
writeFile file: 'deploy.sh', text: scriptContent
sh "chmod +x deploy.sh && ./deploy.sh ${environment}"
}
End-to-End Examples
Theory is only useful when you can see it wired together. The following two Jenkinsfiles represent the two most common consumption patterns: a CI pipeline that builds and pushes an image, and a deploy pipeline that picks up that image reference.
Both Jenkinsfiles live in their respective application repositories — not in the library repo. The library is pinned to a specific Git tag to prevent silent breakage when the library evolves.
CI pipeline — Docker build and push:
@Library('[email protected]') _
pipeline {
agent { label 'docker-agent' }
environment {
REGISTRY = 'registry.kuryzhev.cloud'
}
stages {
stage('CI') {
steps {
script {
def image = buildAndPush(
imageName : 'my-service',
registry : env.REGISTRY,
credentialsId: 'registry-creds',
tag : "${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
)
echo "Pushed: ${image}"
// Pass image reference to deploy stage via environment
env.BUILT_IMAGE = image
}
}
}
}
}
Deploy pipeline — consuming the built image:
@Library('[email protected]') _
pipeline {
agent { label 'deploy-agent' }
parameters {
string(name: 'IMAGE_TAG', defaultValue: '', description: 'Full image reference from CI pipeline')
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Target environment')
}
stages {
stage('Deploy') {
steps {
script {
runDeployScript(params.ENVIRONMENT)
}
}
}
}
}
This separation keeps CI and deploy concerns independent. The CI pipeline produces an artifact reference; the deploy pipeline consumes it. Both pipelines share the same library version, so any update to runDeployScript or buildAndPush is adopted by bumping the tag reference in both files — one change, two pipelines updated.
You can find related pipeline patterns and CI/CD architecture notes on kuryzhev.cloud, including approaches to structuring multi-environment deployment workflows.
Common Mistakes That Break Shared Libraries
Most shared library failures fall into three categories. Knowing them in advance saves significant debugging time.
1. Missing or misplaced call() method in vars/ files.
If you define a method with any name other than call() as the entry point in a vars/ file, Jenkins loads the file without error but the step invocation does nothing or throws a MissingMethodException. Every vars/ file that exposes a pipeline step must have exactly one def call(...) method. Additional helper methods inside the same file are fine, but the public interface is always call().
2. CPS serialization failures — NotSerializableException.
Jenkins pipelines run under CPS (Continuation Passing Style) transformation, which means the entire pipeline execution state must be serializable to disk at any point. If you pass a non-serializable object — a Groovy closure, a complex Java object, or a class that does not implement Serializable — across a step boundary, you will see a NotSerializableException at runtime, often in a location that looks unrelated to the actual problem.
The fix has two parts: mark all src/ classes with implements Serializable, and avoid storing non-serializable objects in variables that persist across stage() boundaries. If you need to call a non-serializable utility, wrap it in a @NonCPS annotated method — but be aware that @NonCPS methods cannot call CPS-transformed steps, so they are only appropriate for pure data transformation logic.
3. Unpinned library versions causing silent regressions.
Using a branch name as the library version (e.g., @Library('kuryzhev-shared-lib@main')) means every pipeline run pulls whatever is on main at that moment. A breaking change merged to the library branch will silently affect all consuming pipelines on their next run — often in production, often at the worst possible time.
The correct approach is to version the library with Git tags and require Jenkinsfiles to pin to a specific tag: @Library('[email protected]'). Library updates then follow a deliberate promotion process: tag a new version, test it in a staging pipeline, and update consuming Jenkinsfiles explicitly. This also makes rollback trivial — change the tag reference and re-run.
A secondary version-related mistake is setting defaultVersion in Jenkins global library configuration to a moving branch and relying on it as a safety net. The defaultVersion field is a fallback for Jenkinsfiles that do not specify a version at all — it does not override an explicit version in the @Library annotation. Treat it as a last-resort default for legacy pipelines, not as a version management strategy.
Addressing these three issues — correct call() structure, serialization hygiene, and tag-based versioning — covers the majority of shared library incidents encountered in production Jenkins environments. The pattern itself is straightforward once the conventions are respected; the problems almost always come from one of these three deviations.
