Jenkins Shared Libraries
Overview¶
Jenkins Shared Libraries allow teams to share reusable pipeline code across multiple projects, reducing duplication and ensuring consistent CI/CD practices. This guide covers organization, naming conventions, parameter validation, error handling, testing, and versioning strategies.
Key Characteristics¶
- Purpose: Share reusable pipeline steps and utilities across repositories
- Language: Groovy (runs in Jenkins Pipeline sandbox)
- Location: Separate repository configured in Jenkins global settings
- Loading:
@Libraryannotation in Jenkinsfiles
See Also¶
- Jenkins & Groovy Style Guide - Pipeline fundamentals
- GitHub Actions Guide - Alternative CI/CD platform
Library Structure¶
Standard Directory Layout¶
jenkins-shared-library/
├── vars/ # Global pipeline steps (most common)
│ ├── buildDockerImage.groovy # Custom step: buildDockerImage()
│ ├── deployToKubernetes.groovy # Custom step: deployToKubernetes()
│ ├── notifySlack.groovy # Custom step: notifySlack()
│ ├── runTests.groovy # Custom step: runTests()
│ └── validateConfig.groovy # Custom step: validateConfig()
├── src/ # Groovy classes (optional, for complex logic)
│ └── org/
│ └── company/
│ ├── Constants.groovy # Shared constants
│ ├── Docker.groovy # Docker utilities class
│ ├── Kubernetes.groovy # Kubernetes utilities class
│ └── Utils.groovy # General utilities
├── resources/ # Static resources (templates, scripts)
│ ├── templates/
│ │ ├── Dockerfile.template # Template files
│ │ └── k8s-deployment.yaml
│ └── scripts/
│ ├── health-check.sh # Helper scripts
│ └── cleanup.sh
├── test/ # Test files
│ └── groovy/
│ ├── BuildDockerImageTest.groovy
│ └── DeployToKubernetesTest.groovy
├── Jenkinsfile # Pipeline to test the library itself
├── build.gradle # Gradle build for testing
└── README.md # Library documentation
Directory Purpose¶
// vars/ - Global variables accessible directly in pipelines
// Usage in Jenkinsfile:
buildDockerImage(imageName: 'my-app', tag: 'latest')
// src/ - Groovy classes for complex logic
// Usage in Jenkinsfile:
import org.company.Docker
def docker = new Docker(this)
docker.build('my-app')
// resources/ - Static files loaded with libraryResource()
// Usage in Jenkinsfile:
def template = libraryResource('templates/Dockerfile.template')
Global Variables (vars/)¶
Basic Step Structure¶
// vars/buildDockerImage.groovy
/**
* Build and optionally push a Docker image.
*
* @param config Map with configuration options:
* - imageName (required): Name of the Docker image
* - tag (optional): Image tag (default: BUILD_NUMBER)
* - registry (optional): Docker registry URL
* - push (optional): Whether to push the image (default: false)
* - dockerfile (optional): Path to Dockerfile (default: 'Dockerfile')
* - context (optional): Build context path (default: '.')
* @return String The full image name with tag
*
* @example
* buildDockerImage(imageName: 'my-app', tag: 'v1.0.0', push: true)
*/
def call(Map config) {
// Parameter validation
if (!config.imageName) {
error 'imageName is required'
}
// Defaults
def tag = config.tag ?: env.BUILD_NUMBER
def registry = config.registry ?: ''
def push = config.push ?: false
def dockerfile = config.dockerfile ?: 'Dockerfile'
def context = config.context ?: '.'
// Build full image name
def fullImageName = registry ? "${registry}/${config.imageName}:${tag}" : "${config.imageName}:${tag}"
// Execute build
stage('Build Docker Image') {
echo "Building image: ${fullImageName}"
sh "docker build -f ${dockerfile} -t ${fullImageName} ${context}"
}
// Optional push
if (push) {
stage('Push Docker Image') {
if (registry) {
withCredentials([usernamePassword(
credentialsId: 'docker-registry-credentials',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh "echo \$DOCKER_PASS | docker login -u \$DOCKER_USER --password-stdin ${registry}"
sh "docker push ${fullImageName}"
}
} else {
sh "docker push ${fullImageName}"
}
}
}
return fullImageName
}
Step with Multiple Entry Points¶
// vars/kubernetes.groovy
/**
* Kubernetes deployment utilities.
* Provides multiple methods for different operations.
*/
/**
* Deploy an application to Kubernetes.
*
* @param config Map with deployment configuration:
* - namespace (required): Kubernetes namespace
* - deployment (required): Deployment name
* - image (required): Container image to deploy
* - replicas (optional): Number of replicas (default: 1)
*/
def deploy(Map config) {
validateRequired(config, ['namespace', 'deployment', 'image'])
def replicas = config.replicas ?: 1
stage("Deploy to ${config.namespace}") {
sh """
kubectl set image deployment/${config.deployment} \
${config.deployment}=${config.image} \
-n ${config.namespace}
kubectl scale deployment/${config.deployment} \
--replicas=${replicas} \
-n ${config.namespace}
kubectl rollout status deployment/${config.deployment} \
-n ${config.namespace} \
--timeout=300s
"""
}
}
/**
* Roll back a deployment to the previous version.
*
* @param config Map with rollback configuration:
* - namespace (required): Kubernetes namespace
* - deployment (required): Deployment name
*/
def rollback(Map config) {
validateRequired(config, ['namespace', 'deployment'])
stage("Rollback ${config.deployment}") {
sh """
kubectl rollout undo deployment/${config.deployment} \
-n ${config.namespace}
kubectl rollout status deployment/${config.deployment} \
-n ${config.namespace} \
--timeout=300s
"""
}
}
/**
* Delete a deployment.
*
* @param config Map with delete configuration:
* - namespace (required): Kubernetes namespace
* - deployment (required): Deployment name
*/
def delete(Map config) {
validateRequired(config, ['namespace', 'deployment'])
stage("Delete ${config.deployment}") {
sh """
kubectl delete deployment/${config.deployment} \
-n ${config.namespace} \
--ignore-not-found
"""
}
}
/**
* Get deployment status.
*
* @param config Map with status configuration
* @return Map with deployment status information
*/
def status(Map config) {
validateRequired(config, ['namespace', 'deployment'])
def statusJson = sh(
script: """
kubectl get deployment/${config.deployment} \
-n ${config.namespace} \
-o json
""",
returnStdout: true
).trim()
return readJSON(text: statusJson)
}
// Private helper method
private void validateRequired(Map config, List<String> required) {
required.each { param ->
if (!config[param]) {
error "${param} is required"
}
}
}
Usage in Jenkinsfile¶
@Library('my-shared-library@v1.2.0') _
pipeline {
agent any
stages {
stage('Build') {
steps {
// Call global variable function
script {
def image = buildDockerImage(
imageName: 'my-app',
tag: env.GIT_COMMIT[0..7],
push: true,
registry: 'registry.example.com'
)
echo "Built image: ${image}"
}
}
}
stage('Deploy') {
steps {
// Call method on global variable
kubernetes.deploy(
namespace: 'production',
deployment: 'my-app',
image: 'registry.example.com/my-app:latest',
replicas: 3
)
}
}
}
post {
failure {
kubernetes.rollback(
namespace: 'production',
deployment: 'my-app'
)
}
}
}
Groovy Classes (src/)¶
Utility Class Pattern¶
// src/org/company/Docker.groovy
package org.company
/**
* Docker utility class for advanced Docker operations.
*
* @module docker_utils
* @description Provides Docker build, push, and management utilities
* @version 1.0.0
* @author Tyler Dukes
*/
class Docker implements Serializable {
private def script
private def registry
private def credentialsId
/**
* Constructor for Docker utility class.
*
* @param script The pipeline script context (typically 'this')
* @param registry Docker registry URL (optional)
* @param credentialsId Jenkins credentials ID for registry auth
*/
Docker(def script, String registry = '', String credentialsId = '') {
this.script = script
this.registry = registry
this.credentialsId = credentialsId
}
/**
* Build a Docker image with caching support.
*
* @param imageName Name of the image
* @param tag Image tag
* @param buildArgs Map of build arguments
* @param cacheFrom Optional image to use as cache source
* @return Full image name with tag
*/
String build(String imageName, String tag = 'latest', Map buildArgs = [:], String cacheFrom = '') {
def fullName = registry ? "${registry}/${imageName}:${tag}" : "${imageName}:${tag}"
def buildArgsStr = buildArgs.collect { k, v -> "--build-arg ${k}=${v}" }.join(' ')
def cacheFromStr = cacheFrom ? "--cache-from ${cacheFrom}" : ''
script.sh """
docker build \
${buildArgsStr} \
${cacheFromStr} \
-t ${fullName} \
.
"""
return fullName
}
/**
* Push an image to the registry.
*
* @param imageName Full image name with tag
*/
void push(String imageName) {
if (!registry) {
script.error 'Registry must be configured for push operations'
}
if (credentialsId) {
script.withCredentials([script.usernamePassword(
credentialsId: credentialsId,
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
script.sh "echo \$DOCKER_PASS | docker login -u \$DOCKER_USER --password-stdin ${registry}"
script.sh "docker push ${imageName}"
}
} else {
script.sh "docker push ${imageName}"
}
}
/**
* Build and push an image in one operation.
*
* @param imageName Name of the image
* @param tag Image tag
* @param buildArgs Map of build arguments
* @return Full image name with tag
*/
String buildAndPush(String imageName, String tag = 'latest', Map buildArgs = [:]) {
def fullName = build(imageName, tag, buildArgs)
push(fullName)
return fullName
}
/**
* Clean up local Docker images.
*
* @param imageName Optional image name pattern to clean
*/
void cleanup(String imageName = '') {
if (imageName) {
script.sh "docker rmi \$(docker images ${imageName} -q) 2>/dev/null || true"
} else {
script.sh 'docker system prune -f'
}
}
/**
* Run security scan on an image.
*
* @param imageName Image to scan
* @param failOnVulnerability Whether to fail on vulnerabilities
* @return Map with scan results
*/
Map securityScan(String imageName, boolean failOnVulnerability = false) {
def severity = failOnVulnerability ? '--exit-code 1' : ''
try {
def output = script.sh(
script: "trivy image ${severity} --format json ${imageName}",
returnStdout: true
)
return script.readJSON(text: output)
} catch (Exception e) {
if (failOnVulnerability) {
script.error "Security vulnerabilities found in ${imageName}"
}
return [vulnerabilities: [], error: e.message]
}
}
}
Constants Class¶
// src/org/company/Constants.groovy
package org.company
/**
* Shared constants for pipeline configurations.
*
* @module constants
* @description Central repository for shared constants
* @version 1.0.0
*/
class Constants {
// Environment names
static final String ENV_DEV = 'development'
static final String ENV_STAGING = 'staging'
static final String ENV_PRODUCTION = 'production'
// Environment list for iteration
static final List<String> ENVIRONMENTS = [ENV_DEV, ENV_STAGING, ENV_PRODUCTION]
// Kubernetes namespaces by environment
static final Map<String, String> K8S_NAMESPACES = [
(ENV_DEV): 'app-dev',
(ENV_STAGING): 'app-staging',
(ENV_PRODUCTION): 'app-prod'
]
// Docker registries
static final String DOCKER_REGISTRY_DEV = 'registry-dev.example.com'
static final String DOCKER_REGISTRY_PROD = 'registry.example.com'
// Timeouts (in minutes)
static final int BUILD_TIMEOUT = 30
static final int TEST_TIMEOUT = 60
static final int DEPLOY_TIMEOUT = 15
// Retry settings
static final int DEFAULT_RETRIES = 3
static final int RETRY_DELAY_SECONDS = 30
// Notification channels
static final Map<String, String> SLACK_CHANNELS = [
(ENV_DEV): '#dev-builds',
(ENV_STAGING): '#staging-builds',
(ENV_PRODUCTION): '#prod-deploys'
]
// Get registry for environment
static String getRegistry(String environment) {
return environment == ENV_PRODUCTION ? DOCKER_REGISTRY_PROD : DOCKER_REGISTRY_DEV
}
// Get namespace for environment
static String getNamespace(String environment) {
def namespace = K8S_NAMESPACES[environment]
if (!namespace) {
throw new IllegalArgumentException("Unknown environment: ${environment}")
}
return namespace
}
}
Using Classes in Pipeline¶
@Library('my-shared-library@v1.2.0') _
import org.company.Docker
import org.company.Constants
pipeline {
agent any
options {
timeout(time: Constants.BUILD_TIMEOUT, unit: 'MINUTES')
}
environment {
DEPLOY_ENV = "${env.BRANCH_NAME == 'main' ? Constants.ENV_PRODUCTION : Constants.ENV_STAGING}"
}
stages {
stage('Build') {
steps {
script {
def docker = new Docker(
this,
Constants.getRegistry(DEPLOY_ENV),
'docker-credentials'
)
def image = docker.buildAndPush(
'my-app',
env.BUILD_NUMBER,
[BUILD_DATE: new Date().format('yyyy-MM-dd')]
)
env.DOCKER_IMAGE = image
}
}
}
stage('Security Scan') {
steps {
script {
def docker = new Docker(this)
def results = docker.securityScan(
env.DOCKER_IMAGE,
DEPLOY_ENV == Constants.ENV_PRODUCTION
)
if (results.vulnerabilities) {
echo "Found ${results.vulnerabilities.size()} vulnerabilities"
}
}
}
}
stage('Deploy') {
steps {
kubernetes.deploy(
namespace: Constants.getNamespace(DEPLOY_ENV),
deployment: 'my-app',
image: env.DOCKER_IMAGE
)
}
}
}
}
Parameter Validation¶
Comprehensive Validation Function¶
// vars/validateConfig.groovy
/**
* Validate configuration parameters with detailed error messages.
*
* @param config The configuration map to validate
* @param schema The validation schema
* @throws Error if validation fails
*
* @example
* validateConfig(
* [imageName: 'my-app', replicas: 3],
* [
* imageName: [required: true, type: 'string', pattern: /^[a-z][a-z0-9-]*$/],
* replicas: [required: false, type: 'integer', min: 1, max: 10, default: 1],
* environment: [required: true, type: 'enum', values: ['dev', 'staging', 'prod']]
* ]
* )
*/
def call(Map config, Map schema) {
def errors = []
def validated = [:]
schema.each { paramName, rules ->
def value = config[paramName]
// Check required
if (rules.required && value == null) {
errors << "${paramName} is required"
return
}
// Apply default if not provided
if (value == null && rules.default != null) {
validated[paramName] = rules.default
return
}
// Skip validation if optional and not provided
if (value == null) {
return
}
// Type validation
if (rules.type) {
switch (rules.type) {
case 'string':
if (!(value instanceof String)) {
errors << "${paramName} must be a string"
}
break
case 'integer':
if (!(value instanceof Integer)) {
errors << "${paramName} must be an integer"
}
break
case 'boolean':
if (!(value instanceof Boolean)) {
errors << "${paramName} must be a boolean"
}
break
case 'list':
if (!(value instanceof List)) {
errors << "${paramName} must be a list"
}
break
case 'map':
if (!(value instanceof Map)) {
errors << "${paramName} must be a map"
}
break
case 'enum':
if (!rules.values?.contains(value)) {
errors << "${paramName} must be one of: ${rules.values.join(', ')}"
}
break
}
}
// Pattern validation for strings
if (rules.pattern && value instanceof String) {
if (!(value ==~ rules.pattern)) {
errors << "${paramName} does not match required pattern: ${rules.pattern}"
}
}
// Range validation for numbers
if (rules.min != null && value < rules.min) {
errors << "${paramName} must be at least ${rules.min}"
}
if (rules.max != null && value > rules.max) {
errors << "${paramName} must be at most ${rules.max}"
}
// Length validation for strings and lists
if (rules.minLength != null && value.size() < rules.minLength) {
errors << "${paramName} must have at least ${rules.minLength} items/characters"
}
if (rules.maxLength != null && value.size() > rules.maxLength) {
errors << "${paramName} must have at most ${rules.maxLength} items/characters"
}
validated[paramName] = value
}
if (errors) {
error "Configuration validation failed:\n - ${errors.join('\n - ')}"
}
return validated
}
Using Validation in Steps¶
// vars/deployApplication.groovy
/**
* Deploy an application with comprehensive parameter validation.
*/
def call(Map config) {
// Define validation schema
def schema = [
appName: [
required: true,
type: 'string',
pattern: /^[a-z][a-z0-9-]{2,62}$/
],
environment: [
required: true,
type: 'enum',
values: ['dev', 'staging', 'production']
],
version: [
required: true,
type: 'string',
pattern: /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9]+)?$/
],
replicas: [
required: false,
type: 'integer',
min: 1,
max: 100,
default: 1
],
resources: [
required: false,
type: 'map',
default: [cpu: '100m', memory: '256Mi']
],
healthCheckPath: [
required: false,
type: 'string',
default: '/health'
],
timeout: [
required: false,
type: 'integer',
min: 30,
max: 600,
default: 300
]
]
// Validate and get sanitized config
def validatedConfig = validateConfig(config, schema)
echo "Deploying ${validatedConfig.appName} v${validatedConfig.version} to ${validatedConfig.environment}"
// Proceed with deployment using validated config
stage("Deploy to ${validatedConfig.environment}") {
timeout(time: validatedConfig.timeout, unit: 'SECONDS') {
sh """
kubectl set image deployment/${validatedConfig.appName} \
${validatedConfig.appName}=registry.example.com/${validatedConfig.appName}:${validatedConfig.version} \
-n ${validatedConfig.environment}
kubectl scale deployment/${validatedConfig.appName} \
--replicas=${validatedConfig.replicas} \
-n ${validatedConfig.environment}
"""
}
}
return validatedConfig
}
Environment-Specific Validation¶
// vars/validateEnvironment.groovy
/**
* Validate deployment configuration for specific environments.
* Applies stricter rules for production.
*/
def call(Map config) {
def environment = config.environment
def errors = []
// Base validation for all environments
if (!config.appName) {
errors << 'appName is required'
}
if (!config.version) {
errors << 'version is required'
}
// Production-specific validation
if (environment == 'production') {
// Require semantic versioning for production
if (config.version && !(config.version ==~ /^v?\d+\.\d+\.\d+$/)) {
errors << 'Production deployments require semantic versioning (e.g., v1.2.3)'
}
// Minimum replicas for production
if ((config.replicas ?: 1) < 2) {
errors << 'Production deployments require at least 2 replicas'
}
// Require health check for production
if (!config.healthCheckPath) {
errors << 'Production deployments require a health check path'
}
// Require approval for production
if (!config.approvedBy) {
errors << 'Production deployments require approval (approvedBy parameter)'
}
// Check branch restriction
if (env.BRANCH_NAME != 'main') {
errors << 'Production deployments only allowed from main branch'
}
}
// Staging-specific validation
if (environment == 'staging') {
// Warn about pre-release versions
if (config.version?.contains('-')) {
echo "WARNING: Deploying pre-release version ${config.version} to staging"
}
}
if (errors) {
error "Environment validation failed for ${environment}:\n - ${errors.join('\n - ')}"
}
return true
}
Error Handling¶
Comprehensive Error Handling Pattern¶
// vars/safeExecute.groovy
/**
* Execute a closure with comprehensive error handling.
*
* @param config Configuration map:
* - name: Name of the operation (for logging)
* - retries: Number of retry attempts (default: 0)
* - retryDelay: Delay between retries in seconds (default: 30)
* - failFast: Whether to fail immediately on error (default: true)
* - onError: Closure to execute on error
* - onSuccess: Closure to execute on success
* @param body The closure to execute
* @return Result of the closure execution
*/
def call(Map config = [:], Closure body) {
def name = config.name ?: 'operation'
def retries = config.retries ?: 0
def retryDelay = config.retryDelay ?: 30
def failFast = config.failFast != false
def attempt = 0
def lastError = null
while (attempt <= retries) {
attempt++
try {
echo "${name}: Attempt ${attempt}/${retries + 1}"
def result = body()
// Success callback
if (config.onSuccess) {
config.onSuccess(result)
}
return result
} catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) {
// User aborted - don't retry
echo "${name}: Aborted by user"
throw e
} catch (hudson.AbortException e) {
// Pipeline aborted - don't retry
echo "${name}: Pipeline aborted"
throw e
} catch (Exception e) {
lastError = e
echo "${name}: Failed - ${e.message}"
// Error callback
if (config.onError) {
try {
config.onError(e, attempt)
} catch (Exception callbackError) {
echo "Error in onError callback: ${callbackError.message}"
}
}
if (attempt <= retries) {
echo "${name}: Retrying in ${retryDelay} seconds..."
sleep(retryDelay)
}
}
}
// All retries exhausted
if (failFast) {
error "${name}: Failed after ${attempt} attempts. Last error: ${lastError?.message}"
} else {
echo "${name}: Failed after ${attempt} attempts, continuing..."
return null
}
}
Error Handling in Steps¶
// vars/deployWithRollback.groovy
/**
* Deploy with automatic rollback on failure.
*/
def call(Map config) {
def previousVersion = null
try {
// Get current version for potential rollback
previousVersion = getCurrentVersion(config)
echo "Current version: ${previousVersion}"
// Perform deployment
safeExecute(
name: "Deploy ${config.appName}",
retries: 2,
retryDelay: 60,
onError: { error, attempt ->
notifySlack(
channel: '#deployments',
message: "Deployment attempt ${attempt} failed: ${error.message}",
color: 'warning'
)
}
) {
performDeployment(config)
}
// Verify deployment
verifyDeployment(config)
// Success notification
notifySlack(
channel: '#deployments',
message: "Successfully deployed ${config.appName} v${config.version}",
color: 'good'
)
} catch (Exception e) {
echo "Deployment failed: ${e.message}"
// Attempt rollback
if (previousVersion && config.autoRollback != false) {
echo "Rolling back to ${previousVersion}..."
try {
performRollback(config, previousVersion)
notifySlack(
channel: '#deployments',
message: "Rolled back ${config.appName} to ${previousVersion} after failed deployment",
color: 'danger'
)
} catch (Exception rollbackError) {
notifySlack(
channel: '#deployments',
message: "CRITICAL: Rollback failed for ${config.appName}! Manual intervention required.",
color: 'danger'
)
error "Deployment and rollback both failed: ${rollbackError.message}"
}
}
throw e
}
}
private def getCurrentVersion(Map config) {
def output = sh(
script: """
kubectl get deployment/${config.appName} \
-n ${config.namespace} \
-o jsonpath='{.spec.template.spec.containers[0].image}'
""",
returnStdout: true
).trim()
return output.split(':').last()
}
private void performDeployment(Map config) {
sh """
kubectl set image deployment/${config.appName} \
${config.appName}=${config.image} \
-n ${config.namespace}
kubectl rollout status deployment/${config.appName} \
-n ${config.namespace} \
--timeout=300s
"""
}
private void performRollback(Map config, String version) {
sh """
kubectl rollout undo deployment/${config.appName} \
-n ${config.namespace}
kubectl rollout status deployment/${config.appName} \
-n ${config.namespace} \
--timeout=300s
"""
}
private void verifyDeployment(Map config) {
def healthCheckUrl = "http://${config.appName}.${config.namespace}.svc.cluster.local${config.healthCheckPath ?: '/health'}"
safeExecute(
name: 'Health Check',
retries: 5,
retryDelay: 10
) {
sh "curl -f -s ${healthCheckUrl}"
}
}
Graceful Degradation¶
// vars/withFallback.groovy
/**
* Execute with fallback behavior.
*
* @param primary Primary closure to execute
* @param fallback Fallback closure if primary fails
* @param config Configuration options
* @return Result from either primary or fallback
*/
def call(Map config = [:], Closure primary, Closure fallback) {
try {
return primary()
} catch (Exception e) {
echo "Primary operation failed: ${e.message}"
if (config.notifyOnFallback) {
notifySlack(
channel: config.channel ?: '#alerts',
message: "Using fallback for ${config.name ?: 'operation'}: ${e.message}",
color: 'warning'
)
}
return fallback()
}
}
// Usage example
// vars/getArtifact.groovy
def call(Map config) {
withFallback(
name: 'Get Artifact',
notifyOnFallback: true,
channel: '#builds'
) {
// Primary: Download from artifact repository
sh "curl -f -O https://artifacts.example.com/${config.name}/${config.version}"
} {
// Fallback: Build from source
echo "Artifact not found, building from source..."
sh "make build"
}
}
Documentation Standards¶
Step Documentation Template¶
// vars/exampleStep.groovy
/**
* Brief one-line description of what this step does.
*
* Extended description providing more context about the step's purpose,
* when to use it, and any important considerations. This section should
* help developers understand if this is the right step for their use case.
*
* @module example_step
* @description Brief description for metadata
* @version 1.0.0
* @author Tyler Dukes
* @since 2025-01-01
*
* @param config Map Configuration options for the step:
* @param config.requiredParam (required) Description of required parameter.
* Type: String
* Example: 'my-value'
* @param config.optionalParam (optional) Description of optional parameter.
* Type: Integer
* Default: 10
* Valid range: 1-100
* @param config.enumParam (required) Parameter with fixed values.
* Type: String
* Values: 'option1', 'option2', 'option3'
*
* @return Map Result containing:
* - success: Boolean indicating if operation succeeded
* - message: String with status message
* - data: Map with operation-specific data
*
* @throws ValidationError When required parameters are missing or invalid
* @throws ExecutionError When the operation fails
*
* @example Basic usage
* exampleStep(
* requiredParam: 'my-value',
* enumParam: 'option1'
* )
*
* @example With all options
* def result = exampleStep(
* requiredParam: 'my-value',
* optionalParam: 50,
* enumParam: 'option2'
* )
* echo "Result: ${result.message}"
*
* @see relatedStep For related functionality
* @see https://docs.example.com/steps/example More documentation
*/
def call(Map config) {
// Implementation
}
Class Documentation Template¶
// src/org/company/ExampleClass.groovy
package org.company
/**
* Brief description of the class purpose.
*
* Extended description explaining the class's role in the shared library,
* typical use cases, and how it integrates with other components.
*
* <h3>Usage Example</h3>
* <pre>
* {@code
* def example = new ExampleClass(this, 'config-value')
* example.performAction()
* }
* </pre>
*
* <h3>Thread Safety</h3>
* This class is thread-safe / not thread-safe.
*
* @module example_class
* @description Brief description for metadata
* @version 1.0.0
* @author Tyler Dukes
* @since 2025-01-01
*/
class ExampleClass implements Serializable {
private static final long serialVersionUID = 1L
/** The pipeline script context */
private final def script
/** Configuration value */
private final String configValue
/**
* Creates a new ExampleClass instance.
*
* @param script The pipeline script context (typically 'this')
* @param configValue Configuration value for the instance
* @throws IllegalArgumentException if configValue is null or empty
*/
ExampleClass(def script, String configValue) {
if (!configValue) {
throw new IllegalArgumentException('configValue cannot be null or empty')
}
this.script = script
this.configValue = configValue
}
/**
* Performs the main action of this class.
*
* Detailed description of what this method does, any side effects,
* and important considerations.
*
* @param input The input to process
* @return Processed result
* @throws ProcessingException if processing fails
*/
String performAction(String input) {
// Implementation
}
}
README Documentation¶
<!-- README.md at repository root -->
# My Jenkins Shared Library
Reusable pipeline code for CI/CD automation.
## Installation
### Global Configuration
1. Navigate to **Manage Jenkins** > **Configure System**
2. Scroll to **Global Pipeline Libraries**
3. Add new library:
- **Name**: `my-shared-library`
- **Default version**: `main`
- **Retrieval method**: Modern SCM
- **Source Code Management**: Git
- **Project Repository**: `https://github.com/company/jenkins-shared-library.git`
### Per-Pipeline Usage
```groovy
@Library('my-shared-library@v1.2.0') _
```
## Available Steps
| Step | Description | Example |
|------|-------------|---------|
| `buildDockerImage` | Build Docker images | `buildDockerImage(imageName: 'app')` |
| `deployToKubernetes` | Deploy to K8s | `deployToKubernetes(namespace: 'prod')` |
| `notifySlack` | Send Slack notification | `notifySlack(channel: '#builds')` |
## Quick Start
```groovy
@Library('my-shared-library@v1.2.0') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildDockerImage(
imageName: 'my-app',
push: true
)
}
}
}
}
```
## Contributing
See CONTRIBUTING.md for development guidelines.
Testing¶
Test Setup with Gradle¶
// build.gradle
plugins {
id 'groovy'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.codehaus.groovy:groovy-all:3.0.19'
testImplementation 'com.lesfurets:jenkins-pipeline-unit:1.19'
testImplementation 'org.spockframework:spock-core:2.4-M1-groovy-3.0'
testImplementation 'junit:junit:4.13.2'
}
sourceSets {
main {
groovy {
srcDirs = ['src', 'vars']
}
}
test {
groovy {
srcDirs = ['test/groovy']
}
}
}
test {
useJUnitPlatform()
testLogging {
events 'passed', 'skipped', 'failed'
}
}
Unit Testing with Spock¶
// test/groovy/BuildDockerImageTest.groovy
import spock.lang.Specification
import com.lesfurets.jenkins.unit.BasePipelineTest
class BuildDockerImageTest extends BasePipelineTest {
def script
def setup() {
super.setUp()
// Load the shared library step
script = loadScript('vars/buildDockerImage.groovy')
// Register mock methods
helper.registerAllowedMethod('stage', [String, Closure], { name, body ->
body()
})
helper.registerAllowedMethod('echo', [String], { msg ->
println "[ECHO] ${msg}"
})
helper.registerAllowedMethod('sh', [String], { cmd ->
println "[SH] ${cmd}"
return 0
})
helper.registerAllowedMethod('withCredentials', [List, Closure], { creds, body ->
body()
})
}
def 'should fail when imageName is not provided'() {
when:
script.call([:])
then:
def e = thrown(Exception)
e.message.contains('imageName is required')
}
def 'should build image with default tag'() {
given:
binding.setVariable('env', [BUILD_NUMBER: '42'])
when:
def result = script.call([imageName: 'my-app'])
then:
result == 'my-app:42'
}
def 'should build image with custom tag'() {
when:
def result = script.call([
imageName: 'my-app',
tag: 'v1.0.0'
])
then:
result == 'my-app:v1.0.0'
}
def 'should build image with registry'() {
when:
def result = script.call([
imageName: 'my-app',
tag: 'latest',
registry: 'registry.example.com'
])
then:
result == 'registry.example.com/my-app:latest'
}
def 'should push image when push is true'() {
given:
def commands = []
helper.registerAllowedMethod('sh', [String], { cmd ->
commands << cmd
return 0
})
when:
script.call([
imageName: 'my-app',
tag: 'latest',
push: true
])
then:
commands.any { it.contains('docker push') }
}
}
Testing Classes¶
// test/groovy/DockerTest.groovy
import spock.lang.Specification
import org.company.Docker
class DockerTest extends Specification {
def script
def commandsExecuted
def setup() {
commandsExecuted = []
// Create mock script context
script = [
sh: { cmd ->
if (cmd instanceof Map) {
commandsExecuted << cmd.script
return cmd.returnStdout ? '{"vulnerabilities": []}' : 0
}
commandsExecuted << cmd
return 0
},
error: { msg -> throw new RuntimeException(msg) },
withCredentials: { creds, body -> body() },
readJSON: { args -> return [vulnerabilities: []] },
usernamePassword: { args -> return args }
]
}
def 'should build image with default settings'() {
given:
def docker = new Docker(script)
when:
def result = docker.build('my-app')
then:
result == 'my-app:latest'
commandsExecuted.any { it.contains('docker build') }
}
def 'should build image with registry'() {
given:
def docker = new Docker(script, 'registry.example.com')
when:
def result = docker.build('my-app', 'v1.0.0')
then:
result == 'registry.example.com/my-app:v1.0.0'
}
def 'should include build args'() {
given:
def docker = new Docker(script)
when:
docker.build('my-app', 'latest', [
BUILD_DATE: '2025-01-01',
VERSION: '1.0.0'
])
then:
commandsExecuted.any {
it.contains('--build-arg BUILD_DATE=2025-01-01') &&
it.contains('--build-arg VERSION=1.0.0')
}
}
def 'should fail push without registry'() {
given:
def docker = new Docker(script)
when:
docker.push('my-app:latest')
then:
thrown(RuntimeException)
}
}
Integration Testing¶
// test/integration/Jenkinsfile.test
@Library('my-shared-library') _
pipeline {
agent any
options {
skipDefaultCheckout()
timeout(time: 10, unit: 'MINUTES')
}
stages {
stage('Test buildDockerImage') {
steps {
script {
// Create test Dockerfile
writeFile file: 'Dockerfile', text: '''
FROM alpine:3.19
RUN echo "test"
'''
// Test the shared library step
def image = buildDockerImage(
imageName: 'test-image',
tag: 'integration-test',
push: false
)
assert image == 'test-image:integration-test'
}
}
}
stage('Test validateConfig') {
steps {
script {
// Test valid config
def config = validateConfig(
[name: 'test', count: 5],
[
name: [required: true, type: 'string'],
count: [required: false, type: 'integer', default: 1]
]
)
assert config.name == 'test'
assert config.count == 5
// Test default values
def configWithDefaults = validateConfig(
[name: 'test'],
[
name: [required: true, type: 'string'],
count: [required: false, type: 'integer', default: 10]
]
)
assert configWithDefaults.count == 10
}
}
}
stage('Test error handling') {
steps {
script {
def attempts = 0
safeExecute(
name: 'Retry Test',
retries: 2,
retryDelay: 1
) {
attempts++
if (attempts < 3) {
throw new Exception("Attempt ${attempts} failed")
}
return 'success'
}
assert attempts == 3
}
}
}
}
post {
always {
cleanWs()
}
}
}
CI Pipeline for Library¶
// Jenkinsfile (for the shared library repository itself)
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timestamps()
timeout(time: 30, unit: 'MINUTES')
}
stages {
stage('Lint') {
steps {
sh 'npm install -g npm-groovy-lint'
sh 'npm-groovy-lint --path . --format json --output lint-report.json || true'
archiveArtifacts artifacts: 'lint-report.json', allowEmptyArchive: true
}
}
stage('Unit Tests') {
steps {
sh './gradlew test'
}
post {
always {
junit 'build/test-results/**/*.xml'
}
}
}
stage('Integration Tests') {
when {
branch 'main'
}
steps {
build job: 'shared-library-integration-tests',
parameters: [
string(name: 'LIBRARY_BRANCH', value: env.GIT_COMMIT)
]
}
}
stage('Documentation') {
steps {
sh './gradlew groovydoc'
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'build/docs/groovydoc',
reportFiles: 'index.html',
reportName: 'Groovy Documentation'
])
}
}
}
post {
success {
script {
if (env.TAG_NAME) {
echo "New version released: ${env.TAG_NAME}"
}
}
}
failure {
emailext(
subject: "Shared Library Build Failed: ${env.JOB_NAME}",
body: "Build ${env.BUILD_NUMBER} failed. Check console output.",
recipientProviders: [developers()]
)
}
}
}
Versioning and Release¶
Semantic Versioning¶
// vars/libraryVersion.groovy
/**
* Get the current library version.
* Uses Git tags for versioning.
*/
@NonCPS
def call() {
def tag = sh(
script: 'git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0"',
returnStdout: true
).trim()
return tag.replaceFirst('^v', '')
}
/**
* Check if running a specific minimum version.
*/
def isMinimumVersion(String required) {
def current = call()
return compareVersions(current, required) >= 0
}
@NonCPS
private int compareVersions(String v1, String v2) {
def parts1 = v1.tokenize('.').collect { it.toInteger() }
def parts2 = v2.tokenize('.').collect { it.toInteger() }
for (int i = 0; i < Math.max(parts1.size(), parts2.size()); i++) {
def p1 = i < parts1.size() ? parts1[i] : 0
def p2 = i < parts2.size() ? parts2[i] : 0
if (p1 != p2) {
return p1 <=> p2
}
}
return 0
}
Loading Specific Versions¶
// Using exact version tag
@Library('my-shared-library@v1.2.3') _
// Using branch
@Library('my-shared-library@main') _
// Using commit SHA
@Library('my-shared-library@abc123def') _
// Multiple libraries
@Library(['my-shared-library@v1.2.3', 'other-library@v2.0.0']) _
// Dynamic version loading
library identifier: "my-shared-library@${params.LIBRARY_VERSION}",
retriever: modernSCM([
$class: 'GitSCMSource',
remote: 'https://github.com/company/jenkins-shared-library.git'
])
Version Compatibility¶
// vars/requireVersion.groovy
/**
* Ensure minimum library version is loaded.
*/
def call(String minVersion) {
def currentVersion = libraryVersion()
if (!libraryVersion.isMinimumVersion(minVersion)) {
error """
This pipeline requires shared library version ${minVersion} or higher.
Current version: ${currentVersion}
Update your @Library annotation:
@Library('my-shared-library@v${minVersion}') _
"""
}
echo "Using shared library version ${currentVersion}"
}
// Usage in Jenkinsfile
@Library('my-shared-library@v1.2.0') _
pipeline {
agent any
stages {
stage('Check Version') {
steps {
requireVersion('1.2.0')
}
}
}
}
Changelog Management¶
// vars/generateChangelog.groovy
/**
* Generate changelog from Git commits.
*/
def call(String fromTag = '', String toTag = 'HEAD') {
def from = fromTag ?: sh(
script: 'git describe --tags --abbrev=0 HEAD^ 2>/dev/null || git rev-list --max-parents=0 HEAD',
returnStdout: true
).trim()
def log = sh(
script: """
git log ${from}..${toTag} \
--pretty=format:'- %s (%an)' \
--no-merges
""",
returnStdout: true
).trim()
return """
## Changes since ${from}
${log}
## Upgrade Instructions
Update your Jenkinsfile:
@Library('my-shared-library@${toTag}') _
"""
}
Loading Libraries¶
Global Configuration¶
// Configure in Jenkins: Manage Jenkins > Configure System > Global Pipeline Libraries
// Or using Jenkins Configuration as Code (JCasC):
// jenkins.yaml
unclassified:
globalLibraries:
libraries:
- name: 'my-shared-library'
defaultVersion: 'main'
implicit: false
allowVersionOverride: true
includeInChangesets: true
retriever:
modernSCM:
scm:
git:
remote: 'https://github.com/company/jenkins-shared-library.git'
credentialsId: 'github-token'
traits:
- gitBranchDiscovery
- gitTagDiscovery
Dynamic Library Loading¶
// vars/loadSharedLib.groovy
/**
* Dynamically load a shared library.
*
* @param name Library name
* @param version Version to load
* @param repo Repository URL (optional, uses global config if not provided)
*/
def call(Map config) {
def name = config.name
def version = config.version ?: 'main'
def repo = config.repo
if (repo) {
library identifier: "${name}@${version}",
retriever: modernSCM([
$class: 'GitSCMSource',
remote: repo,
credentialsId: config.credentialsId
])
} else {
library "${name}@${version}"
}
echo "Loaded ${name}@${version}"
}
// Usage in Jenkinsfile
pipeline {
agent any
stages {
stage('Setup') {
steps {
script {
// Load library dynamically based on environment
def libVersion = env.BRANCH_NAME == 'main' ? 'v1.2.0' : 'develop'
loadSharedLib(
name: 'my-shared-library',
version: libVersion
)
}
}
}
}
}
Multiple Libraries¶
// Loading multiple libraries with explicit names
@Library([
'my-shared-library@v1.2.0',
'common-steps@v2.0.0',
'notification-library@v1.0.0'
]) _
// With aliasing to avoid conflicts
@Library('my-shared-library@v1.2.0')
import org.company.Docker as MyDocker
@Library('other-library@v1.0.0')
import org.other.Docker as OtherDocker
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
def myDocker = new MyDocker(this)
def otherDocker = new OtherDocker(this)
}
}
}
}
}
Folder-Level Libraries¶
// Configure library at folder level in Jenkins UI
// Properties > Pipeline Libraries
// Or using JCasC:
jobs:
- script: |
folder('my-team') {
properties {
folderLibraries {
libraries {
libraryConfiguration {
name 'team-library'
defaultVersion 'main'
implicit true
retriever {
modernSCM {
scm {
git {
remote 'https://github.com/team/shared-library.git'
credentialsId 'github-token'
}
}
}
}
}
}
}
}
}
Common Patterns¶
Pipeline Template Pattern¶
// vars/standardPipeline.groovy
/**
* Standard pipeline template for microservices.
* Provides consistent CI/CD workflow across all services.
*/
def call(Map config, Closure body = null) {
// Validate configuration
validateConfig(config, [
appName: [required: true, type: 'string'],
language: [required: true, type: 'enum', values: ['java', 'python', 'node', 'go']],
deployEnvironments: [required: false, type: 'list', default: ['dev', 'staging', 'prod']]
])
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '20'))
timestamps()
timeout(time: 60, unit: 'MINUTES')
disableConcurrentBuilds()
}
environment {
APP_NAME = "${config.appName}"
DOCKER_REGISTRY = 'registry.example.com'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
script {
buildForLanguage(config.language)
}
}
}
stage('Test') {
steps {
script {
testForLanguage(config.language)
}
}
post {
always {
publishTestResults(config.language)
}
}
}
stage('Security Scan') {
steps {
securityScan(appName: config.appName)
}
}
stage('Build Image') {
steps {
script {
env.DOCKER_IMAGE = buildDockerImage(
imageName: config.appName,
push: true
)
}
}
}
stage('Deploy') {
when {
anyOf {
branch 'main'
branch 'develop'
buildingTag()
}
}
steps {
script {
def environment = determineEnvironment()
deployToKubernetes(
namespace: environment,
deployment: config.appName,
image: env.DOCKER_IMAGE
)
}
}
}
stage('Custom Steps') {
when {
expression { body != null }
}
steps {
script {
body()
}
}
}
}
post {
success {
notifySlack(
channel: '#builds',
message: "Build succeeded: ${config.appName} #${env.BUILD_NUMBER}",
color: 'good'
)
}
failure {
notifySlack(
channel: '#builds',
message: "Build failed: ${config.appName} #${env.BUILD_NUMBER}",
color: 'danger'
)
}
always {
cleanWs()
}
}
}
}
private void buildForLanguage(String language) {
switch (language) {
case 'java':
sh './gradlew build -x test'
break
case 'python':
sh 'pip install -e . && python -m build'
break
case 'node':
sh 'npm ci && npm run build'
break
case 'go':
sh 'go build ./...'
break
}
}
private void testForLanguage(String language) {
switch (language) {
case 'java':
sh './gradlew test'
break
case 'python':
sh 'pytest --junitxml=test-results.xml'
break
case 'node':
sh 'npm test'
break
case 'go':
sh 'go test -v ./... -coverprofile=coverage.out'
break
}
}
private String determineEnvironment() {
if (env.TAG_NAME) {
return 'production'
} else if (env.BRANCH_NAME == 'main') {
return 'staging'
} else {
return 'development'
}
}
Usage of Pipeline Template¶
// Jenkinsfile in application repository
@Library('my-shared-library@v1.2.0') _
standardPipeline(
appName: 'user-service',
language: 'java',
deployEnvironments: ['dev', 'staging', 'prod']
) {
// Optional custom steps
stage('Database Migration') {
sh './gradlew flywayMigrate'
}
}
Decorator Pattern¶
// vars/withStandardOptions.groovy
/**
* Wrap a closure with standard pipeline options.
*/
def call(Map config = [:], Closure body) {
def buildTimeout = config.timeout ?: 60
timestamps {
timeout(time: buildTimeout, unit: 'MINUTES') {
ansiColor('xterm') {
body()
}
}
}
}
// vars/withNotifications.groovy
/**
* Wrap a closure with start/end notifications.
*/
def call(Map config, Closure body) {
def channel = config.channel ?: '#builds'
def startMessage = config.startMessage ?: "Build started: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
def successMessage = config.successMessage ?: "Build succeeded: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
def failureMessage = config.failureMessage ?: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
try {
notifySlack(channel: channel, message: startMessage, color: '#439FE0')
body()
notifySlack(channel: channel, message: successMessage, color: 'good')
} catch (Exception e) {
notifySlack(channel: channel, message: failureMessage, color: 'danger')
throw e
}
}
// Usage
withStandardOptions(timeout: 30) {
withNotifications(channel: '#deployments') {
// Pipeline logic here
}
}
Anti-Patterns¶
❌ Avoid: Hardcoded Values¶
// Bad - Hardcoded values in shared library
def call(Map config) {
sh 'docker push registry.company.com/app:latest' // ❌ Hardcoded registry
sh 'kubectl apply -f deployment.yaml -n production' // ❌ Hardcoded namespace
}
// Good - Parameterized and configurable
def call(Map config) {
def registry = config.registry ?: env.DOCKER_REGISTRY
def namespace = config.namespace
if (!registry) {
error 'Registry must be provided or set in DOCKER_REGISTRY environment variable'
}
if (!namespace) {
error 'namespace is required'
}
sh "docker push ${registry}/${config.imageName}:${config.tag}"
sh "kubectl apply -f deployment.yaml -n ${namespace}"
}
❌ Avoid: No Error Context¶
// Bad - Generic error with no context
def call(Map config) {
if (!config.appName) {
error 'Invalid configuration' // ❌ What's invalid?
}
}
// Good - Specific error with context
def call(Map config) {
if (!config.appName) {
error """
Configuration error in deployApplication step:
- appName is required but was not provided
Example usage:
deployApplication(
appName: 'my-app',
environment: 'staging'
)
"""
}
}
❌ Avoid: Blocking Operations Without Timeout¶
// Bad - No timeout on blocking operation
def call(Map config) {
sh 'kubectl rollout status deployment/app' // ❌ Can hang forever
}
// Good - Timeout on all blocking operations
def call(Map config) {
def timeoutSeconds = config.timeout ?: 300
timeout(time: timeoutSeconds, unit: 'SECONDS') {
sh """
kubectl rollout status deployment/${config.appName} \
-n ${config.namespace} \
--timeout=${timeoutSeconds}s
"""
}
}
❌ Avoid: Swallowing Exceptions¶
// Bad - Silent failure
def call(Map config) {
try {
sh 'make deploy'
} catch (Exception e) {
echo 'Deployment failed' // ❌ Continues as if nothing happened
}
}
// Good - Proper error handling
def call(Map config) {
try {
sh 'make deploy'
} catch (Exception e) {
echo "Deployment failed: ${e.message}"
// Attempt cleanup
try {
sh 'make rollback'
} catch (Exception rollbackError) {
echo "Rollback also failed: ${rollbackError.message}"
}
// Re-throw to fail the build
throw e
}
}
❌ Avoid: Global State Modification¶
// Bad - Modifying global state
class DeploymentHelper {
static String lastDeployedVersion // ❌ Global mutable state
def call(Map config) {
// ...
lastDeployedVersion = config.version // ❌ Race condition risk
}
}
// Good - Return values and pass state explicitly
def call(Map config) {
def result = performDeployment(config)
return [
version: config.version,
timestamp: new Date(),
status: result.status
]
}
❌ Avoid: Missing Serializable¶
// Bad - Class not serializable (will fail on Jenkins restart)
class DeploymentConfig { // ❌ Missing implements Serializable
String appName
String version
}
// Good - Properly serializable
class DeploymentConfig implements Serializable {
private static final long serialVersionUID = 1L
String appName
String version
}
Security Considerations¶
Credential Handling¶
// vars/secureCredentials.groovy
/**
* Securely handle credentials with minimal exposure.
*/
def call(String credentialsId, Closure body) {
// Validate credentials exist before use
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardCredentials.class,
Jenkins.instance,
null,
null
).find { it.id == credentialsId }
if (!creds) {
error "Credentials '${credentialsId}' not found. Please configure in Jenkins Credentials."
}
// Use withCredentials for minimal exposure
withCredentials([usernamePassword(
credentialsId: credentialsId,
usernameVariable: 'CRED_USER',
passwordVariable: 'CRED_PASS'
)]) {
// Credentials only available in this scope
body()
}
// Credentials automatically cleared after scope
}
Input Sanitization¶
// vars/sanitizeInput.groovy
/**
* Sanitize user inputs to prevent injection attacks.
*/
def call(String input, String type = 'general') {
if (input == null) {
return null
}
switch (type) {
case 'shell':
// Escape shell metacharacters
return input.replaceAll(/[;&|`\$\(\)\{\}\[\]<>\\!"']/, '\\\\$0')
case 'docker':
// Only allow safe characters in Docker image names
if (!(input ==~ /^[a-z0-9][a-z0-9._-]*[a-z0-9]$/)) {
error "Invalid Docker image name: ${input}"
}
return input
case 'kubernetes':
// Only allow DNS-compatible names
if (!(input ==~ /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/)) {
error "Invalid Kubernetes resource name: ${input}"
}
return input
default:
// Remove potentially dangerous characters
return input.replaceAll(/[<>&;|`]/, '')
}
}
// Usage
def call(Map config) {
def safeName = sanitizeInput(config.appName, 'kubernetes')
sh "kubectl get deployment ${safeName}"
}
References¶
Official Documentation¶
Testing Resources¶
Tools¶
Related Guides¶
Maintainer: Tyler Dukes