
Jenkinsfile Validator
- 416 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
jenkinsfile-validator is a Claude Code agent skill that validates Declarative and Scripted Jenkinsfile pipelines for developers who need fast CI feedback on syntax, security, and shared-library issues.
About
jenkinsfile-validator is a comprehensive Jenkins pipeline validation skill from akin-ozer/cc-devops-skills for Declarative and Scripted Jenkinsfiles plus shared-library Groovy. The skill auto-detects pipeline type, then runs scripts such as validate_jenkinsfile.sh, validate_declarative.sh, validate_scripted.sh, common_validation.sh, best_practices.sh, and validate_shared_library.sh to catch syntax errors, hardcoded secrets, credential misuse, CPS compatibility problems, and @NonCPS violations in vars/*.groovy and src/**/*.groovy files. CLI flags support syntax-only, security-only, best-practices-only, strict mode treating warnings as failures, and subset runs so CI can fail fast in pull request review. Developers reach for jenkinsfile-validator when Jenkins jobs break on the controller, shared-library steps misbehave, or pipeline changes need categorized errors and warnings before merge. Outputs are unified summaries designed for review comments and pre-merge quality gates rather than silent rewrites.
- Declarative and scripted Jenkinsfile checks
- Pipeline stage and agent validation
- Early CI failure prevention
- Integrates with cc-devops-skills tooling
- Reduces broken Jenkins job debugging
Jenkinsfile Validator by the numbers
- 416 all-time installs (skills.sh)
- Ranked #286 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill jenkinsfile-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 416 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you validate a Jenkinsfile before CI runs?
Check Jenkinsfile syntax, pipeline stages, and declarative structure so CI jobs fail fast in review instead of breaking builds on the controller.
Who is it for?
Platform engineers and backend developers who maintain Jenkins Declarative or Scripted pipelines and Jenkins shared libraries needing pre-merge validation.
Skip if: Teams generating brand-new Jenkins pipelines from scratch should use jenkinsfile-generator rather than this validator-focused skill.
When should I use this skill?
User asks to validate, lint, debug, or security-check a Jenkinsfile, Jenkins pipeline, or vars/*.groovy shared-library step.
What you get
Unified validation summary with categorized syntax, security, and best-practice errors and warnings for Jenkinsfiles and shared-library Groovy files.
- Categorized Jenkins pipeline validation report
- Security and best-practice finding lists
- Shared-library CPS and credential analysis output
By the numbers
- Validates both Declarative and Scripted Jenkinsfiles plus vars/*.groovy and src/**/*.groovy shared-library files
- Exposes dedicated CLI modes for syntax-only, security-only, best-practices-only, and strict validation
Files
Jenkinsfile Validator Skill
Use this skill to validate Jenkins pipelines and shared libraries with local scripts first, then optionally enrich findings with plugin documentation.
Trigger Phrases
Use this skill when requests look like:
- "Validate this Jenkinsfile"
- "Check this pipeline for security issues"
- "Lint my Declarative/Scripted pipeline"
- "Why is this Jenkins pipeline failing syntax checks?"
- "Validate vars/.groovy or src//.groovy shared library files"
Scope
This skill validates:
- Declarative pipelines (
pipeline { ... }) - Scripted pipelines (
node { ... }and Groovy-style pipelines) - Shared library files (
vars/*.groovy,src/**/*.groovy) - Hardcoded credential patterns
- Pipeline best practices and maintainability signals
Prerequisites
Run commands from repository root unless noted.
Required tools
bashgrepsedawkheadwcfind(needed for shared-library directory scans)
Recommended tools
jq(optional; improves JSON-heavy troubleshooting workflows)
Script prerequisites
- Scripts live in
devops-skills-plugin/skills/jenkinsfile-validator/scripts/ - Main orchestrator can run child scripts even if
+xis missing (it usesbashfallback) - If you want direct execution (
./script.sh), make scripts executable:
chmod +x devops-skills-plugin/skills/jenkinsfile-validator/scripts/*.shPreflight check (recommended)
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
command -v bash grep sed awk head wc find >/dev/null && echo "required tools: ok" || echo "required tools: missing"
command -v jq >/dev/null && echo "jq: installed (optional)" || echo "jq: missing (optional)"
[ -d "$SKILL_DIR/scripts" ] && echo "scripts dir: ok" || echo "scripts dir: missing"
[ -f "$SKILL_DIR/scripts/validate_jenkinsfile.sh" ] && echo "main validator: ok" || echo "main validator: missing"Quick Start (Normalized Paths)
Use a single base path variable to avoid path ambiguity.
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
TARGET_JENKINSFILE="Jenkinsfile"
# Full validation (recommended)
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" "$TARGET_JENKINSFILE"Common options
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
TARGET_JENKINSFILE="Jenkinsfile"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --syntax-only "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --security-only "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --best-practices "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --no-security "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --no-best-practices "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --strict "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --assume-declarative "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" --assume-scripted "$TARGET_JENKINSFILE"Shared library validation
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
bash "$SKILL_DIR/scripts/validate_shared_library.sh" vars/myStep.groovy
bash "$SKILL_DIR/scripts/validate_shared_library.sh" vars/
bash "$SKILL_DIR/scripts/validate_shared_library.sh" src/
bash "$SKILL_DIR/scripts/validate_shared_library.sh" /path/to/shared-libraryRegression and local CI checks
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
bash "$SKILL_DIR/tests/run_local_ci.sh"run_local_ci.sh is the supported local/CI entrypoint for regression coverage. It runs:
bash -nsyntax checks for allscripts/*.shandtests/*.shfilestests/test_validate_jenkinsfile.shregression scenarios
Deterministic Validation Flow
1) Detect pipeline type
pipeline {=> Declarative validatornode (...)ornode {=> Scripted validator- Unknown => fails closed by default (
ERROR [TypeDetection]) - Override intentionally ambiguous files with
--assume-declarativeor--assume-scripted
2) Run syntax validation
- Declarative:
validate_declarative.sh - Scripted:
validate_scripted.sh
3) Run security scan
common_validation.sh check_credentials
4) Run best practices check
best_practices.sh
5) Aggregate and return final status
- Unified summary with pass/fail per phase and final exit code
6) Run regression suite after script changes
bash tests/run_local_ci.sh- Intended for both local pre-commit checks and CI job wiring
Individual Script Commands (Advanced)
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
TARGET_JENKINSFILE="Jenkinsfile"
# Type detection
bash "$SKILL_DIR/scripts/common_validation.sh" detect_type "$TARGET_JENKINSFILE"
# Syntax-only by type
bash "$SKILL_DIR/scripts/validate_declarative.sh" "$TARGET_JENKINSFILE"
bash "$SKILL_DIR/scripts/validate_scripted.sh" "$TARGET_JENKINSFILE"
# Security-only
bash "$SKILL_DIR/scripts/common_validation.sh" check_credentials "$TARGET_JENKINSFILE"
# Best-practices-only
bash "$SKILL_DIR/scripts/best_practices.sh" "$TARGET_JENKINSFILE"Exit Code and Log Interpretation
Main orchestrator: validate_jenkinsfile.sh
0: Validation passed1: Validation failed (syntax/security errors, or warnings in--strictmode)2: Usage or environment error (bad args, missing file, missing required tools)
Sub-scripts
validate_declarative.sh:0pass (errors=0),1usage/file/validation failurevalidate_scripted.sh:0pass (errors=0),1usage/file/validation failurecommon_validation.sh check_credentials:0no credential errors,1credential issues foundvalidate_shared_library.sh:0pass,1validation errors found,2invalid input targetbest_practices.sh:1only for usage/file errors; content findings are reported in logs and score output
Log severity patterns
ERROR [Line N]: ...=> must fixWARNING [Line N]: ...=> should reviewINFO [Line N]: ...=> optional improvement- Summary banners (
VALIDATION PASSED/FAILED) determine final interpretation quickly
Practical interpretation rules
- For CI gating, rely on main orchestrator exit code.
- Use
--strictwhen warnings should fail pipelines. - When
best_practices.shis run standalone, read report sections (CRITICAL ISSUES,IMPROVEMENTS RECOMMENDED, score); do not rely only on exit code.
Fallback Behavior
Missing optional tools
- If
jqis missing, continue validation; treat as non-blocking.
Non-executable child scripts
- Main orchestrator warns and falls back to
bash <script>execution.
Missing child scripts
- Main orchestrator reports runner error and returns failure for that phase.
Unknown plugin steps
Use this order: 1. Check local reference: devops-skills-plugin/skills/jenkinsfile-validator/references/common_plugins.md 2. Context7 lookup:
mcp__context7__resolve-library-idwith query likejenkinsci <plugin-name>-pluginmcp__context7__query-docsfor usage and parameters
3. Web fallback: plugins.jenkins.io and official Jenkins docs
Offline/air-gapped mode
- Run all local validators.
- If plugin docs cannot be fetched, report: "Plugin docs lookup skipped due to environment constraints; local validation only."
Plugin Documentation Lookup Workflow
When plugin-specific validation is requested: 1. Identify unknown steps from Jenkinsfile or validator logs. 2. Check references/common_plugins.md first. 3. If missing, use Context7 (resolve-library-id then query-docs). 4. If still missing, use web search against official plugin index/docs. 5. Return required parameters, optional parameters, version-sensitive notes, and security guidance.
References
Local references:
devops-skills-plugin/skills/jenkinsfile-validator/references/declarative_syntax.mddevops-skills-plugin/skills/jenkinsfile-validator/references/scripted_syntax.mddevops-skills-plugin/skills/jenkinsfile-validator/references/best_practices.mddevops-skills-plugin/skills/jenkinsfile-validator/references/common_plugins.md
External references:
Reporting Template
Use this structure in validation responses:
Validation Target: <path>
Pipeline Type: <Declarative|Scripted|Shared Library|Unknown>
Findings:
- ERROR [Line X]: <issue>
- WARNING [Line Y]: <issue>
- INFO [Line Z]: <suggestion>
Phase Results:
- Syntax: <PASSED|FAILED|SKIPPED>
- Security: <PASSED|FAILED|SKIPPED>
- Best Practices: <PASSED|REVIEW NEEDED|SKIPPED>
Exit Code: <0|1|2>
Next Actions:
1. <highest-priority fix>
2. <second fix>Example Flows
Example 1: Full Jenkinsfile validation
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
bash "$SKILL_DIR/scripts/validate_jenkinsfile.sh" JenkinsfileExpected behavior:
- Runs syntax + security + best practices
- Prints per-phase results and unified summary
- Returns
0/1/2per orchestrator rules
Example 2: Shared library directory validation
SKILL_DIR="devops-skills-plugin/skills/jenkinsfile-validator"
bash "$SKILL_DIR/scripts/validate_shared_library.sh" examples/shared-libraryExpected behavior:
- Validates both
vars/andsrc/files - Aggregates issues with line references
- Returns
1when errors are present
Example 3: Unknown plugin step follow-up
Input step:
nexusArtifactUploader artifacts: [[...]], nexusUrl: 'https://nexus.example.com'Flow: 1. Validate locally first. 2. If step behavior is unclear, resolve docs via Context7. 3. If unavailable, use plugin site docs. 4. Report usage guidance and security-safe parameter patterns.
Done Criteria
The skill usage is complete when all are true:
- Commands use normalized paths (
$SKILL_DIR/scripts/...) with no cwd ambiguity. - Prerequisites and optional dependencies are explicit.
- Exit-code semantics and log-severity interpretation are documented.
- Fallback behavior is defined for missing tools/docs and constrained environments.
- At least one runnable example exists for full validation and shared-library validation.
- Reporting format is deterministic and actionable.
// BAD DECLARATIVE PIPELINE - Contains multiple validation issues
// This file is intentionally written poorly to demonstrate validation capabilities
// Missing 'pipeline' wrapper - ERROR
stages {
// Missing 'agent' directive - ERROR
// Stage without proper name quotes - ERROR
stage(Build Stage) {
// Missing 'steps' block - ERROR
echo 'Building...'; // Unnecessary semicolon - WARNING
sh 'echo "Starting build"'; // Multiple individual sh steps - WARNING
sh 'mkdir build';
sh 'cd build';
sh 'make';
sh 'echo "Build complete"';
}
stage('Test') {
steps {
// Hardcoded credentials - ERROR
sh 'docker login -u admin -p password123'
// Hardcoded API key - ERROR
sh 'curl -H "Authorization: Bearer abc123xyz789" https://api.example.com'
// AWS credentials hardcoded - CRITICAL ERROR
sh 'aws configure set aws_access_key_id AKIAIOSFODNN7EXAMPLE'
// Test commands without result publishing - WARNING
sh 'mvn test'
// Missing: junit '**/target/test-results/*.xml'
}
}
// Directive in wrong place - ERROR
environment {
// Should be at pipeline level, not inside stages
api_key = "hardcoded-secret-key" // Bad variable naming + hardcoded secret - ERROR
}
stage('Deploy') {
// No 'when' condition for production deploy - WARNING
steps {
// JsonSlurper on controller - WARNING
script {
def jsonSlurper = new groovy.json.JsonSlurper()
def config = jsonSlurper.parseText(readFile('config.json'))
}
// HTTP request on controller - WARNING
script {
def response = new URL('http://api.example.com').getText()
}
sh 'kubectl apply -f deployment.yaml'
}
}
}
// Missing post section - INFO
// No error handling - WARNING
// No timeout configuration - WARNING
// No workspace cleanup - INFO
// No notifications - INFO
// No artifact archiving - INFO
// No parallel execution for independent stages - INFO
/* Additional Issues:
* - No build discarder option
* - No timestamps option
* - No parameters defined
* - No triggers configured
* - No credential management
* - No proper environment variable naming
* - No Docker/container usage for consistent builds
* - Build could hang forever (no timeout)
* - No input validation
* - No test result publishing
* - Pipeline could be >500 lines (maintainability issue if expanded)
*/// BAD SCRIPTED PIPELINE - Contains multiple validation issues
// This file is intentionally written poorly to demonstrate validation capabilities
// No timestamps wrapper - INFO
// No ansiColor wrapper - INFO
// No timeout wrapper - WARNING
// Missing 'node' block - WARNING
stage('Checkout') {
// Code outside node block - not recommended
checkout scm; // Unnecessary semicolon - WARNING
}
node {
// Unbalanced brace example (will be caught)
stage('Build') {
// Multiple individual sh steps - WARNING
sh 'echo "Starting build"';
sh 'mkdir -p build';
sh 'cd build';
sh 'cmake ..';
sh 'make';
sh 'echo "Build complete"';
// Hardcoded credentials - CRITICAL ERROR
sh 'git config user.name "admin"'
sh 'git config user.password "supersecret123"'
// Hardcoded API token - ERROR
def apiToken = 'sk-1234567890abcdefghijklmnop'
sh "curl -H 'Authorization: Bearer ${apiToken}' https://api.example.com"
// Variable without 'def' - INFO
myVar = 'global variable' // Should use 'def' for proper scoping
// Variable with bad interpolation - WARNING
def message = 'Build number: $BUILD_NUMBER' // Should use double quotes
}
stage('Test') {
// No try-catch error handling - WARNING
sh 'npm test' // Could fail without proper handling
sh 'pytest tests/'
// No test result publishing - WARNING
// Missing: junit '**/test-results/*.xml'
// JsonSlurper on controller - WARNING (performance issue)
def jsonSlurper = new groovy.json.JsonSlurper()
def testResults = jsonSlurper.parseText(readFile('results.json'))
// Large file read on controller - INFO
def logFile = readFile('huge-application.log') // Could be huge!
def lines = logFile.split('\n')
}
stage('Security Scan') {
// Hardcoded database credentials - CRITICAL ERROR
def dbUser = "admin"
def dbPass = "P@ssw0rd123!"
sh "mysql -u ${dbUser} -p${dbPass} -e 'SELECT * FROM users'"
// Hardcoded SSH key - CRITICAL ERROR
sh 'echo "-----BEGIN RSA PRIVATE KEY-----" > /tmp/key.pem'
// Base64 encoded credential (still detectable) - INFO
def encodedCred = "YWRtaW46cGFzc3dvcmQ=" // admin:password in base64
}
stage('Docker Build') {
// Docker build without proper tagging - WARNING
sh 'docker build .'
// No Docker registry credentials - if pushing
sh 'docker push myapp' // Will likely fail
}
stage('Complex Logic') {
// Method that should be @NonCPS but isn't
def processData(data) {
// Complex iteration that should be @NonCPS
return data.collect { it.toUpperCase() }
}
// Using pipeline steps in what should be @NonCPS - ERROR if marked
@NonCPS
def badMethod() {
sh 'echo "This will fail!"' // Can't use pipeline steps in @NonCPS
sleep 5 // Can't use async steps in @NonCPS
}
}
stage('Parallel Deploy') {
// Parallel without failFast - INFO
parallel(
'Deploy US': {
sh 'deploy-us.sh'
},
'Deploy EU': {
sh 'deploy-eu.sh'
}
// Missing failFast: true
)
}
stage('Manual Approval') {
// Input without timeout - WARNING
input message: 'Deploy to production?' // Could wait forever!
// No submitter restriction - INFO
// Missing: submitter: 'admin,ops'
}
stage('Production Deploy') {
// No environment check - WARNING
// Should check: if (env.BRANCH_NAME == 'main')
// HTTP request on controller - WARNING
def response = sh(script: 'curl http://api.example.com/status', returnStdout: true)
// Should use curl on agent with jq instead of parsing on controller
sh 'kubectl apply -f production.yaml'
}
// No cleanup at end - INFO
// Missing: cleanWs() or deleteDir()
}
// No error handling (try-catch-finally) - WARNING
// No currentBuild.result setting
// No notifications on failure - INFO
// No proper logging
// No workspace cleanup - INFO
// Unmatched braces (will be caught by validator)
def brokenFunction() {
if (true) {
echo "Missing closing brace"
// Missing }
}
// Unused variable (will be detected)
def unusedVar = 'never used'
// Missing properties configuration
// No build parameters defined
// No build discarder
// No concurrent build control
/* Additional Issues:
* - No retry for flaky operations
* - No stash/unstash for artifacts between agents
* - No proper Git operations (tags, etc.)
* - No Docker inside() for consistent environment
* - No Kubernetes pod template
* - No proper credential management with withCredentials
* - No environment variable validation
* - Multiple stages could run in parallel but don't
* - No archiveArtifacts for build outputs
* - No fingerprinting
* - No build description or display name
* - Could exceed reasonable pipeline size
* - Few/no comments for complex logic
* - Hardcoded values instead of parameters
* - No version tagging
* - No rollback capability
*/
// Unmatched quotes (will be caught)
def badString = "unclosed string
// Unmatched parentheses (will be caught)
def badFunction(param {
echo "missing closing paren"
}// Good Declarative Pipeline Example - Docker CI/CD
// This pipeline demonstrates best practices for Docker-based builds
pipeline {
agent {
docker {
image 'maven:3.8.1-adoptopenjdk-11'
args '-v $HOME/.m2:/root/.m2'
}
}
options {
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5'))
disableConcurrentBuilds()
timeout(time: 1, unit: 'HOURS')
timestamps()
ansiColor('xterm')
}
parameters {
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Deployment environment')
booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Run test suite')
booleanParam(name: 'DEPLOY', defaultValue: false, description: 'Deploy after build')
}
environment {
APP_NAME = 'my-docker-app'
DOCKER_REGISTRY = 'registry.example.com'
DOCKER_IMAGE = "${DOCKER_REGISTRY}/${APP_NAME}:${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh '''
echo "Building application..."
mvn clean compile -DskipTests
'''
}
}
stage('Test') {
when {
expression { return params.RUN_TESTS }
}
failFast true
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test -Dtest=**/unit/**'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -Dtest=**/integration/**'
}
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('Package') {
steps {
sh 'mvn package -DskipTests'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
stage('Docker Build') {
agent any
steps {
script {
docker.build("${DOCKER_IMAGE}")
}
}
}
stage('Deploy') {
when {
allOf {
expression { return params.DEPLOY }
branch 'main'
}
}
input {
message "Deploy to ${params.ENVIRONMENT}?"
ok "Deploy"
submitter "ops,admin"
}
steps {
withCredentials([usernamePassword(
credentialsId: 'docker-registry-creds',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh '''
docker login -u $DOCKER_USER -p $DOCKER_PASS ${DOCKER_REGISTRY}
docker push ${DOCKER_IMAGE}
'''
}
}
}
}
post {
always {
cleanWs()
}
success {
echo "Build ${env.BUILD_NUMBER} succeeded!"
}
failure {
echo "Build ${env.BUILD_NUMBER} failed!"
}
}
}// Good Declarative Pipeline Example - Kubernetes CI/CD
// This pipeline demonstrates best practices for Kubernetes-based builds
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins: agent
spec:
containers:
- name: maven
image: maven:3.8.1-adoptopenjdk-11
command:
- cat
tty: true
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
- name: docker
image: docker:20.10
command:
- cat
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
- name: kubectl
image: bitnami/kubectl:latest
command:
- cat
tty: true
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
'''
}
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 1, unit: 'HOURS')
timestamps()
}
environment {
APP_NAME = 'k8s-app'
DOCKER_IMAGE = "myregistry/${APP_NAME}:${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
container('maven') {
sh '''
echo "Building with Maven..."
mvn clean package -DskipTests
'''
}
}
}
stage('Test') {
failFast true
parallel {
stage('Unit Tests') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
stage('Code Quality') {
steps {
container('maven') {
sh 'mvn checkstyle:check'
}
}
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('Docker Build') {
steps {
container('docker') {
sh "docker build -t ${DOCKER_IMAGE} ."
}
}
}
stage('Deploy to Dev') {
when {
branch 'develop'
}
steps {
container('kubectl') {
withCredentials([file(credentialsId: 'kubeconfig-dev', variable: 'KUBECONFIG')]) {
sh '''
kubectl --kubeconfig=$KUBECONFIG set image deployment/${APP_NAME} \
${APP_NAME}=${DOCKER_IMAGE}
kubectl --kubeconfig=$KUBECONFIG rollout status deployment/${APP_NAME}
'''
}
}
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input {
message "Deploy to production?"
ok "Deploy"
submitter "ops,admin"
}
steps {
container('kubectl') {
withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) {
sh '''
kubectl --kubeconfig=$KUBECONFIG set image deployment/${APP_NAME} \
${APP_NAME}=${DOCKER_IMAGE}
kubectl --kubeconfig=$KUBECONFIG rollout status deployment/${APP_NAME}
'''
}
}
}
}
}
post {
always {
cleanWs()
}
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
}
}// Good Declarative Pipeline Example - Parallel Stages
// This pipeline demonstrates best practices for parallel execution
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 45, unit: 'MINUTES')
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh '''
echo "Building application..."
mkdir -p dist
echo "Build complete"
'''
}
}
stage('Quality Gates') {
failFast true
parallel {
stage('Unit Tests') {
agent {
label 'linux'
}
steps {
sh 'echo "Running unit tests..."'
}
post {
always {
junit allowEmptyResults: true, testResults: '**/unit-test-results/*.xml'
}
}
}
stage('Integration Tests') {
agent {
label 'linux'
}
steps {
sh 'echo "Running integration tests..."'
}
post {
always {
junit allowEmptyResults: true, testResults: '**/integration-test-results/*.xml'
}
}
}
stage('Security Scan') {
agent {
label 'security'
}
steps {
sh 'echo "Running security scan..."'
}
}
stage('Code Quality') {
agent {
label 'linux'
}
steps {
sh 'echo "Running code quality checks..."'
}
}
}
}
stage('Package') {
steps {
sh 'echo "Creating deployment package..."'
archiveArtifacts artifacts: 'dist/**/*', fingerprint: true, allowEmptyArchive: true
}
}
stage('Deploy') {
failFast true
parallel {
stage('Deploy to Region US-East') {
steps {
sh 'echo "Deploying to US-East..."'
}
}
stage('Deploy to Region US-West') {
steps {
sh 'echo "Deploying to US-West..."'
}
}
stage('Deploy to Region EU') {
steps {
sh 'echo "Deploying to EU..."'
}
}
}
}
}
post {
always {
cleanWs()
}
success {
echo 'All parallel stages completed successfully!'
}
failure {
echo 'One or more parallel stages failed!'
}
}
}// Declarative Pipeline with Unknown/Custom Plugins
// This example is designed to test the plugin documentation lookup workflow
// It contains plugins NOT in references/common_plugins.md that require Claude to look them up
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 1, unit: 'HOURS')
timestamps()
}
environment {
APP_NAME = 'my-application'
APP_VERSION = '1.0.0'
NEXUS_URL = 'https://nexus.example.com'
SONAR_URL = 'https://sonarqube.example.com'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh '''
echo "Building ${APP_NAME} version ${APP_VERSION}"
./gradlew clean build -x test
'''
}
}
stage('Code Quality') {
steps {
// Unknown Plugin: SonarQube Scanner
// Claude should look this up: "Jenkins sonarqube scanner plugin"
withSonarQubeEnv('SonarQube') {
sh './gradlew sonarqube'
}
// Unknown Plugin: Quality Gate
// Part of SonarQube plugin
waitForQualityGate abortPipeline: true
}
}
stage('Security Scan') {
steps {
// Unknown Plugin: OWASP Dependency Check
// Claude should look this up
dependencyCheck additionalArguments: '--scan ./', odcInstallation: 'dependency-check'
dependencyCheckPublisher pattern: '**/dependency-check-report.xml'
}
}
stage('Publish Artifacts') {
steps {
// Unknown Plugin: Nexus Artifact Uploader
// Claude should look this up: "Jenkins nexus artifact uploader plugin"
nexusArtifactUploader(
nexusVersion: 'nexus3',
protocol: 'https',
nexusUrl: "${NEXUS_URL}",
groupId: 'com.example',
version: "${APP_VERSION}",
repository: 'maven-releases',
credentialsId: 'nexus-credentials',
artifacts: [
[artifactId: "${APP_NAME}", classifier: '', file: "build/libs/${APP_NAME}-${APP_VERSION}.jar", type: 'jar']
]
)
}
}
stage('Deploy to Kubernetes') {
steps {
// Unknown Plugin: Kubernetes Continuous Deploy
// Different from kubernetes plugin - Claude should look this up
kubernetesDeploy(
configs: 'k8s/*.yaml',
kubeConfig: [path: ''],
enableConfigSubstitution: true
)
}
}
stage('Performance Test') {
steps {
// Unknown Plugin: Performance Plugin
// Claude should look this up
perfReport sourceDataFiles: 'results/*.jtl',
compareBuildPrevious: true,
errorFailedThreshold: 5,
errorUnstableThreshold: 10
}
}
stage('Notify') {
steps {
// Unknown Plugin: Datadog
// Claude should look this up: "Jenkins datadog plugin"
datadogEvent(
title: "Deployment Complete",
text: "${APP_NAME} v${APP_VERSION} deployed successfully",
alertType: "success",
tags: ["app:${APP_NAME}", "version:${APP_VERSION}"]
)
// Unknown Plugin: Microsoft Teams
// Claude should look this up
office365ConnectorSend(
webhookUrl: 'https://outlook.office.com/webhook/xxx',
message: "Build ${BUILD_NUMBER} completed for ${APP_NAME}",
status: 'Success'
)
// Unknown Plugin: Jira
// Claude should look this up
jiraComment(
issueKey: 'PROJ-123',
body: "Deployed ${APP_NAME} v${APP_VERSION} - Build #${BUILD_NUMBER}"
)
}
}
}
post {
always {
cleanWs()
}
failure {
// Unknown Plugin: PagerDuty
// Claude should look this up
pagerduty(
resolve: false,
serviceKey: 'xxx',
incidentKey: "${JOB_NAME}",
incidentDescription: "Build failed for ${APP_NAME}"
)
}
}
}
/*
* UNKNOWN PLUGINS IN THIS FILE (for testing plugin lookup):
*
* 1. withSonarQubeEnv / waitForQualityGate - SonarQube Scanner Plugin
* 2. dependencyCheck / dependencyCheckPublisher - OWASP Dependency Check Plugin
* 3. nexusArtifactUploader - Nexus Artifact Uploader Plugin
* 4. kubernetesDeploy - Kubernetes Continuous Deploy Plugin
* 5. perfReport - Performance Plugin
* 6. datadogEvent - Datadog Plugin
* 7. office365ConnectorSend - Office 365 Connector Plugin
* 8. jiraComment - Jira Plugin
* 9. pagerduty - PagerDuty Plugin
*
* Claude should:
* 1. Run validation script (will pass syntax check)
* 2. Identify these as unknown plugins
* 3. Look up documentation using Context7 or WebSearch
* 4. Provide guidance on proper usage and parameters
*/// Good Scripted Pipeline Example - Basic CI
// This pipeline demonstrates best practices for scripted pipelines
// Build properties
properties([
buildDiscarder(logRotator(numToKeepStr: '10')),
disableConcurrentBuilds(),
parameters([
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Target environment'),
booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Run test suite')
])
])
// Global variables
def version
def buildSuccess = false
// Helper method for Groovy operations
@NonCPS
def parseVersion(String content) {
def matcher = (content =~ /version\s*=\s*['"](.+)['"]/)
return matcher ? matcher[0][1] : '1.0.0'
}
timeout(time: 30, unit: 'MINUTES') {
timestamps {
ansiColor('xterm') {
node('linux') {
try {
stage('Checkout') {
checkout scm
version = sh(script: 'git describe --tags --always', returnStdout: true).trim()
currentBuild.displayName = "#${env.BUILD_NUMBER} - ${version}"
}
stage('Build') {
sh '''
echo "Starting build..."
mkdir -p build
echo "Compiling application..."
echo "Build complete"
'''
}
if (params.RUN_TESTS) {
stage('Test') {
try {
sh 'echo "Running tests..."'
// Publish test results
junit allowEmptyResults: true, testResults: '**/test-results/*.xml'
} catch (Exception e) {
echo "Tests failed: ${e.message}"
throw e
}
}
}
stage('Package') {
sh 'echo "Packaging application..."'
archiveArtifacts artifacts: 'build/**/*', allowEmptyArchive: true, fingerprint: true
}
buildSuccess = true
currentBuild.result = 'SUCCESS'
} catch (Exception e) {
currentBuild.result = 'FAILURE'
echo "Pipeline failed: ${e.message}"
throw e
} finally {
stage('Cleanup') {
cleanWs()
// Send notification
def status = buildSuccess ? 'succeeded' : 'failed'
echo "Build ${env.BUILD_NUMBER} ${status}"
}
}
}
}
}
}// Good Scripted Pipeline Example - Conditional and Parallel Execution
// This pipeline demonstrates best practices for conditional logic and parallel stages
// Build properties
properties([
buildDiscarder(logRotator(numToKeepStr: '10')),
parameters([
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Target environment'),
booleanParam(name: 'RUN_SECURITY_SCAN', defaultValue: true, description: 'Run security scan'),
booleanParam(name: 'RUN_PERFORMANCE_TEST', defaultValue: false, description: 'Run performance tests')
])
])
// Global variables
def testResults = [:]
// Non-CPS method for complex Groovy operations
@NonCPS
def aggregateResults(Map results) {
def total = results.values().sum { it ? 1 : 0 }
return "Passed: ${total}/${results.size()}"
}
timestamps {
ansiColor('xterm') {
timeout(time: 1, unit: 'HOURS') {
node('linux') {
try {
stage('Checkout') {
checkout scm
currentBuild.displayName = "#${env.BUILD_NUMBER} - ${env.BRANCH_NAME ?: 'unknown'}"
}
stage('Build') {
sh '''
echo "Building application..."
mkdir -p build
echo "Build complete"
'''
}
stage('Parallel Tests') {
def branches = [:]
branches['failFast'] = true
branches['Unit Tests'] = {
node('linux') {
try {
sh 'echo "Running unit tests..."'
testResults['unit'] = true
} catch (Exception e) {
testResults['unit'] = false
throw e
}
}
}
branches['Integration Tests'] = {
node('linux') {
try {
sh 'echo "Running integration tests..."'
testResults['integration'] = true
} catch (Exception e) {
testResults['integration'] = false
throw e
}
}
}
if (params.RUN_SECURITY_SCAN) {
branches['Security Scan'] = {
node('security') {
try {
sh 'echo "Running security scan..."'
testResults['security'] = true
} catch (Exception e) {
testResults['security'] = false
throw e
}
}
}
}
if (params.RUN_PERFORMANCE_TEST) {
branches['Performance Tests'] = {
node('performance') {
try {
sh 'echo "Running performance tests..."'
testResults['performance'] = true
} catch (Exception e) {
testResults['performance'] = false
throw e
}
}
}
}
parallel branches
}
// Conditional deployment based on branch
if (env.BRANCH_NAME == 'main' || env.BRANCH_NAME == 'master') {
stage('Deploy to Production') {
timeout(time: 30, unit: 'MINUTES') {
input message: 'Deploy to production?', submitter: 'ops,admin'
}
withCredentials([string(credentialsId: 'deploy-token', variable: 'DEPLOY_TOKEN')]) {
sh 'echo "Deploying to production..."'
}
}
} else if (env.BRANCH_NAME == 'develop') {
stage('Deploy to Staging') {
withCredentials([string(credentialsId: 'deploy-token', variable: 'DEPLOY_TOKEN')]) {
sh 'echo "Deploying to staging..."'
}
}
} else {
echo "Skipping deployment for branch: ${env.BRANCH_NAME}"
}
currentBuild.result = 'SUCCESS'
echo "Test results: ${aggregateResults(testResults)}"
} catch (Exception e) {
currentBuild.result = 'FAILURE'
echo "Pipeline failed: ${e.message}"
throw e
} finally {
stage('Cleanup') {
cleanWs()
echo "Build ${env.BUILD_NUMBER} completed with status: ${currentBuild.result}"
}
}
}
}
}
}// Good Scripted Pipeline Example - Docker CI/CD
// This pipeline demonstrates best practices for Docker-based scripted pipelines
// Build properties
properties([
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5')),
disableConcurrentBuilds(),
parameters([
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Target environment'),
booleanParam(name: 'DEPLOY', defaultValue: false, description: 'Deploy after build')
])
])
// Global variables
def dockerImage
def version
timestamps {
ansiColor('xterm') {
timeout(time: 1, unit: 'HOURS') {
node('docker') {
try {
stage('Checkout') {
checkout scm
version = sh(script: 'git describe --tags --always', returnStdout: true).trim()
currentBuild.displayName = "#${env.BUILD_NUMBER} - ${version}"
}
stage('Build') {
docker.image('maven:3.8.1-adoptopenjdk-11').inside('-v $HOME/.m2:/root/.m2') {
sh '''
echo "Building with Maven..."
mvn clean package -DskipTests
'''
}
}
stage('Test') {
docker.image('maven:3.8.1-adoptopenjdk-11').inside('-v $HOME/.m2:/root/.m2') {
try {
sh 'mvn test'
} finally {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('Docker Build') {
dockerImage = docker.build("myapp:${version}", "-f Dockerfile .")
}
if (params.DEPLOY) {
stage('Push to Registry') {
withCredentials([usernamePassword(
credentialsId: 'docker-registry-creds',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
docker.withRegistry('https://registry.example.com', 'docker-registry-creds') {
dockerImage.push()
dockerImage.push('latest')
}
}
}
if (params.ENVIRONMENT == 'production') {
stage('Approval') {
timeout(time: 1, unit: 'HOURS') {
input message: 'Deploy to production?', submitter: 'ops,admin'
}
}
}
stage('Deploy') {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
sh """
kubectl --kubeconfig=\$KUBECONFIG set image deployment/myapp myapp=myapp:${version}
kubectl --kubeconfig=\$KUBECONFIG rollout status deployment/myapp
"""
}
}
}
currentBuild.result = 'SUCCESS'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
} catch (Exception e) {
currentBuild.result = 'FAILURE'
echo "Pipeline failed: ${e.message}"
throw e
} finally {
stage('Cleanup') {
cleanWs()
echo "Build ${env.BUILD_NUMBER} completed with status: ${currentBuild.result}"
}
}
}
}
}
}package com.example
import groovy.transform.ToString
/**
* BuildConfig - Configuration class for build parameters
* Implements Serializable for CPS compatibility
*/
@ToString
class BuildConfig implements Serializable {
private static final long serialVersionUID = 1L
String language
String buildCommand
String testCommand
boolean skipTests = false
Map<String, String> environment = [:]
BuildConfig() {}
BuildConfig(Map config) {
this.language = config.language ?: 'java'
this.buildCommand = config.buildCommand
this.testCommand = config.testCommand
this.skipTests = config.skipTests ?: false
this.environment = config.environment ?: [:]
}
/**
* Validate configuration
*/
void validate() {
if (!language) {
throw new IllegalArgumentException("Language is required")
}
}
/**
* Get default build command for language
*/
String getDefaultBuildCommand() {
switch (language) {
case 'java': return './mvnw clean package'
case 'node': return 'npm run build'
case 'python': return 'python setup.py build'
default: return buildCommand
}
}
}// BAD SHARED LIBRARY EXAMPLE - Contains multiple validation issues
// This file is intentionally written poorly to demonstrate validation capabilities
// Missing documentation comment - WARNING
// Bad filename: should be camelCase starting with lowercase (badStep.groovy) - WARNING
// Missing call() method - WARNING
def execute(String command) {
// Hardcoded credentials - ERROR
def password = "supersecret123"
def apiKey = "sk-1234567890abcdef"
// Using System.getenv instead of env - WARNING
def home = System.getenv("HOME")
// Using Thread.sleep instead of sleep() step - WARNING
Thread.sleep(5000)
// Using new File() instead of readFile - WARNING
def config = new File('config.json').text
// HTTP request on controller - WARNING
def url = new URL("http://api.example.com/data")
def response = url.text
// JsonSlurper on controller without @NonCPS - WARNING
def slurper = new groovy.json.JsonSlurper()
def data = slurper.parseText(response)
// Using closures without @NonCPS - INFO
data.items.each { item ->
echo "Processing: ${item.name}"
}
sh command
}
// @NonCPS method with pipeline steps - ERROR
@NonCPS
def processWithSteps() {
sh 'echo "This will fail!"' // Cannot use pipeline steps in @NonCPS
echo "This too!"
sleep 5 // Async steps not allowed in @NonCPS
}
// Method with closure that should be @NonCPS
def transformData(List items) {
// .collect{} requires @NonCPS - INFO
return items.collect { it.toUpperCase() }
}/**
* buildApp - Build and test an application
*
* @param config Map containing build configuration:
* - language: Programming language (java, node, python)
* - testCommand: Command to run tests (optional)
* - buildCommand: Command to build (optional)
* @return Map with build results
*
* Example usage:
* buildApp(language: 'java')
* buildApp(language: 'node', testCommand: 'npm test')
*/
def call(Map config = [:]) {
def language = config.language ?: 'java'
def testCommand = config.testCommand
def buildCommand = config.buildCommand
def results = [:]
try {
stage('Build') {
echo "Building ${language} application..."
switch (language) {
case 'java':
sh '''
./mvnw clean package -DskipTests
echo "Build complete"
'''
break
case 'node':
sh '''
npm ci
npm run build
'''
break
case 'python':
sh '''
pip install -r requirements.txt
python setup.py build
'''
break
default:
if (buildCommand) {
sh buildCommand
} else {
error "Unknown language: ${language}"
}
}
results.buildStatus = 'SUCCESS'
}
stage('Test') {
echo "Running tests..."
try {
if (testCommand) {
sh testCommand
} else {
switch (language) {
case 'java':
sh './mvnw test'
break
case 'node':
sh 'npm test'
break
case 'python':
sh 'pytest tests/'
break
}
}
results.testStatus = 'SUCCESS'
} catch (Exception e) {
results.testStatus = 'FAILED'
results.testError = e.message
throw e
} finally {
// Publish test results
junit allowEmptyResults: true, testResults: '**/test-results/*.xml'
}
}
return results
} catch (Exception e) {
results.error = e.message
throw e
}
}
/**
* Helper method to get version from pom.xml
* Must be @NonCPS because it uses Groovy XML parsing
*/
@NonCPS
def getVersionFromPom(String pomFile = 'pom.xml') {
def pom = new XmlSlurper().parse(pomFile)
return pom.version.text()
}
/**
* Helper method to parse JSON configuration
* @NonCPS required for JsonSlurperClassic
*/
@NonCPS
def parseConfig(String jsonText) {
def slurper = new groovy.json.JsonSlurperClassic()
return slurper.parseText(jsonText)
}Jenkins Pipeline Best Practices
Comprehensive guide based on official Jenkins documentation and community best practices.
Performance Best Practices
1. Combine Shell Commands
Bad:
sh 'echo "Starting build"'
sh 'mkdir build'
sh 'cd build'
sh 'cmake ..'
sh 'make'
sh 'echo "Build complete"'Good:
sh '''
echo "Starting build"
mkdir build
cd build
cmake ..
make
echo "Build complete"
'''Why: Each sh step has start-up and tear-down overhead. Combining commands reduces this overhead and improves performance.
2. Use Agent-Based Operations
Bad (runs on controller):
@NonCPS
def parseJson(String jsonString) {
def jsonSlurper = new groovy.json.JsonSlurper()
return jsonSlurper.parseText(jsonString)
}
def data = readFile('data.json')
def parsed = parseJson(data)Good (runs on agent):
def result = sh(script: 'jq ".field" data.json', returnStdout: true).trim()Why: Controller resources are shared across all builds. Heavy operations should run on agents to prevent controller bottlenecks.
3. Minimize Data Transfer to Controller
Bad:
def logFile = readFile('huge-log.txt') // Loads entire file into controller memory
def lines = logFile.split('\n')Good:
def errorCount = sh(script: 'grep ERROR huge-log.txt | wc -l', returnStdout: true).trim()Why: Reduces memory usage on controller and network transfer time.
Security Best Practices
1. Never Hardcode Credentials
Bad:
sh 'docker login -u admin -p password123'
sh 'curl -H "Authorization: Bearer abc123xyz" https://api.example.com'Good:
withCredentials([usernamePassword(
credentialsId: 'docker-hub',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh 'docker login -u $DOCKER_USER -p $DOCKER_PASS'
}
withCredentials([string(credentialsId: 'api-token', variable: 'API_TOKEN')]) {
sh 'curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com'
}Why: Credentials stored in Jenkins Credentials Manager are encrypted and access-controlled.
2. Use Credentials Binding
Good:
environment {
AWS_CREDENTIALS = credentials('aws-credentials-id')
// Creates AWS_CREDENTIALS_USR and AWS_CREDENTIALS_PSW
}3. Validate User Input
Bad:
parameters {
string(name: 'BRANCH', defaultValue: '', description: 'Branch to build')
}
sh "git checkout ${params.BRANCH}" // Injection risk!Good:
parameters {
choice(name: 'BRANCH', choices: ['main', 'develop', 'release'], description: 'Branch to build')
}
// Or validate input
def branch = params.BRANCH
if (!branch.matches(/^[a-zA-Z0-9_\-\/]+$/)) {
error "Invalid branch name: ${branch}"
}Reliability Best Practices
1. Use Timeouts
Good:
// Declarative
options {
timeout(time: 1, unit: 'HOURS')
}
// Scripted
timeout(time: 30, unit: 'MINUTES') {
node {
// steps
}
}Why: Prevents builds from hanging indefinitely and consuming resources.
2. Implement Error Handling
Declarative:
post {
always {
cleanWs()
}
success {
slackSend color: 'good', message: "Build succeeded"
}
failure {
mail to: 'team@example.com',
subject: "Build Failed: ${currentBuild.fullDisplayName}",
body: "Check ${env.BUILD_URL}"
}
}Scripted:
node {
try {
stage('Build') {
sh 'make build'
}
stage('Test') {
sh 'make test'
}
} catch (Exception e) {
currentBuild.result = 'FAILURE'
mail to: 'team@example.com',
subject: "Build Failed",
body: "Error: ${e.message}"
throw e
} finally {
cleanWs()
}
}3. Use Proper Workspace Cleanup
Good:
post {
always {
cleanWs()
}
}
// Or for specific cleanup
post {
cleanup {
deleteDir()
}
}Why: Ensures consistent build environment and prevents disk space issues.
4. Implement Retries for Flaky Operations
Good:
retry(3) {
sh 'curl -f https://flaky-api.example.com/data'
}
// Or with exponential backoff
script {
def attempts = 0
retry(3) {
attempts++
if (attempts > 1) {
sleep time: attempts * 10, unit: 'SECONDS'
}
sh 'flaky-command'
}
}Maintainability Best Practices
1. Use Shared Libraries
Bad: Copy-pasting common code across Jenkinsfiles
Good:
@Library('my-shared-library@master') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildMavenProject() // From shared library
}
}
stage('Deploy') {
steps {
deployToKubernetes(env: 'production') // From shared library
}
}
}
}2. Use Descriptive Stage Names
Bad:
stage('Step 1') { }
stage('Step 2') { }Good:
stage('Build Application') { }
stage('Run Unit Tests') { }
stage('Build Docker Image') { }
stage('Deploy to Staging') { }3. Add Comments for Complex Logic
Good:
script {
// Calculate next version based on git tags
def lastTag = sh(script: 'git describe --tags --abbrev=0', returnStdout: true).trim()
def (major, minor, patch) = lastTag.tokenize('.')
// Increment patch version for feature branches
if (env.BRANCH_NAME.startsWith('feature/')) {
patch = patch.toInteger() + 1
}
def nextVersion = "${major}.${minor}.${patch}"
echo "Next version: ${nextVersion}"
}4. Break Long Pipelines into Stages
Good:
pipeline {
stages {
stage('Preparation') {
stages {
stage('Checkout') { }
stage('Setup Environment') { }
}
}
stage('Build') {
stages {
stage('Compile') { }
stage('Package') { }
}
}
stage('Quality Checks') {
parallel {
stage('Unit Tests') { }
stage('Integration Tests') { }
stage('Code Analysis') { }
}
}
}
}Optimization Best Practices
1. Use Parallel Execution
Good:
stage('Tests') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify'
}
}
stage('E2E Tests') {
steps {
sh 'npm run e2e'
}
}
}
}2. Use failFast with Parallel
Good:
stage('Deploy') {
failFast true
parallel {
stage('Region 1') { }
stage('Region 2') { }
stage('Region 3') { }
}
}Why: Stops remaining parallel tasks immediately if one fails, saving time and resources.
3. Use Stash/Unstash for Artifacts
Good:
node('build-agent') {
stage('Build') {
sh 'mvn package'
stash name: 'app-jar', includes: 'target/*.jar'
}
}
node('test-agent') {
stage('Test') {
unstash 'app-jar'
sh 'java -jar target/*.jar --test'
}
}4. Skip Default Checkout When Not Needed
Good:
options {
skipDefaultCheckout() // Don't checkout automatically
}
stages {
stage('Build') {
steps {
checkout scm // Checkout only when needed
}
}
}Docker Best Practices
1. Use Docker Agents for Consistent Environment
Good:
agent {
docker {
image 'maven:3.8.1-adoptopenjdk-11'
args '-v $HOME/.m2:/root/.m2'
}
}2. Reuse Docker Images
Bad:
sh 'docker run maven:3.8.1 mvn clean'
sh 'docker run maven:3.8.1 mvn compile'
sh 'docker run maven:3.8.1 mvn package'Good:
docker.image('maven:3.8.1').inside {
sh 'mvn clean compile package'
}3. Build Once, Deploy Many Times
Good:
stage('Build') {
steps {
script {
dockerImage = docker.build("myapp:${env.BUILD_NUMBER}")
}
}
}
stage('Test') {
steps {
script {
dockerImage.inside {
sh 'run-tests.sh'
}
}
}
}
stage('Deploy to Staging') {
steps {
script {
dockerImage.push('staging')
}
}
}
stage('Deploy to Production') {
steps {
script {
dockerImage.push('production')
dockerImage.push('latest')
}
}
}Kubernetes Best Practices
1. Use Resource Limits
Good:
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.8.1
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
'''
}
}2. Use Service Accounts
Good:
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins-agent
containers:
- name: kubectl
image: bitnami/kubectl:latest
'''
}
}Testing Best Practices
1. Always Publish Test Results
Good:
post {
always {
junit '**/target/test-results/*.xml'
publishHTML([
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}2. Archive Artifacts
Good:
post {
success {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}3. Separate Build and Test Stages
Good:
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/test-results/*.xml'
}
}
}
}Build Trigger Best Practices
1. Use Webhooks Instead of Polling
Bad:
triggers {
pollSCM('H/5 * * * *') // Polls every 5 minutes
}Good: Configure webhooks in your repository to trigger builds on push/PR
Why: Webhooks are more efficient and provide faster feedback than polling.
2. Use Appropriate Cron Syntax
Good:
triggers {
cron('H 2 * * *') // Daily at ~2 AM (H for hash-based distribution)
cron('H H(0-7) * * *') // Once between midnight and 7 AM
}Notification Best Practices
1. Send Notifications for Important Events
Good:
post {
failure {
slackSend (
color: 'danger',
message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
)
}
fixed {
slackSend (
color: 'good',
message: "Build FIXED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}2. Include Relevant Information
Good:
post {
failure {
mail to: 'team@example.com',
subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
Build: ${env.BUILD_URL}
Branch: ${env.BRANCH_NAME}
Commit: ${env.GIT_COMMIT}
Author: ${env.CHANGE_AUTHOR}
Please check the build logs for details.
"""
}
}Multi-Branch Pipeline Best Practices
1. Use Branch-Specific Logic
Good:
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'deploy-production.sh'
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
sh 'deploy-staging.sh'
}
}2. Use Pull Request Triggers
Good:
stage('PR Validation') {
when {
changeRequest()
}
steps {
sh 'run-pr-checks.sh'
}
}Credential Management Best Practices
1. Use Least Privilege
- Create separate credentials for different purposes
- Use read-only credentials where possible
- Rotate credentials regularly
2. Use Credential Domains
Organize credentials by domain (global, project-specific, etc.)
3. Mask Sensitive Output
Good:
withCredentials([string(credentialsId: 'api-key', variable: 'API_KEY')]) {
wrap([$class: 'MaskPasswordsBuildWrapper']) {
sh 'echo "Using API key: $API_KEY"' // Will be masked in logs
}
}Pipeline Configuration Best Practices
1. Use Build Discarder
Good:
options {
buildDiscarder(logRotator(
numToKeepStr: '10', // Keep last 10 builds
daysToKeepStr: '30', // Keep builds from last 30 days
artifactNumToKeepStr: '5', // Keep artifacts from last 5 builds
artifactDaysToKeepStr: '14' // Keep artifacts from last 14 days
))
}2. Disable Concurrent Builds When Needed
Good:
options {
disableConcurrentBuilds()
}3. Use Timestamps
Good:
options {
timestamps()
}Summary Checklist
- [ ] Combine multiple shell commands into single steps
- [ ] Use agent-based operations, not controller-based
- [ ] Never hardcode credentials
- [ ] Implement timeouts for all builds
- [ ] Add proper error handling (try-catch, post blocks)
- [ ] Clean workspace after builds
- [ ] Use parallel execution for independent tasks
- [ ] Publish test results and artifacts
- [ ] Send notifications for important events
- [ ] Use webhooks instead of polling
- [ ] Implement retries for flaky operations
- [ ] Use descriptive stage names
- [ ] Add comments for complex logic
- [ ] Use shared libraries for common code
- [ ] Configure build discarder
- [ ] Use Docker for consistent build environment
- [ ] Set resource limits for Kubernetes pods
- [ ] Validate user input
- [ ] Use least-privilege credentials
- [ ] Separate build and test stages
References
Common Jenkins Plugins Reference
Documentation for frequently used Jenkins plugins in pipelines.
Table of Contents
1. Git Plugin 2. Docker Plugin 3. Kubernetes Plugin 4. Credentials Plugin 5. Pipeline Utility Steps 6. JUnit Plugin 7. HTML Publisher Plugin 8. Slack Notification Plugin 9. Email Extension Plugin 10. Build Timeout Plugin 11. Timestamper Plugin 12. AnsiColor Plugin 13. Workspace Cleanup Plugin
---
Git Plugin
Provides Git repository access for Jenkins jobs.
Checkout SCM
Declarative:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
}
}Scripted:
node {
checkout scm
}Explicit Git Checkout
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
userRemoteConfigs: [[
url: 'https://github.com/user/repo.git',
credentialsId: 'github-credentials'
]]
])
// With multiple remotes
checkout([
$class: 'GitSCM',
branches: [[name: '*/develop']],
userRemoteConfigs: [
[url: 'https://github.com/user/repo.git', name: 'origin'],
[url: 'https://github.com/upstream/repo.git', name: 'upstream']
]
])Git Operations
// Get commit hash
def commit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
// Get short commit hash
def shortCommit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
// Get current branch
def branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
// Get commit author
def author = sh(script: 'git log -1 --pretty=%an', returnStdout: true).trim()
// Get commit message
def message = sh(script: 'git log -1 --pretty=%B', returnStdout: true).trim()
// Tag commit
sh "git tag -a v${env.BUILD_NUMBER} -m 'Release ${env.BUILD_NUMBER}'"
sh 'git push origin --tags'Environment Variables
GIT_COMMIT- Current commit hashGIT_BRANCH- Branch nameGIT_PREVIOUS_COMMIT- Previous commitGIT_PREVIOUS_SUCCESSFUL_COMMIT- Last successful build commitGIT_URL- Repository URLGIT_AUTHOR_NAME- Commit author nameGIT_AUTHOR_EMAIL- Commit author email
---
Docker Plugin
Jenkins plugin for running builds in Docker containers.
Docker Agent
Declarative:
pipeline {
agent {
docker {
image 'maven:3.8.1-adoptopenjdk-11'
args '-v /tmp:/tmp'
label 'docker-agent'
}
}
stages {
stage('Build') {
steps {
sh 'mvn --version'
}
}
}
}Docker in Scripted Pipeline
node {
// Run inside container
docker.image('maven:3.8.1').inside {
sh 'mvn clean package'
}
// With additional arguments
docker.image('node:14').inside('-v /tmp:/tmp -e NODE_ENV=production') {
sh 'npm install'
sh 'npm test'
}
// Build Docker image
def image = docker.build("myapp:${env.BUILD_NUMBER}")
// Build with custom Dockerfile
def image2 = docker.build("myapp:latest", "-f Dockerfile.prod .")
// Push to registry
docker.withRegistry('https://registry.example.com', 'registry-credentials') {
image.push()
image.push('latest')
}
// Run container
def container = docker.image('nginx:latest').run('-p 8080:80')
try {
sh 'curl http://localhost:8080'
} finally {
container.stop()
}
}Docker Compose
sh 'docker-compose up -d'
try {
sh 'run-integration-tests.sh'
} finally {
sh 'docker-compose down'
}---
Kubernetes Plugin
Run Jenkins agents as Kubernetes pods.
Pod Template
Declarative:
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins: agent
spec:
containers:
- name: maven
image: maven:3.8.1-adoptopenjdk-11
command:
- cat
tty: true
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
- name: docker
image: docker:latest
command:
- cat
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
'''
}
}
stages {
stage('Build') {
steps {
container('maven') {
sh 'mvn clean package'
}
}
}
stage('Docker Build') {
steps {
container('docker') {
sh 'docker build -t myapp:latest .'
}
}
}
}
}Scripted with Pod Template
podTemplate(
label: 'my-pod',
containers: [
containerTemplate(name: 'maven', image: 'maven:3.8.1', ttyEnabled: true, command: 'cat'),
containerTemplate(name: 'kubectl', image: 'bitnami/kubectl:latest', ttyEnabled: true, command: 'cat')
],
volumes: [
secretVolume(secretName: 'kubeconfig', mountPath: '/home/jenkins/.kube')
]
) {
node('my-pod') {
stage('Build') {
container('maven') {
sh 'mvn clean package'
}
}
stage('Deploy') {
container('kubectl') {
sh 'kubectl apply -f deployment.yaml'
}
}
}
}---
Credentials Plugin
Securely store and use credentials in pipelines.
Credential Types
Username and Password
withCredentials([usernamePassword(
credentialsId: 'my-credentials',
usernameVariable: 'USERNAME',
passwordVariable: 'PASSWORD'
)]) {
sh 'echo "User: $USERNAME"'
// Use $PASSWORD
}Secret Text
withCredentials([string(
credentialsId: 'api-token',
variable: 'API_TOKEN'
)]) {
sh 'curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com'
}SSH User Private Key
withCredentials([sshUserPrivateKey(
credentialsId: 'ssh-key',
keyFileVariable: 'SSH_KEY',
usernameVariable: 'SSH_USER'
)]) {
sh 'ssh -i $SSH_KEY $SSH_USER@server.example.com "deploy.sh"'
}File
withCredentials([file(
credentialsId: 'kubeconfig',
variable: 'KUBECONFIG'
)]) {
sh 'kubectl --kubeconfig=$KUBECONFIG get pods'
}Certificate
withCredentials([certificate(
credentialsId: 'cert-id',
keystoreVariable: 'KEYSTORE',
passwordVariable: 'KEYSTORE_PASSWORD'
)]) {
sh 'sign-app.sh $KEYSTORE $KEYSTORE_PASSWORD'
}Environment Credentials Binding
Declarative:
environment {
DOCKER_CREDENTIALS = credentials('docker-hub-credentials')
// Creates DOCKER_CREDENTIALS_USR and DOCKER_CREDENTIALS_PSW
API_KEY = credentials('api-key') // Secret text
}---
Pipeline Utility Steps
Common utility steps for pipelines.
Read and Write Files
// Read file
def content = readFile(file: 'version.txt')
// Write file
writeFile(file: 'output.txt', text: 'Hello World')
// Read JSON
def json = readJSON(file: 'config.json')
// Or from text
def data = readJSON(text: '{"key": "value"}')
// Write JSON
writeJSON(file: 'output.json', json: [name: 'Jenkins', version: '2.0'])
// Read YAML
def yaml = readYAML(file: 'config.yaml')
// Write YAML
writeYAML(file: 'output.yaml', data: [name: 'Jenkins', version: '2.0'])
// Read CSV
def csv = readCSV(file: 'data.csv')
// Read properties
def props = readProperties(file: 'config.properties')File Operations
// Check if file exists
if (fileExists('path/to/file')) {
echo 'File exists'
}
// Find files
def files = findFiles(glob: '**/*.jar')
files.each { file ->
echo "Found: ${file.path}"
}
// Touch file
touch(file: 'marker.txt')
// ZIP files
zip(zipFile: 'archive.zip', dir: 'target')
// Unzip
unzip(zipFile: 'archive.zip', dir: 'output')---
JUnit Plugin
Publish JUnit test results.
Basic Usage
post {
always {
junit '**/target/test-results/*.xml'
}
}
// With options
junit(
testResults: '**/target/surefire-reports/*.xml',
allowEmptyResults: true,
keepLongStdio: true,
healthScaleFactor: 1.0
)---
HTML Publisher Plugin
Publish HTML reports.
publishHTML([
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report',
keepAll: true,
alwaysLinkToLastBuild: true,
allowMissing: false
])
// Multiple reports
publishHTML([
reportDir: 'test-results',
reportFiles: 'index.html',
reportName: 'Test Results'
])
publishHTML([
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Code Coverage'
])---
Slack Notification Plugin
Send notifications to Slack.
// Simple notification
slackSend(
color: 'good',
message: 'Build succeeded!'
)
// With details
slackSend(
color: currentBuild.result == 'SUCCESS' ? 'good' : 'danger',
message: """
Build: ${env.JOB_NAME} #${env.BUILD_NUMBER}
Status: ${currentBuild.result}
Duration: ${currentBuild.durationString}
URL: ${env.BUILD_URL}
""",
channel: '#builds',
teamDomain: 'myteam',
tokenCredentialId: 'slack-token'
)
// Conditional notifications
post {
success {
slackSend color: 'good', message: "Build ${env.BUILD_NUMBER} succeeded"
}
failure {
slackSend color: 'danger', message: "Build ${env.BUILD_NUMBER} failed"
}
fixed {
slackSend color: 'good', message: "Build ${env.BUILD_NUMBER} fixed!"
}
}---
Email Extension Plugin
Send detailed email notifications.
emailext(
subject: "Build ${currentBuild.result}: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
<h2>Build ${currentBuild.result}</h2>
<p><strong>Job:</strong> ${env.JOB_NAME}</p>
<p><strong>Build Number:</strong> ${env.BUILD_NUMBER}</p>
<p><strong>Build URL:</strong> <a href="${env.BUILD_URL}">${env.BUILD_URL}</a></p>
<p><strong>Duration:</strong> ${currentBuild.durationString}</p>
""",
to: 'team@example.com',
from: 'jenkins@example.com',
replyTo: 'noreply@example.com',
mimeType: 'text/html',
attachLog: true,
compressLog: true,
attachmentsPattern: '**/target/*.jar'
)
// Conditional emails
post {
failure {
emailext(
subject: "Build Failed: ${env.JOB_NAME}",
body: "Check ${env.BUILD_URL}",
to: 'team@example.com',
recipientProviders: [
developers(), // Send to developers who made changes
culprits(), // Send to developers who broke the build
requestor() // Send to user who triggered the build
]
)
}
}---
Build Timeout Plugin
Set timeouts for builds.
Declarative:
options {
timeout(time: 1, unit: 'HOURS')
}Scripted:
timeout(time: 30, unit: 'MINUTES') {
node {
// steps
}
}
// Activity timeout (no console output)
timeout(time: 10, unit: 'MINUTES', activity: true) {
node {
// steps
}
}---
Timestamper Plugin
Add timestamps to console output.
Declarative:
options {
timestamps()
}Scripted:
timestamps {
node {
echo 'This will have timestamps'
}
}---
AnsiColor Plugin
Add color to console output.
Declarative:
options {
ansiColor('xterm')
}Scripted:
ansiColor('xterm') {
node {
sh 'ls --color=always'
}
}---
Workspace Cleanup Plugin
Clean workspace before/after builds.
// Clean before build
cleanWs()
// Clean after build
post {
always {
cleanWs()
}
}
// Clean with options
cleanWs(
deleteDirs: true,
disableDeferredWipeout: true,
notFailBuild: true,
patterns: [
[pattern: 'target', type: 'INCLUDE'],
[pattern: '*.log', type: 'INCLUDE']
]
)
// Delete directory
deleteDir()---
Additional Common Plugins
Archive Artifacts
archiveArtifacts(
artifacts: '**/*.jar',
allowEmptyArchive: false,
fingerprint: true,
onlyIfSuccessful: true
)Stash/Unstash
// Stash files
stash(
name: 'build-artifacts',
includes: 'target/*.jar',
excludes: 'target/*-sources.jar'
)
// Unstash files
unstash 'build-artifacts'Build Job
// Trigger another job
build(
job: 'downstream-job',
parameters: [
string(name: 'ENVIRONMENT', value: 'production'),
booleanParam(name: 'RUN_TESTS', value: true)
],
wait: true,
propagate: true
)Input
def userInput = input(
message: 'Deploy to production?',
ok: 'Deploy',
parameters: [
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Target environment'),
string(name: 'VERSION', defaultValue: '1.0', description: 'Version to deploy')
],
submitter: 'admin,ops',
submitterParameter: 'approver'
)
echo "Deploying ${userInput.VERSION} to ${userInput.ENVIRONMENT}"
echo "Approved by: ${userInput.approver}"Retry
retry(3) {
sh 'flaky-command'
}Sleep
sleep(time: 30, unit: 'SECONDS')Wait Until
waitUntil {
def status = sh(script: 'check-status.sh', returnStatus: true)
return status == 0
}---
Plugin Documentation Lookup
For unlisted plugins, use:
1. Context7: Search for /jenkinsci/<plugin-name>-plugin 2. Web Search: "Jenkins <plugin-name> plugin documentation" 3. Official Plugins: https://plugins.jenkins.io/
---
References
Declarative Pipeline Syntax Reference
Complete reference for Jenkins Declarative Pipeline syntax based on official documentation.
Basic Structure
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Building...'
}
}
}
}Required Sections
1. pipeline
The outermost block that contains all pipeline code.
pipeline {
// All declarative pipeline code goes here
}2. agent
Specifies where the pipeline or stage will execute. Required at top level or per stage.
// Execute on any available agent
agent any
// Execute on agent with specific label
agent {
label 'linux'
}
// Execute in Docker container
agent {
docker {
image 'maven:3.8.1-adoptopenjdk-11'
args '-v /tmp:/tmp'
}
}
// Execute in Kubernetes pod
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.8.1-adoptopenjdk-11
'''
}
}
// No agent (stages must define their own)
agent none3. stages
Contains a sequence of one or more stage directives. Required.
stages {
stage('Build') {
steps {
// build steps
}
}
stage('Test') {
steps {
// test steps
}
}
}4. steps
Defines actions to execute within a stage. Required in each stage (unless stage has stages).
steps {
echo 'Hello World'
sh 'make'
bat 'build.bat'
script {
// Groovy script
def myVar = 'value'
}
}Optional Top-Level Directives
environment
Defines environment variables available to all steps.
environment {
CC = 'clang'
DISABLE_AUTH = 'true'
DB_ENGINE = 'sqlite'
// From credentials
AWS_ACCESS_KEY_ID = credentials('aws-secret-key-id')
// From credentials with username/password
DOCKER_CREDS = credentials('docker-hub-credentials')
// Creates: DOCKER_CREDS_USR and DOCKER_CREDS_PSW
}options
Configures pipeline-specific settings.
options {
// Keep only last 10 builds
buildDiscarder(logRotator(numToKeepStr: '10'))
// Disable concurrent builds
disableConcurrentBuilds()
// Prevent builds from running forever
timeout(time: 1, unit: 'HOURS')
// Add timestamps to console output
timestamps()
// Retry failed pipeline up to 3 times
retry(3)
// Skip default checkout
skipDefaultCheckout()
// Prepend all console output with time
ansiColor('xterm')
}parameters
Defines build parameters users can provide.
parameters {
string(
name: 'DEPLOY_ENV',
defaultValue: 'staging',
description: 'Environment to deploy to'
)
choice(
name: 'VERSION',
choices: ['1.0', '1.1', '2.0'],
description: 'Version to deploy'
)
booleanParam(
name: 'RUN_TESTS',
defaultValue: true,
description: 'Run tests before deploy'
)
text(
name: 'RELEASE_NOTES',
defaultValue: '',
description: 'Release notes'
)
password(
name: 'SECRET',
defaultValue: '',
description: 'Secret value'
)
}
// Access in pipeline:
// ${params.DEPLOY_ENV}triggers
Defines automatic build triggers.
triggers {
// Poll SCM every 15 minutes
pollSCM('H/15 * * * *')
// Cron schedule
cron('H 4 * * 1-5') // Weekdays at 4 AM
// Trigger from upstream job
upstream(
upstreamProjects: 'job1,job2',
threshold: hudson.model.Result.SUCCESS
)
}tools
Auto-installs and configures tools.
tools {
maven 'Maven 3.8.1'
jdk 'JDK 11'
gradle 'Gradle 7.0'
}libraries
Loads shared libraries.
@Library('my-shared-library@master') _
// Or
libraries {
lib('my-shared-library@master')
}Stage-Level Directives
agent (stage-level)
Override agent for specific stage.
stage('Build') {
agent {
docker 'maven:3.8.1-adoptopenjdk-11'
}
steps {
sh 'mvn clean package'
}
}environment (stage-level)
Stage-specific environment variables.
stage('Deploy') {
environment {
DEPLOY_ENV = 'production'
}
steps {
sh 'deploy.sh $DEPLOY_ENV'
}
}when
Conditional execution of stage.
stage('Deploy to Production') {
when {
branch 'main'
environment name: 'DEPLOY_ENV', value: 'production'
expression { return params.RUN_DEPLOY }
}
steps {
echo 'Deploying...'
}
}
// When conditions:
when {
branch 'main' // Branch name
branch pattern: "release-\\d+", comparator: "REGEXP"
environment name: 'DEPLOY', value: 'true' // Environment variable
expression { return currentBuild.result == null } // Groovy expression
tag "release-*" // Git tag
tag pattern: "release-\\d+", comparator: "REGEXP"
not { branch 'main' } // Negation
allOf { // AND
branch 'main'
environment name: 'DEPLOY', value: 'true'
}
anyOf { // OR
branch 'main'
branch 'develop'
}
triggeredBy 'UserIdCause' // Trigger type
triggeredBy cause: 'UserIdCause', detail: 'admin'
buildingTag() // Building a tag
changelog '.*\\[DEPLOY\\].*' // Changelog pattern
changeset "**/*.js" // Changed files
equals expected: 2, actual: currentBuild.number // Comparison
}input
Pause for user input.
stage('Deploy') {
input {
message "Deploy to production?"
ok "Deploy"
submitter "admin,ops"
parameters {
string(name: 'VERSION', description: 'Version to deploy')
}
}
steps {
echo "Deploying ${VERSION}"
}
}options (stage-level)
Stage-specific options.
stage('Test') {
options {
timeout(time: 30, unit: 'MINUTES')
retry(2)
timestamps()
}
steps {
sh 'run-tests.sh'
}
}post
Runs after pipeline/stage completion.
post {
always {
// Always run, regardless of status
echo 'Pipeline completed'
cleanWs()
}
success {
// Run only if successful
slackSend color: 'good', message: 'Build succeeded!'
}
failure {
// Run only if failed
mail to: 'team@example.com',
subject: "Build Failed: ${currentBuild.fullDisplayName}",
body: "Something is wrong"
}
unstable {
// Run if unstable (tests failed but build succeeded)
echo 'Build is unstable'
}
changed {
// Run if status changed from previous build
echo 'Build status changed'
}
fixed {
// Run if previous build failed but current succeeded
echo 'Build is fixed'
}
regression {
// Run if previous build succeeded but current failed
echo 'Build regressed'
}
aborted {
// Run if aborted
echo 'Build was aborted'
}
cleanup {
// Always run, after all other post conditions
echo 'Cleaning up...'
deleteDir()
}
}Parallel Stages
Execute stages in parallel.
stage('Parallel Tests') {
parallel {
stage('Test on Linux') {
agent { label 'linux' }
steps {
sh 'make test'
}
}
stage('Test on Windows') {
agent { label 'windows' }
steps {
bat 'make test'
}
}
stage('Test on Mac') {
agent { label 'mac' }
steps {
sh 'make test'
}
}
}
}
// With failFast
stage('Parallel Deploy') {
failFast true // Stop all parallel stages if one fails
parallel {
stage('Deploy to Region 1') {
steps { sh 'deploy-region1.sh' }
}
stage('Deploy to Region 2') {
steps { sh 'deploy-region2.sh' }
}
}
}Sequential Stages
Nested stages that run sequentially.
stage('Build and Test') {
stages {
stage('Build') {
steps {
sh 'make build'
}
}
stage('Test') {
steps {
sh 'make test'
}
}
}
}Matrix
Run stages across combinations of axes.
stage('Test') {
matrix {
axes {
axis {
name 'PLATFORM'
values 'linux', 'mac', 'windows'
}
axis {
name 'BROWSER'
values 'chrome', 'firefox', 'safari'
}
}
excludes {
exclude {
axis {
name 'PLATFORM'
values 'linux'
}
axis {
name 'BROWSER'
values 'safari'
}
}
}
stages {
stage('Test') {
steps {
echo "Testing on ${PLATFORM} with ${BROWSER}"
}
}
}
}
}Common Steps
steps {
// Shell commands
sh 'echo "Hello"'
sh '''
echo "Multi-line"
echo "shell script"
'''
sh(script: 'ls -la', returnStdout: true)
sh(script: 'exit 1', returnStatus: true)
// Windows batch
bat 'echo Hello'
// PowerShell
powershell 'Write-Host "Hello"'
// Echo
echo 'Message'
// Error
error 'Build failed'
// Retry
retry(3) {
sh 'flaky-command'
}
// Timeout
timeout(time: 5, unit: 'MINUTES') {
sh 'long-running-command'
}
// Script (run Groovy code)
script {
def myVar = 'value'
if (myVar == 'value') {
echo 'Condition met'
}
}
// Credentials
withCredentials([string(credentialsId: 'my-secret', variable: 'SECRET')]) {
sh 'echo $SECRET'
}
// Git checkout
checkout scm
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
userRemoteConfigs: [[url: 'https://github.com/user/repo.git']]
])
// Archive artifacts
archiveArtifacts artifacts: '**/*.jar', fingerprint: true
// Publish test results
junit '**/target/test-results/*.xml'
// Stash/unstash
stash name: 'build-artifacts', includes: 'target/*.jar'
unstash 'build-artifacts'
// Delete workspace
deleteDir()
// Clean workspace
cleanWs()
}Built-in Variables
// Build info
currentBuild.number // Build number
currentBuild.result // SUCCESS, FAILURE, UNSTABLE, ABORTED
currentBuild.currentResult // Current result
currentBuild.displayName // Display name
currentBuild.description // Build description
currentBuild.duration // Build duration in ms
// Environment variables
env.BUILD_ID
env.BUILD_NUMBER
env.BUILD_TAG
env.BUILD_URL
env.JOB_NAME
env.JOB_BASE_NAME
env.NODE_NAME
env.WORKSPACE
env.JENKINS_HOME
env.BRANCH_NAME // For multibranch pipelines
env.CHANGE_ID // For pull requests
env.GIT_COMMIT
env.GIT_BRANCH
// Parameters
params.PARAMETER_NAME
// SCM
scm.userRemoteConfigs
scm.branchesComplete Example
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
timeout(time: 1, unit: 'HOURS')
timestamps()
}
parameters {
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Deployment environment')
booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Run tests')
}
environment {
APP_NAME = 'my-app'
VERSION = "${env.BUILD_NUMBER}"
DOCKER_IMAGE = "${APP_NAME}:${VERSION}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
agent {
docker {
image 'maven:3.8.1-adoptopenjdk-11'
}
}
steps {
sh 'mvn clean package'
stash name: 'build-artifacts', includes: 'target/*.jar'
}
}
stage('Test') {
when {
expression { return params.RUN_TESTS }
}
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify'
}
}
}
post {
always {
junit '**/target/test-results/*.xml'
}
}
}
stage('Docker Build') {
steps {
unstash 'build-artifacts'
sh "docker build -t ${DOCKER_IMAGE} ."
}
}
stage('Deploy') {
when {
branch 'main'
}
input {
message "Deploy to ${params.ENVIRONMENT}?"
ok "Deploy"
submitter "ops,admin"
}
steps {
withCredentials([usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
sh '''
docker login -u $USER -p $PASS
docker push ${DOCKER_IMAGE}
'''
}
sh "kubectl set image deployment/${APP_NAME} ${APP_NAME}=${DOCKER_IMAGE}"
}
}
}
post {
success {
slackSend color: 'good', message: "Build ${env.BUILD_NUMBER} succeeded"
}
failure {
mail to: 'team@example.com',
subject: "Build ${env.BUILD_NUMBER} failed",
body: "Check ${env.BUILD_URL}"
}
cleanup {
cleanWs()
}
}
}References
Scripted Pipeline Syntax Reference
Complete reference for Jenkins Scripted Pipeline syntax using Groovy.
Overview
Scripted Pipeline is written using Groovy, providing maximum flexibility and power. Unlike Declarative Pipeline, Scripted Pipeline uses imperative programming and has few structural restrictions.
Basic Structure
node {
stage('Build') {
echo 'Building...'
}
stage('Test') {
echo 'Testing...'
}
stage('Deploy') {
echo 'Deploying...'
}
}node Block
The node block allocates an executor (agent) for the pipeline.
// Run on any available agent
node {
// steps here
}
// Run on specific labeled agent
node('linux') {
// steps here
}
// Run on Docker agent
node('docker') {
// steps here
}
// Run on specific node
node('master') {
// steps here
}
// Run on agent matching expression
node('linux && java11') {
// steps here
}stage Block
Stages organize pipeline into logical sections (mainly for visualization).
node {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'make build'
}
stage('Test') {
sh 'make test'
}
}Variables and Data Types
Variable Declaration
// Using def (recommended for local scope)
def myString = 'Hello'
def myNumber = 42
def myBoolean = true
def myList = [1, 2, 3]
def myMap = [key1: 'value1', key2: 'value2']
// Without def (global scope - use cautiously)
globalVar = 'accessible everywhere'
// Typed variables
String name = 'Jenkins'
Integer count = 10
Boolean flag = false
List<String> items = ['a', 'b', 'c']
Map<String, String> config = [env: 'prod', version: '1.0']String Interpolation
def name = 'World'
// Double quotes for interpolation
def greeting = "Hello, ${name}!"
// Single quotes for literal strings
def literal = 'Hello, ${name}!' // Won't interpolate
// Multi-line strings
def multiLine = """
This is a
multi-line string
with ${name}
"""
// Multi-line without interpolation
def multiLineLiteral = '''
This is a
literal multi-line string
with ${name}
'''Control Structures
if-else
node {
def environment = 'production'
if (environment == 'production') {
echo 'Deploying to production'
} else if (environment == 'staging') {
echo 'Deploying to staging'
} else {
echo 'Deploying to development'
}
// Ternary operator
def message = (environment == 'production') ? 'PROD' : 'NON-PROD'
}for Loops
node {
// Iterate over list
def items = ['build', 'test', 'deploy']
for (item in items) {
echo "Step: ${item}"
}
// Iterate with index
for (int i = 0; i < items.size(); i++) {
echo "${i}: ${items[i]}"
}
// Range iteration
for (i in 0..5) {
echo "Number: ${i}"
}
// Each method
items.each { item ->
echo "Processing ${item}"
}
// Each with index
items.eachWithIndex { item, index ->
echo "${index}: ${item}"
}
}while Loops
node {
def counter = 0
while (counter < 5) {
echo "Counter: ${counter}"
counter++
}
}switch Statement
node {
def environment = 'staging'
switch(environment) {
case 'development':
echo 'Dev environment'
break
case 'staging':
echo 'Staging environment'
break
case 'production':
echo 'Production environment'
break
default:
error 'Unknown environment'
}
}Error Handling
try-catch-finally
node {
try {
sh 'make build'
sh 'make test'
} catch (Exception e) {
echo "Build failed: ${e.message}"
currentBuild.result = 'FAILURE'
throw e // Re-throw if needed
} finally {
echo 'Cleaning up...'
sh 'make clean'
}
}try-catch with Different Exception Types
node {
try {
sh 'risky-command'
} catch (hudson.AbortException e) {
echo "Process was aborted: ${e.message}"
} catch (Exception e) {
echo "General error: ${e.message}"
currentBuild.result = 'FAILURE'
}
}Catching Specific Errors
node {
try {
def result = sh(script: 'test-command', returnStatus: true)
if (result != 0) {
error "Command failed with exit code ${result}"
}
} catch (Exception e) {
echo "Handling error: ${e}"
// Continue or fail
}
}Methods and Functions
Defining Methods
// Method definition
def buildApplication() {
echo 'Building application...'
sh 'mvn clean package'
}
// Method with parameters
def deploy(String environment, String version) {
echo "Deploying version ${version} to ${environment}"
sh "kubectl set image deployment/app app=${version}"
}
// Method with return value
def getVersion() {
return sh(script: 'git describe --tags', returnStdout: true).trim()
}
// Usage
node {
buildApplication()
def version = getVersion()
deploy('production', version)
}@NonCPS Methods
Methods that should not use Continuation Passing Style (for complex Groovy operations).
@NonCPS
def parseJson(String json) {
def jsonSlurper = new groovy.json.JsonSlurper()
return jsonSlurper.parseText(json)
}
@NonCPS
def processData(data) {
// Complex Groovy logic that doesn't involve pipeline steps
return data.collect { it.toUpperCase() }
}
node {
def json = '{"name": "Jenkins", "version": "2.0"}'
def parsed = parseJson(json)
echo "Name: ${parsed.name}"
// WARNING: Cannot use pipeline steps (sh, echo, etc.) in @NonCPS methods
}Parallel Execution
Basic Parallel
node {
stage('Parallel Tests') {
parallel(
'Unit Tests': {
node('linux') {
sh 'make unit-test'
}
},
'Integration Tests': {
node('linux') {
sh 'make integration-test'
}
},
'Smoke Tests': {
node('linux') {
sh 'make smoke-test'
}
}
)
}
}Parallel with failFast
node {
stage('Deploy to Regions') {
parallel(
failFast: true, // Stop all if one fails
'Region US-EAST': {
sh 'deploy-us-east.sh'
},
'Region US-WEST': {
sh 'deploy-us-west.sh'
},
'Region EU': {
sh 'deploy-eu.sh'
}
)
}
}Dynamic Parallel Execution
node {
def branches = [:]
def environments = ['dev', 'qa', 'staging']
for (int i = 0; i < environments.size(); i++) {
def env = environments[i] // Important: capture variable
branches["Deploy to ${env}"] = {
node {
echo "Deploying to ${env}"
sh "deploy.sh ${env}"
}
}
}
parallel branches
}Working with Credentials
Username and Password
node {
withCredentials([usernamePassword(
credentialsId: 'my-credentials',
usernameVariable: 'USERNAME',
passwordVariable: 'PASSWORD'
)]) {
sh '''
echo "Username: $USERNAME"
# Use $PASSWORD in commands
'''
}
}Secret Text
node {
withCredentials([string(
credentialsId: 'api-token',
variable: 'API_TOKEN'
)]) {
sh 'curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com'
}
}SSH Key
node {
withCredentials([sshUserPrivateKey(
credentialsId: 'ssh-key',
keyFileVariable: 'SSH_KEY',
usernameVariable: 'SSH_USER'
)]) {
sh '''
ssh -i $SSH_KEY $SSH_USER@server.example.com 'deploy.sh'
'''
}
}Multiple Credentials
node {
withCredentials([
usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS'),
string(credentialsId: 'api-key', variable: 'API_KEY')
]) {
sh 'docker login -u $DOCKER_USER -p $DOCKER_PASS'
sh 'curl -H "X-API-Key: $API_KEY" https://api.example.com'
}
}Environment Variables
Setting Environment Variables
node {
// Using withEnv
withEnv(['ENV=production', 'VERSION=1.0']) {
sh 'echo "Environment: $ENV, Version: $VERSION"'
}
// Direct assignment
env.MY_VAR = 'value'
sh 'echo $MY_VAR'
// From shell command
env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
}Accessing Environment Variables
node {
echo "Build number: ${env.BUILD_NUMBER}"
echo "Job name: ${env.JOB_NAME}"
echo "Workspace: ${env.WORKSPACE}"
def branch = env.BRANCH_NAME ?: 'main'
echo "Branch: ${branch}"
}Common Wrappers
Timestamps
timestamps {
node {
echo 'This output will have timestamps'
sh 'sleep 5'
echo 'Done'
}
}Timeout
timeout(time: 30, unit: 'MINUTES') {
node {
sh 'long-running-command'
}
}
// With activity timeout
timeout(time: 5, unit: 'MINUTES', activity: true) {
node {
// Timeout if no console output for 5 minutes
sh 'command-with-output'
}
}Retry
retry(3) {
node {
sh 'flaky-test-command'
}
}
// With custom condition
retry(3) {
try {
sh 'test-command'
} catch (Exception e) {
if (e.message.contains('timeout')) {
throw e // Retry
} else {
return // Don't retry
}
}
}Lock
lock(resource: 'deployment-lock', inversePrecedence: true) {
node {
echo 'Only one build can deploy at a time'
sh 'deploy.sh'
}
}AnsiColor
ansiColor('xterm') {
node {
sh 'ls --color=always'
}
}Working with Docker
Using Docker Images
node {
docker.image('maven:3.8.1-adoptopenjdk-11').inside {
sh 'mvn --version'
sh 'mvn clean package'
}
// With additional arguments
docker.image('maven:3.8.1').inside('-v /tmp:/tmp -e MAVEN_OPTS="-Xmx1024m"') {
sh 'mvn clean install'
}
}Building Docker Images
node {
def image = docker.build("my-app:${env.BUILD_NUMBER}")
// With custom Dockerfile
def image2 = docker.build("my-app:latest", "-f Dockerfile.prod .")
// Push to registry
docker.withRegistry('https://registry.example.com', 'registry-credentials') {
image.push()
image.push('latest')
}
}Running Docker Containers
node {
def container = docker.image('nginx:latest').run('-p 8080:80')
try {
// Run tests against container
sh 'curl http://localhost:8080'
} finally {
container.stop()
}
}Working with Git
Basic Checkout
node {
checkout scm
// Or explicit checkout
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
userRemoteConfigs: [[
url: 'https://github.com/user/repo.git',
credentialsId: 'github-credentials'
]]
])
}Git Operations
node {
// Get commit info
def commit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
def shortCommit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
def branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
echo "Commit: ${commit}"
echo "Short: ${shortCommit}"
echo "Branch: ${branch}"
// Tag
sh "git tag -a v${env.BUILD_NUMBER} -m 'Build ${env.BUILD_NUMBER}'"
sh 'git push origin --tags'
}Stash and Unstash
node('build-agent') {
stage('Build') {
sh 'make build'
stash name: 'build-artifacts', includes: 'target/*.jar'
}
}
node('deploy-agent') {
stage('Deploy') {
unstash 'build-artifacts'
sh 'deploy.sh target/*.jar'
}
}Input and Approval
node {
stage('Build') {
sh 'make build'
}
stage('Approval') {
def userInput = input(
message: 'Deploy to production?',
parameters: [
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Target environment'),
string(name: 'VERSION', defaultValue: '1.0', description: 'Version to deploy')
],
submitter: 'admin,ops'
)
echo "Deploying ${userInput.VERSION} to ${userInput.ENVIRONMENT}"
}
stage('Deploy') {
sh "deploy.sh ${userInput.ENVIRONMENT} ${userInput.VERSION}"
}
}Build Parameters
properties([
parameters([
string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: 'Deployment environment'),
choice(name: 'VERSION', choices: ['1.0', '1.1', '2.0'], description: 'Version'),
booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Run tests')
])
])
node {
echo "Environment: ${params.DEPLOY_ENV}"
echo "Version: ${params.VERSION}"
if (params.RUN_TESTS) {
sh 'make test'
}
}Accessing Build Information
node {
// Current build
echo "Build number: ${currentBuild.number}"
echo "Build result: ${currentBuild.result}" // SUCCESS, FAILURE, UNSTABLE, ABORTED
echo "Display name: ${currentBuild.displayName}"
echo "Duration: ${currentBuild.duration}"
// Set build properties
currentBuild.displayName = "#${env.BUILD_NUMBER} - ${env.BRANCH_NAME}"
currentBuild.description = "Deployed version ${version}"
currentBuild.result = 'SUCCESS'
// Previous build
if (currentBuild.previousBuild) {
echo "Previous result: ${currentBuild.previousBuild.result}"
}
}Complete Example
@Library('shared-library@master') _
// Build properties
properties([
buildDiscarder(logRotator(numToKeepStr: '10')),
disableConcurrentBuilds(),
parameters([
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Target environment'),
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: 'Skip tests')
])
])
// Variables
def version
def dockerImage
// Helper methods
def buildApp() {
sh 'mvn clean package'
}
@NonCPS
def parseVersion(String pomXml) {
def matcher = (pomXml =~ /<version>(.+)<\/version>/)
return matcher[0][1]
}
// Main pipeline
timestamps {
ansiColor('xterm') {
node('linux') {
try {
stage('Checkout') {
checkout scm
version = sh(script: 'git describe --tags --always', returnStdout: true).trim()
currentBuild.displayName = "#${env.BUILD_NUMBER} - ${version}"
}
stage('Build') {
docker.image('maven:3.8.1-adoptopenjdk-11').inside {
buildApp()
stash name: 'app-jar', includes: 'target/*.jar'
}
}
if (!params.SKIP_TESTS) {
stage('Test') {
parallel(
'Unit Tests': {
sh 'mvn test'
},
'Integration Tests': {
sh 'mvn verify'
}
)
junit '**/target/test-results/*.xml'
}
}
stage('Docker Build') {
unstash 'app-jar'
dockerImage = docker.build("myapp:${version}")
}
if (params.ENVIRONMENT == 'production') {
stage('Approval') {
timeout(time: 1, unit: 'HOURS') {
input message: 'Deploy to production?', submitter: 'ops,admin'
}
}
}
stage('Deploy') {
withCredentials([
usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'USER', passwordVariable: 'PASS'),
string(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')
]) {
sh 'docker login -u $USER -p $PASS registry.example.com'
dockerImage.push()
sh """
kubectl set image deployment/myapp myapp=myapp:${version}
kubectl rollout status deployment/myapp
"""
}
}
currentBuild.result = 'SUCCESS'
} catch (Exception e) {
currentBuild.result = 'FAILURE'
echo "Pipeline failed: ${e.message}"
throw e
} finally {
stage('Cleanup') {
cleanWs()
// Send notification
def color = currentBuild.result == 'SUCCESS' ? 'good' : 'danger'
slackSend color: color, message: "Build ${currentBuild.displayName}: ${currentBuild.result}"
}
}
}
}
}References
#!/bin/bash
# Scripted Pipeline Validator
# Validates Jenkins Scripted Pipeline syntax and structure
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Counters
ERRORS=0
WARNINGS=0
INFO=0
# Validation result arrays
declare -a ERROR_MESSAGES=()
declare -a WARNING_MESSAGES=()
declare -a INFO_MESSAGES=()
usage() {
echo "Usage: $0 <jenkinsfile>"
echo "Validates a Scripted Jenkins Pipeline"
exit 1
}
log_error() {
local line=$1
local message=$2
ERROR_MESSAGES+=("ERROR [Line $line]: $message")
((ERRORS++))
}
log_warning() {
local line=$1
local message=$2
WARNING_MESSAGES+=("WARNING [Line $line]: $message")
((WARNINGS++))
}
log_info() {
local line=$1
local message=$2
INFO_MESSAGES+=("INFO [Line $line]: $message")
((INFO++))
}
# Basic Groovy syntax validation
validate_groovy_syntax() {
local file=$1
local line_num=0
while IFS= read -r line; do
((line_num++))
# Skip comments and empty lines
if echo "$line" | grep -qE '^\s*(//|$)'; then
continue
fi
# Check for unmatched braces (simple check)
local open_braces=$(echo "$line" | grep -o '{' | wc -l)
local close_braces=$(echo "$line" | grep -o '}' | wc -l)
# Check for unmatched quotes
# Skip lines that are part of multi-line strings (triple quotes or heredocs)
if echo "$line" | grep -qE "'''|\"\"\""; then
continue
fi
# Skip lines inside multi-line sh blocks (contain just string content)
if echo "$line" | grep -qE '^\s*(echo|mkdir|make|cd|cmake|mvn|gradle|npm|yarn|kubectl|docker|git)\s'; then
continue
fi
# Remove escaped quotes before counting
local clean_line=$(echo "$line" | sed "s/\\\\'//g" | sed 's/\\"//g')
local single_quotes=$(echo "$clean_line" | grep -o "'" | wc -l)
local double_quotes=$(echo "$clean_line" | grep -o '"' | wc -l)
# Only flag truly unbalanced quotes (not in multi-line contexts)
if (( single_quotes % 2 != 0 )); then
# Only error if not a shell command continuation
if ! echo "$line" | grep -qE "sh\s+'''|sh\s*\(|script:|'''"; then
log_error "$line_num" "Unmatched single quote detected"
fi
fi
if (( double_quotes % 2 != 0 )); then
# Only error if not a shell command continuation
if ! echo "$line" | grep -qE 'sh\s+"""|sh\s*\(|script:|"""'; then
log_error "$line_num" "Unmatched double quote detected"
fi
fi
# Note: Per-line parenthesis check removed to avoid false positives
# Multi-line constructs like properties([...]) are valid Groovy
done < "$file"
# Check overall brace balance
local total_open=$(grep -o '{' "$file" | wc -l)
local total_close=$(grep -o '}' "$file" | wc -l)
if (( total_open != total_close )); then
log_error "EOF" "Unbalanced braces: $total_open opening, $total_close closing"
fi
}
# Validate node block usage
validate_node_blocks() {
local file=$1
local line_num=0
local has_node=false
while IFS= read -r line; do
((line_num++))
# Check for node blocks
if echo "$line" | grep -qE '^\s*node\s*(\(|{)'; then
has_node=true
# Check if node has label or is empty
if echo "$line" | grep -qE '^\s*node\s*\(\s*\)'; then
log_warning "$line_num" "Empty node() - consider using node('label') for specific agents"
log_warning "$line_num" " → Use: node('docker') or node('linux') for specific agents"
fi
fi
# Check for stage definitions outside node blocks (potential issue)
if echo "$line" | grep -qE '^\s*stage\s*\(' && [[ "$has_node" == false ]]; then
log_info "$line_num" "Stage defined outside node block - ensure this is intentional"
fi
done < "$file"
if [[ "$has_node" == false ]]; then
log_warning 1 "No node blocks found - scripted pipelines typically use node blocks for agent allocation"
log_warning 1 " → Wrap your pipeline code in node { ... } block"
fi
}
# Check for error handling
validate_error_handling() {
local file=$1
local line_num=0
local has_try=false
local has_catch=false
local has_finally=false
while IFS= read -r line; do
((line_num++))
# Check for try-catch-finally blocks
if echo "$line" | grep -qE '^\s*try\s*{'; then
has_try=true
local try_line=$line_num
# Look for corresponding catch/finally in next 100 lines
local check_line=$line_num
local found_catch=false
local found_finally=false
for ((i=0; i<100; i++)); do
((check_line++))
local next_line=$(sed -n "${check_line}p" "$file")
# Match catch with optional leading } for "} catch (" pattern
if echo "$next_line" | grep -qE 'catch\s*\('; then
found_catch=true
fi
# Match finally with optional leading } for "} finally {" pattern
if echo "$next_line" | grep -qE 'finally\s*\{'; then
found_finally=true
break
fi
done
if [[ "$found_catch" == false ]]; then
log_warning "$try_line" "try block without catch - consider adding error handling"
log_warning "$try_line" " → Add: catch (Exception e) { ... }"
fi
fi
# Check for sh/bat commands without try-catch
if echo "$line" | grep -qE '\s+(sh|bat)\s*["\047(]' && ! echo "$line" | grep -q 'returnStatus'; then
# This is a heuristic - check if we're in a try block by looking backward
# Also need to check we're not in catch/finally (which would be inside try)
local in_try=false
local in_catch_finally=false
local check_back=$line_num
local brace_depth=0
for ((i=0; i<100 && check_back>0; i++)); do
((check_back--))
local prev_line=$(sed -n "${check_back}p" "$file")
# Track brace depth to understand scope
local close_braces=$(echo "$prev_line" | grep -o '}' | wc -l)
local open_braces=$(echo "$prev_line" | grep -o '{' | wc -l)
brace_depth=$((brace_depth + close_braces - open_braces))
# Check if we find a try block at the right depth
if echo "$prev_line" | grep -qE '^\s*try\s*\{'; then
in_try=true
break
fi
# If we're in catch or finally block, we're effectively in error handling
if echo "$prev_line" | grep -qE 'catch\s*\(|finally\s*\{'; then
in_catch_finally=true
break
fi
# Stop if we exit the current block structure too far
if [ $brace_depth -gt 3 ]; then
break
fi
done
if [[ "$in_try" == false ]] && [[ "$in_catch_finally" == false ]]; then
log_info "$line_num" "Consider wrapping shell commands in try-catch for better error handling"
fi
fi
done < "$file"
}
# Validate @NonCPS usage
validate_noncps() {
local file=$1
local line_num=0
while IFS= read -r line; do
((line_num++))
if echo "$line" | grep -q '@NonCPS'; then
# Check if next lines contain problematic pipeline steps
local check_line=$line_num
local found_issue=false
for ((i=0; i<30; i++)); do
((check_line++))
local next_line=$(sed -n "${check_line}p" "$file")
# Check for async pipeline steps in @NonCPS method
if echo "$next_line" | grep -qE '\s+(sh|bat|sleep|timeout|node|stage)\s*["\047(]'; then
log_error "$check_line" "Cannot use pipeline steps (sh, sleep, etc.) in @NonCPS methods"
log_error "$check_line" " → Remove @NonCPS or refactor to not use async pipeline steps"
found_issue=true
break
fi
# Stop at method end
if echo "$next_line" | grep -qE '^\s*}\s*$'; then
break
fi
done
fi
done < "$file"
}
# Check for proper variable declarations
validate_variables() {
local file=$1
local line_num=0
# Track declared variables to avoid false positives on reassignments
# Using a simple string list instead of associative array for bash 3.x compatibility
local declared_vars=""
# First pass: collect all declared variables
while IFS= read -r line; do
((line_num++))
# Track variables declared with def, String, Integer, Boolean, Map, List
if echo "$line" | grep -qE '^\s*(def|String|Integer|Boolean|Map|List)\s+([a-zA-Z_][a-zA-Z0-9_]*)'; then
local var_name=$(echo "$line" | grep -oE '(def|String|Integer|Boolean|Map|List)\s+[a-zA-Z_][a-zA-Z0-9_]*' | awk '{print $2}')
if [[ -n "$var_name" ]]; then
declared_vars="$declared_vars:$var_name:"
fi
fi
done < "$file"
# Second pass: check for issues
line_num=0
while IFS= read -r line; do
((line_num++))
# Skip comments
if echo "$line" | grep -qE '^\s*//'; then
continue
fi
# Check for variable assignments without def
if echo "$line" | grep -qE '^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*=' && ! echo "$line" | grep -qE '^\s*(def|String|Integer|Boolean|Map|List)'; then
# Check if it's not a property assignment (has dot notation)
if ! echo "$line" | grep -q '\.'; then
# Extract variable name
local var_name=$(echo "$line" | grep -oE '^\s*[a-zA-Z_][a-zA-Z0-9_]*' | tr -d ' ')
# Only warn if variable was not previously declared
if [[ "$declared_vars" != *":$var_name:"* ]]; then
log_info "$line_num" "Consider using 'def' for variable declaration for better scoping"
log_info "$line_num" " → Use: def myVar = ... instead of myVar = ..."
fi
fi
fi
# Check for proper string interpolation
if echo "$line" | grep -q '\$[A-Z_][A-Z0-9_]*' && ! echo "$line" | grep -q '"\$'; then
log_warning "$line_num" "Variable interpolation should use double quotes, not single quotes"
log_warning "$line_num" " → Use: \"text \${VAR}\" instead of 'text \$VAR'"
fi
done < "$file"
}
# Check for common anti-patterns
validate_antipatterns() {
local file=$1
local line_num=0
while IFS= read -r line; do
((line_num++))
# Check for JsonSlurper on controller
if echo "$line" | grep -q 'JsonSlurper'; then
log_warning "$line_num" "JsonSlurper runs on controller - consider using jq on agent instead"
log_warning "$line_num" " → Use: sh(script: 'jq ...', returnStdout: true)"
fi
# Check for HttpRequest without proper delegation
if echo "$line" | grep -qE 'HttpRequest|httpRequest' && ! echo "$line" | grep -q 'sh.*curl'; then
log_warning "$line_num" "Consider using curl/wget on agent instead of HTTP libraries on controller"
log_warning "$line_num" " → Use: sh 'curl -s http://...' for better performance"
fi
# Check for readFile without size checks
if echo "$line" | grep -q 'readFile' && ! echo "$line" | grep -q 'encoding'; then
log_info "$line_num" "Consider checking file size before readFile to avoid memory issues"
fi
# Check for writeFile
if echo "$line" | grep -q 'writeFile'; then
log_info "$line_num" "Ensure writeFile is writing to workspace, not arbitrary locations"
fi
done < "$file"
}
# Check for parallel execution opportunities
validate_parallel_usage() {
local file=$1
local line_num=0
local stage_count=0
while IFS= read -r line; do
((line_num++))
if echo "$line" | grep -qE '^\s*stage\s*\('; then
((stage_count++))
fi
done < "$file"
if [ $stage_count -gt 3 ] && ! grep -q 'parallel' "$file"; then
log_info 1 "Consider using parallel execution for independent stages to improve build time"
log_info 1 " → See: references/scripted_syntax.md#parallel-execution"
fi
}
# Check for timestamps and other useful wrappers
validate_wrappers() {
local file=$1
if ! grep -q 'timestamps' "$file"; then
log_info 1 "Consider adding timestamps() wrapper for better log readability"
log_info 1 " → Wrap pipeline: timestamps { node { ... } }"
fi
if ! grep -q 'ansiColor' "$file" && ! grep -q 'xterm' "$file"; then
log_info 1 "Consider adding ansiColor wrapper for colorized output"
log_info 1 " → Wrap pipeline: ansiColor('xterm') { ... }"
fi
}
# Main validation function
validate_scripted() {
local file=$1
echo -e "${BLUE}=== Validating Scripted Pipeline ===${NC}"
echo "File: $file"
echo ""
# Run all validation checks (|| true prevents early exit on validation failures)
validate_groovy_syntax "$file" || true
validate_node_blocks "$file" || true
validate_error_handling "$file" || true
validate_noncps "$file" || true
validate_variables "$file" || true
validate_antipatterns "$file" || true
validate_parallel_usage "$file" || true
validate_wrappers "$file" || true
# Print results
echo -e "${BLUE}=== Validation Results ===${NC}"
echo ""
if [ ${#ERROR_MESSAGES[@]} -gt 0 ]; then
echo -e "${RED}ERRORS (${ERRORS}):${NC}"
for msg in "${ERROR_MESSAGES[@]}"; do
echo -e "${RED}$msg${NC}"
done
echo ""
fi
if [ ${#WARNING_MESSAGES[@]} -gt 0 ]; then
echo -e "${YELLOW}WARNINGS (${WARNINGS}):${NC}"
for msg in "${WARNING_MESSAGES[@]}"; do
echo -e "${YELLOW}$msg${NC}"
done
echo ""
fi
if [ ${#INFO_MESSAGES[@]} -gt 0 ]; then
echo -e "${BLUE}INFO (${INFO}):${NC}"
for msg in "${INFO_MESSAGES[@]}"; do
echo -e "${BLUE}$msg${NC}"
done
echo ""
fi
# Summary
echo -e "${BLUE}=== Summary ===${NC}"
if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
echo -e "${GREEN}✓ Validation passed with no errors or warnings${NC}"
return 0
elif [ $ERRORS -eq 0 ]; then
echo -e "${YELLOW}✓ Validation passed with $WARNINGS warning(s)${NC}"
return 0
else
echo -e "${RED}✗ Validation failed with $ERRORS error(s) and $WARNINGS warning(s)${NC}"
return 1
fi
}
# Main execution
if [ $# -ne 1 ]; then
usage
fi
JENKINSFILE="$1"
if [ ! -f "$JENKINSFILE" ]; then
echo -e "${RED}Error: File '$JENKINSFILE' not found${NC}"
exit 1
fi
validate_scripted "$JENKINSFILE"
Related skills
How it compares
Choose jenkinsfile-validator over generic Groovy linters when you need Jenkins-specific pipeline type detection, shared-library CPS checks, and credential scanning in one scripted workflow.
FAQ
Which Jenkins pipeline styles does jenkinsfile-validator support?
jenkinsfile-validator supports Declarative and Scripted Jenkinsfiles and auto-detects pipeline type before running validate_declarative.sh or validate_scripted.sh. Ambiguous files still receive syntax, security, and best-practice analysis through the shared validation scripts.
Can jenkinsfile-validator check Jenkins shared library Groovy files?
jenkinsfile-validator includes validate_shared_library.sh for vars/*.groovy and src/**/*.groovy files, reporting CPS compatibility, @NonCPS misuse, package and class issues, and credential handling problems alongside root Jenkinsfile checks.
How do you run only security checks on a Jenkinsfile?
jenkinsfile-validator exposes bash scripts/validate_jenkinsfile.sh --security-only Jenkinsfile to scan for hardcoded secrets and credential misuse without running full best-practice or syntax subsets, useful for fast CI security gates.