Skip to main content
TWYTech World by Yashrajsinh

Jenkins Shared Libraries Guide

Y
Yashrajsinh
··10 min read·Advanced

Jenkins Shared Libraries Guide

As organizations scale beyond a handful of Jenkins pipelines, duplicated Groovy code across Jenkinsfiles becomes a serious maintenance burden. When twenty repositories each contain their own deployment logic, a change to the deployment process requires twenty separate pull requests, twenty reviews, and twenty merge cycles. Shared libraries solve this problem by extracting common pipeline logic into a dedicated Git repository that Jenkins loads at runtime. Any pipeline in the organization can call library functions as if they were built-in Jenkins steps, and updating the library propagates changes to all consumers automatically.

Shared libraries represent the transition from individual pipeline authoring to platform engineering. Instead of every team reinventing deployment steps, notification patterns, and quality gates, a platform team maintains a curated library of battle-tested pipeline components. Teams consume these components through simple function calls in their Jenkinsfiles, focusing on what makes their service unique rather than reimplementing infrastructure automation from scratch. This pattern mirrors how application developers use frameworks and libraries rather than writing everything from raw primitives.

The power of shared libraries extends beyond code reuse. They enforce organizational standards by embedding best practices into reusable steps. When the security team requires vulnerability scanning before every deployment, adding that check to the shared deployment step ensures compliance across all pipelines without modifying individual Jenkinsfiles. When the platform team improves the deployment process, every consuming pipeline benefits immediately. This centralization creates a single source of truth for how your organization builds, tests, and deploys software.

This guide covers the shared library directory structure, how to write custom pipeline steps, how to create full pipeline templates, how to test library code, and how to manage versioning and rollout strategies that prevent breaking changes from disrupting active pipelines. If you are comfortable with Jenkins declarative pipelines and want to scale your pipeline practices across multiple teams, shared libraries are the next essential skill. The patterns here apply whether you are deploying containerized applications with Docker or managing cloud infrastructure on AWS.

What You Will Learn

By working through this deep dive, you will gain comprehensive knowledge of Jenkins shared library architecture and how to apply it in production environments:

  • How the shared library directory structure organizes custom steps, utility classes, and static resources into a coherent package
  • How to write custom pipeline steps in the vars/ directory that any Jenkinsfile can call as native functions
  • How to create full pipeline templates that standardize delivery workflows across dozens or hundreds of repositories
  • How utility classes in src/ encapsulate complex logic that can be unit tested independently of the Jenkins runtime
  • How to load static resources like deployment templates and shell scripts from the resources/ directory
  • How version pinning in @Library annotations protects consuming pipelines from breaking changes
  • How to test shared library code using the Jenkins Pipeline Unit framework before merging changes
  • How to manage library rollout strategies that allow gradual adoption of new versions across teams

Prerequisites

Before building shared libraries, ensure you have the following foundations:

  • Strong familiarity with Jenkins declarative pipeline syntax including stages, agents, environment variables, and post-conditions
  • Experience writing and maintaining Jenkinsfiles for at least a few projects so you can identify patterns worth extracting
  • Basic Groovy knowledge including classes, methods, closures, string interpolation, and map/list manipulation
  • Access to a Git hosting platform where you can create a repository for the shared library
  • Jenkins administrator access to configure global pipeline libraries or folder-level library settings
  • Understanding of Git team workflow concepts since shared libraries are version-controlled and follow branching strategies

Concept Overview

A Jenkins shared library is a Git repository with a specific directory structure that Jenkins recognizes and loads into the pipeline runtime. The library provides three types of reusable components: global variables (custom steps), class-based utilities, and static resources. Jenkins loads the library at pipeline start, compiles the Groovy code, and makes it available throughout the pipeline execution.

The directory structure follows a convention that Jenkins enforces:

my-shared-library/
├── vars/
│   ├── deployService.groovy      # Custom step: deployService()
│   ├── deployService.txt         # Help text for deployService
│   ├── notifySlack.groovy        # Custom step: notifySlack()
│   └── standardPipeline.groovy   # Full pipeline template
├── src/
│   └── com/
│       └── example/
│           ├── Docker.groovy     # Utility class
│           ├── Kubernetes.groovy # Utility class
│           └── Config.groovy     # Configuration helper
├── resources/
│   ├── templates/
│   │   └── deployment.yaml       # Static resource files
│   └── scripts/
│       └── health-check.sh       # Shell scripts
└── test/
    └── vars/
        └── DeployServiceTest.groovy  # Unit tests

The vars/ directory contains global variable scripts. Each .groovy file in this directory becomes a callable step in any pipeline that loads the library. The filename becomes the step name, so vars/deployService.groovy creates a deployService() step. These scripts must define a call() method that Jenkins invokes when the step is used.

The src/ directory contains standard Groovy classes organized in packages. These classes are compiled and added to the classpath, making them available for import in both vars/ scripts and Jenkinsfiles. Use src/ for complex logic that benefits from object-oriented design, unit testing, and separation of concerns.

The resources/ directory holds static files that library code can load at runtime using the libraryResource() step. Templates, configuration files, and shell scripts that your custom steps need live here.

Step-by-Step Explanation

Building a production-grade shared library involves creating the repository structure, writing custom steps, configuring Jenkins to load the library, and establishing versioning practices that protect consuming pipelines from breaking changes. Each subsection below covers a different aspect of library development with practical examples you can adapt to your organization's needs.

Creating Your First Custom Step

Custom steps live in the vars/ directory. Each file exports a call() method that accepts parameters and executes pipeline logic. Start with a simple notification step that standardizes how your organization sends build notifications:

// vars/notifySlack.groovy
def call(Map config = [:]) {
    def channel = config.channel ?: '#builds'
    def status = config.status ?: currentBuild.currentResult
    def message = config.message ?: "${env.JOB_NAME} #${env.BUILD_NUMBER} - ${status}"
 
    def color = 'warning'
    switch (status) {
        case 'SUCCESS':
            color = 'good'
            break
        case 'FAILURE':
            color = 'danger'
            break
        case 'UNSTABLE':
            color = 'warning'
            break
    }
 
    slackSend(
        channel: channel,
        color: color,
        message: message,
        teamDomain: env.SLACK_TEAM_DOMAIN,
        tokenCredentialId: 'slack-bot-token'
    )
}

Pipelines consume this step with a simple call:

// In any Jenkinsfile
@Library('my-shared-library') _
 
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'npm ci && npm run build'
            }
        }
    }
    post {
        success {
            notifySlack(status: 'SUCCESS', channel: '#deployments')
        }
        failure {
            notifySlack(status: 'FAILURE', channel: '#alerts')
        }
    }
}

The @Library annotation loads the shared library at pipeline start. The underscore _ after the annotation is required when you do not need to assign the library to a variable. Once loaded, notifySlack() is available as a native pipeline step.

Building a Deployment Step

Deployment steps encapsulate your organization's deployment process into a single reusable function. This example handles Docker image building, registry pushing, and Kubernetes deployment:

// vars/deployService.groovy
def call(Map config) {
    def service = config.service ?: error("'service' parameter is required")
    def environment = config.environment ?: error("'environment' parameter is required")
    def registry = config.registry ?: 'registry.example.com'
    def namespace = config.namespace ?: environment
    def tag = config.tag ?: "${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(7) ?: 'latest'}"
    def timeout = config.timeout ?: 300
 
    def image = "${registry}/${service}:${tag}"
 
    echo "Deploying ${service} to ${environment} with image ${image}"
 
    stage("Build Image: ${service}") {
        sh "docker build -t ${image} -f Dockerfile ."
    }
 
    stage("Push Image: ${service}") {
        withCredentials([usernamePassword(
            credentialsId: "${environment}-registry-creds",
            usernameVariable: 'REG_USER',
            passwordVariable: 'REG_PASS'
        )]) {
            sh "echo \${REG_PASS} | docker login ${registry} -u \${REG_USER} --password-stdin"
            sh "docker push ${image}"
        }
    }
 
    stage("Deploy: ${service}${environment}") {
        withCredentials([file(credentialsId: "${environment}-kubeconfig", variable: 'KUBECONFIG')]) {
            sh """
                kubectl set image deployment/${service} \
                    ${service}=${image} \
                    --namespace=${namespace} \
                    --kubeconfig=\${KUBECONFIG}
                kubectl rollout status deployment/${service} \
                    --namespace=${namespace} \
                    --timeout=${timeout}s \
                    --kubeconfig=\${KUBECONFIG}
            """
        }
    }
 
    echo "Successfully deployed ${service}:${tag} to ${environment}"
    return [image: image, tag: tag, environment: environment]
}

Consuming pipelines call this step with minimal configuration:

@Library('my-shared-library') _
 
pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                sh 'npm test -- --watchAll=false'
            }
        }
        stage('Deploy to Staging') {
            when { branch 'develop' }
            steps {
                deployService(
                    service: 'payment-api',
                    environment: 'staging'
                )
            }
        }
        stage('Deploy to Production') {
            when { branch 'main' }
            steps {
                input message: 'Deploy to production?', ok: 'Approve'
                deployService(
                    service: 'payment-api',
                    environment: 'production',
                    timeout: 600
                )
            }
        }
    }
}

The deployment complexity is hidden behind a clean interface. Teams do not need to know the details of Docker registry authentication, Kubernetes rollout commands, or credential naming conventions. They call deployService() with their service name and target environment, and the library handles everything else.

Creating Pipeline Templates

Pipeline templates define entire pipeline structures that teams instantiate with minimal configuration. This is the most powerful shared library pattern because it standardizes not just individual steps but the entire delivery workflow:

// vars/standardPipeline.groovy
def call(Map config) {
    def service = config.service ?: error("'service' parameter is required")
    def language = config.language ?: 'node'
    def deployEnvironments = config.deployEnvironments ?: ['staging', 'production']
    def runIntegrationTests = config.runIntegrationTests ?: true
    def notifyChannel = config.notifyChannel ?: '#builds'
 
    pipeline {
        agent none
 
        options {
            timeout(time: 45, unit: 'MINUTES')
            disableConcurrentBuilds()
            buildDiscarder(logRotator(numToKeepStr: '20'))
            timestamps()
        }
 
        stages {
            stage('Build and Test') {
                agent {
                    docker { image getDockerImage(language) }
                }
                stages {
                    stage('Install') {
                        steps {
                            sh getBuildCommand(language, 'install')
                        }
                    }
                    stage('Quality Gates') {
                        parallel {
                            stage('Lint') {
                                steps { sh getBuildCommand(language, 'lint') }
                            }
                            stage('Unit Tests') {
                                steps { sh getBuildCommand(language, 'test') }
                            }
                            stage('Security Scan') {
                                steps { sh getBuildCommand(language, 'security') }
                            }
                        }
                    }
                    stage('Build') {
                        steps {
                            sh getBuildCommand(language, 'build')
                            stash includes: getStashPattern(language), name: 'build-output'
                        }
                    }
                }
            }
 
            stage('Deploy Staging') {
                when {
                    anyOf {
                        branch 'develop'
                        branch 'main'
                    }
                }
                agent any
                steps {
                    unstash 'build-output'
                    deployService(service: service, environment: 'staging')
                }
            }
 
            stage('Integration Tests') {
                when {
                    allOf {
                        expression { return runIntegrationTests }
                        anyOf {
                            branch 'develop'
                            branch 'main'
                        }
                    }
                }
                agent { docker { image getDockerImage(language) } }
                steps {
                    sh getBuildCommand(language, 'integration')
                }
            }
 
            stage('Deploy Production') {
                when { branch 'main' }
                agent any
                steps {
                    input message: "Deploy ${service} to production?", ok: 'Deploy'
                    unstash 'build-output'
                    deployService(service: service, environment: 'production')
                }
            }
        }
 
        post {
            success {
                notifySlack(status: 'SUCCESS', channel: notifyChannel)
            }
            failure {
                notifySlack(status: 'FAILURE', channel: notifyChannel)
            }
        }
    }
}
 
// Helper methods
def getDockerImage(String language) {
    switch (language) {
        case 'node': return 'node:20-alpine'
        case 'java': return 'maven:3.9-eclipse-temurin-21'
        case 'python': return 'python:3.12-slim'
        default: return 'ubuntu:22.04'
    }
}
 
def getBuildCommand(String language, String phase) {
    def commands = [
        node: [install: 'npm ci', lint: 'npm run lint', test: 'npm test -- --watchAll=false', security: 'npx audit-ci --moderate', build: 'npm run build', integration: 'npm run test:integration'],
        java: [install: 'mvn dependency:resolve', lint: 'mvn checkstyle:check', test: 'mvn test', security: 'mvn dependency-check:check', build: 'mvn package -DskipTests', integration: 'mvn verify -Pintegration'],
        python: [install: 'pip install -r requirements.txt', lint: 'flake8 .', test: 'pytest --cov', security: 'safety check', build: 'python setup.py bdist_wheel', integration: 'pytest tests/integration/']
    ]
    return commands[language]?."${phase}" ?: error("Unknown language '${language}' or phase '${phase}'")
}
 
def getStashPattern(String language) {
    switch (language) {
        case 'node': return 'dist/**'
        case 'java': return 'target/*.jar'
        case 'python': return 'dist/*.whl'
        default: return 'build/**'
    }
}

Teams consume the entire pipeline template with a single call in their Jenkinsfile:

// Jenkinsfile - entire pipeline in one line
@Library('my-shared-library') _
 
standardPipeline(
    service: 'user-service',
    language: 'node',
    notifyChannel: '#team-alpha'
)

This three-line Jenkinsfile gives the team a complete CI/CD pipeline with building, testing, security scanning, staging deployment, integration testing, production deployment with approval gates, and Slack notifications. The platform team maintains the pipeline logic centrally while individual teams configure only what is unique to their service.

Using Utility Classes from src/

The src/ directory holds Groovy classes for complex logic that benefits from object-oriented design and unit testing. These classes are compiled and available on the classpath:

// src/com/example/SemanticVersion.groovy
package com.example
 
class SemanticVersion implements Serializable {
    int major
    int minor
    int patch
    String preRelease
 
    static SemanticVersion parse(String version) {
        def matcher = version =~ /^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/
        if (!matcher.matches()) {
            throw new IllegalArgumentException("Invalid version: ${version}")
        }
        return new SemanticVersion(
            major: matcher[0][1] as int,
            minor: matcher[0][2] as int,
            patch: matcher[0][3] as int,
            preRelease: matcher[0][4]
        )
    }
 
    String bump(String type) {
        switch (type) {
            case 'major': return "${major + 1}.0.0"
            case 'minor': return "${major}.${minor + 1}.0"
            case 'patch': return "${major}.${minor}.${patch + 1}"
            default: throw new IllegalArgumentException("Unknown bump type: ${type}")
        }
    }
 
    String toString() {
        def base = "${major}.${minor}.${patch}"
        return preRelease ? "${base}-${preRelease}" : base
    }
}

Custom steps in vars/ import and use these classes:

// vars/bumpVersion.groovy
import com.example.SemanticVersion
 
def call(Map config = [:]) {
    def bumpType = config.type ?: 'patch'
    def tagPrefix = config.prefix ?: 'v'
 
    def currentTag = sh(script: "git describe --tags --abbrev=0 2>/dev/null || echo 'v0.0.0'", returnStdout: true).trim()
    def current = SemanticVersion.parse(currentTag)
    def next = current.bump(bumpType)
    def newTag = "${tagPrefix}${next}"
 
    echo "Bumping version: ${currentTag}${newTag} (${bumpType})"
 
    sh "git tag ${newTag}"
    sh "git push origin ${newTag}"
 
    return [previous: currentTag, current: newTag, version: next]
}

Loading Static Resources

The resources/ directory stores files that library code loads at runtime using the libraryResource() step. This is useful for deployment templates, Kubernetes manifests, Terraform configurations, shell scripts, and any other static content that your custom steps need to function. Rather than embedding multi-line strings in Groovy code, you keep templates as separate files that are easier to read, edit, and validate independently:

// vars/deployKubernetes.groovy
def call(Map config) {
    def service = config.service
    def image = config.image
    def namespace = config.namespace ?: 'default'
    def replicas = config.replicas ?: 2
 
    // Load template from resources/
    def template = libraryResource('templates/deployment.yaml')
 
    // Replace placeholders
    def manifest = template
        .replace('{{SERVICE_NAME}}', service)
        .replace('{{IMAGE}}', image)
        .replace('{{NAMESPACE}}', namespace)
        .replace('{{REPLICAS}}', replicas.toString())
 
    // Write and apply
    writeFile file: 'deployment.yaml', text: manifest
    sh "kubectl apply -f deployment.yaml --namespace=${namespace}"
    sh "kubectl rollout status deployment/${service} --namespace=${namespace} --timeout=300s"
}

Configuring Library Loading in Jenkins

Jenkins loads shared libraries through configuration at the global, folder, or pipeline level. Global libraries are available to all pipelines. Folder-level libraries are available to pipelines within a specific folder. Pipeline-level loading uses the @Library annotation.

Configure a global library through Manage Jenkins → Configure System → Global Pipeline Libraries. Specify the library name, default version (branch), and the Git repository URL. Enable "Load implicitly" if you want the library available without explicit @Library annotations, though explicit loading is recommended for clarity.

Version pinning in the @Library annotation controls which library version a pipeline uses:

// Use the default branch configured in Jenkins
@Library('my-shared-library') _
 
// Pin to a specific branch
@Library('my-shared-library@main') _
 
// Pin to a specific tag for stability
@Library('[email protected]') _
 
// Pin to a specific commit for maximum reproducibility
@Library('my-shared-library@abc1234') _

Testing Shared Library Code

Testing shared libraries requires mocking the Jenkins pipeline environment since library code depends on Jenkins-specific functions like sh, echo, and withCredentials. The Jenkins Pipeline Unit framework provides this mocking capability:

// test/vars/NotifySlackTest.groovy
import com.lesfurets.jenkins.unit.BasePipelineTest
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.*
 
class NotifySlackTest extends BasePipelineTest {
    def notifySlack
 
    @Before
    void setUp() {
        super.setUp()
        notifySlack = loadScript('vars/notifySlack.groovy')
        binding.setVariable('env', [
            JOB_NAME: 'test-job',
            BUILD_NUMBER: '42',
            SLACK_TEAM_DOMAIN: 'myteam'
        ])
        binding.setVariable('currentBuild', [currentResult: 'SUCCESS'])
    }
 
    @Test
    void testSuccessNotification() {
        notifySlack(status: 'SUCCESS', channel: '#deploys')
 
        def slackCalls = helper.callStack.findAll { it.methodName == 'slackSend' }
        assertEquals(1, slackCalls.size())
        assertEquals('good', slackCalls[0].args[0].color)
        assertEquals('#deploys', slackCalls[0].args[0].channel)
    }
 
    @Test
    void testFailureNotification() {
        notifySlack(status: 'FAILURE')
 
        def slackCalls = helper.callStack.findAll { it.methodName == 'slackSend' }
        assertEquals('danger', slackCalls[0].args[0].color)
        assertEquals('#builds', slackCalls[0].args[0].channel)
    }
}

Run tests with Gradle or Maven before merging library changes to ensure custom steps behave correctly without requiring a live Jenkins instance.

Real-World Use Cases

Shared libraries serve organizations at every scale. These scenarios demonstrate how libraries transform pipeline management from individual effort into platform engineering:

Multi-team standardization is the most common use case. An organization with fifty microservices maintained by ten teams uses a shared library to define the standard deployment pipeline. Each team's Jenkinsfile is three to five lines that call the pipeline template with service-specific configuration. When the platform team adds container vulnerability scanning to the deployment process, all fifty services gain the capability without any team modifying their Jenkinsfile.

Compliance enforcement embeds regulatory requirements into shared steps. Financial services organizations use shared libraries to ensure every deployment includes audit logging, approval gates with specific role requirements, and automated compliance report generation. Since the compliance logic lives in the library rather than individual Jenkinsfiles, auditors review one codebase instead of hundreds.

Migration acceleration uses pipeline templates to onboard new services quickly. When a team creates a new microservice, they add a three-line Jenkinsfile and immediately get a complete CI/CD pipeline with all organizational standards built in. This reduces the time from repository creation to first production deployment from days to minutes.

Gradual rollout of infrastructure changes uses library versioning to migrate pipelines incrementally. When the platform team rewrites the deployment step to use a new container orchestrator, they release it as a new major version. Teams pin to the previous version until they are ready to migrate, then update their @Library annotation to adopt the new version on their own schedule.

Best Practices

These practices keep shared libraries maintainable, reliable, and safe for consumption across your organization:

Version your shared library with semantic versioning tags. Breaking changes get a major version bump, new features get a minor bump, and bug fixes get a patch bump. This allows consuming pipelines to pin to a major version and receive fixes without risking breaking changes.

Keep custom steps focused on a single responsibility. A step called deployService should deploy a service, not also run tests, send notifications, and update documentation. Compose focused steps together in pipeline templates rather than creating monolithic steps that do everything.

Document every custom step with a corresponding .txt file in vars/. Jenkins displays this documentation in the Pipeline Syntax snippet generator, making it discoverable for pipeline authors who may not know what steps are available.

Test library code before merging. Use the Jenkins Pipeline Unit framework for unit tests and a dedicated Jenkins instance for integration tests. Treat your shared library with the same rigor as production application code because it affects every pipeline in your organization.

Use the src/ directory for complex logic and keep vars/ scripts thin. The call() method in a vars/ script should orchestrate pipeline steps and delegate complex logic to classes in src/. This separation makes the complex logic unit-testable without mocking the Jenkins environment.

Never store secrets in the shared library repository. Library code should reference credential IDs that are configured in Jenkins, not contain actual secret values. Use naming conventions for credential IDs so library code can construct the correct ID from parameters like environment name and service name.

Common Mistakes

These mistakes cause the most pain when organizations adopt shared libraries:

Loading libraries implicitly without version pinning means every pipeline immediately gets any change pushed to the library's default branch. A bug in the library breaks every pipeline in the organization simultaneously. Always pin library versions in the @Library annotation and use explicit loading rather than implicit.

Writing monolithic pipeline templates that cannot be customized forces teams to fork the library when their needs diverge slightly from the standard. Design templates with configuration parameters for every decision point. Use sensible defaults so simple cases remain simple while complex cases can override specific behaviors.

Skipping the Serializable interface on classes in src/ causes pipeline failures when Jenkins serializes pipeline state to disk. Every class used in pipeline context must implement Serializable or be confined to methods annotated with @NonCPS that Jenkins does not serialize.

Not testing library changes before merging treats the shared library as less important than application code despite it affecting every pipeline. A broken library change can halt all deployments across the organization. Establish the same code review, testing, and staged rollout practices for library changes as for production application deployments.

Mixing pipeline DSL with business logic in vars/ scripts makes code untestable and hard to reason about. Keep vars/ scripts focused on Jenkins pipeline orchestration and delegate business logic to classes in src/ that can be unit tested without a Jenkins environment.

Summary

Jenkins shared libraries transform pipeline maintenance from a per-repository burden into a centralized platform capability. The vars/ directory provides custom steps that any pipeline can call as native Jenkins functions. The src/ directory holds testable utility classes for complex logic. The resources/ directory stores templates and scripts that library code loads at runtime. Pipeline templates in vars/ define entire delivery workflows that teams instantiate with minimal configuration. Version pinning through @Library annotations protects consuming pipelines from breaking changes while allowing gradual adoption of new library versions. Combined with proper testing, documentation, and semantic versioning, shared libraries enable platform teams to enforce organizational standards, accelerate service onboarding, and propagate improvements across hundreds of pipelines through a single code change.

Advanced13 min read

Jenkins Agents and Build Scaling

Configure Jenkins agents, scale builds with Docker and Kubernetes, manage executors, and optimize distributed pipelines.

Intermediate11 min read

Jenkins Secrets Management

Secure Jenkins pipelines with the credential store, withCredentials bindings, credential scoping, and secret rotation patterns.

Intermediate10 min read

Jenkins Declarative Pipelines Deep Dive

Master Jenkins declarative pipeline syntax including stages, parallel execution, conditional logic, and Jenkinsfile patterns.