← Back to DevBytes

Jenkins Pipeline: Complete Configuration Guide

What is a Jenkins Pipeline?

A Jenkins Pipeline is a suite of plugins and a domain-specific language (DSL) that allows you to model your entire software delivery process as code. Rather than configuring individual build steps through the Jenkins UI with freestyle jobs, a Pipeline defines the complete build, test, and deployment workflow in a text file called a Jenkinsfile, which can be checked into source control alongside your application code.

Pipelines are built on two fundamental concepts:

A Pipeline also provides durable execution: it can survive planned and unplanned restarts of the Jenkins controller, pause for human approval, and resume right where it left off. This makes Pipelines far more resilient than traditional freestyle jobs.

Declarative versus Scripted Pipeline

Jenkins supports two flavors of Pipeline syntax:

Why Jenkins Pipeline Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Adopting Jenkins Pipeline brings several concrete benefits to your development workflow:

How to Use Jenkins Pipeline

1. Creating Your First Jenkinsfile

A Jenkinsfile is the heart of your Pipeline. Place it in the root of your repository. Below is a minimal declarative Pipeline that checks out code, builds, tests, and archives artifacts:

pipeline {
    agent any
    
    tools {
        maven 'maven-3.9'  // Name must match a configured Maven installation in Jenkins
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm  // Checks out from the SCM configured in the job
            }
        }
        stage('Build') {
            steps {
                sh 'mvn clean compile'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/**/*.xml'
                }
            }
        }
        stage('Package') {
            steps {
                sh 'mvn package -DskipTests'
                archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
            }
        }
    }
    
    post {
        success {
            echo 'Pipeline succeeded! Sending notification...'
        }
        failure {
            echo 'Pipeline failed! Notifying team...'
        }
    }
}

This example demonstrates the essential building blocks: an agent declaration, tools for build environment setup, multiple stages with steps, and a post section for conditional actions after completion.

2. Choosing and Configuring Agents

The agent directive tells Jenkins where to execute the Pipeline. You have several options:

// Run on any available agent with the specified label
agent { label 'linux-docker' }

// Run on a specific node by name
agent { node { label 'builder-01' } }

// Run inside a Docker container (ephemeral, clean environment)
agent {
    docker {
        image 'openjdk:17-jdk-slim'
        args '-v /tmp:/tmp'
    }
}

// Run on the controller itself (use sparingly for lightweight tasks only)
agent { node { label 'master' } }

// Run entirely on the controller (discouraged for production pipelines)
agent none  // Top-level; then assign per-stage agents with same syntax

Docker agents are particularly powerful because they guarantee a reproducible, isolated environment on every run, eliminating "works on my machine" issues.

3. Defining Environment Variables

Environment variables can be set at the pipeline level, stage level, or dynamically from scripts. They are essential for parameterizing builds and injecting secrets:

pipeline {
    agent any
    
    environment {
        APP_NAME = 'payment-service'
        ARTIFACT_REPO = 'https://nexus.internal.example.com'
        // Credential reference: pulls username/password from Jenkins credential store
        DB_PASSWORD = credentials('prod-db-password-id')
    }
    
    stages {
        stage('Deploy') {
            environment {
                TARGET_ENV = 'staging'
            }
            steps {
                sh '''
                    echo "Deploying ${APP_NAME} to ${TARGET_ENV}"
                    # DB_PASSWORD is automatically masked in logs
                '''
            }
        }
    }
}

Use the credentials() helper to inject secrets from the Jenkins credential store. Their values are automatically redacted in console output, preventing accidental exposure.

4. Working with Parameters

Parameters allow you to prompt users for input when triggering a Pipeline manually. Declare them in a parameters block:

pipeline {
    agent any
    
    parameters {
        string(name: 'BRANCH', defaultValue: 'main', description: 'Branch to build')
        choice(name: 'DEPLOY_ENV', choices: ['dev', 'staging', 'production'], description: 'Target environment')
        booleanParam(name: 'RUN_INTEGRATION_TESTS', defaultValue: true)
        password(name: 'DEPLOY_KEY', description: 'Deployment encryption key')
    }
    
    stages {
        stage('Initialize') {
            steps {
                script {
                    echo "Building branch: ${params.BRANCH}"
                    echo "Target environment: ${params.DEPLOY_ENV}"
                }
            }
        }
    }
}

Parameters are accessed via the params object. They are only presented on the first run or when you click "Build with Parameters" in the UI; subsequent automated triggers (SCM polling, webhooks) use default values.

5. Handling Failures and Post-Build Actions

The post section lets you run steps conditionally based on the completion status of the Pipeline or a specific stage. This is critical for notifications, cleanup, and artifact retention:

pipeline {
    agent any
    
    stages {
        stage('Run Tests') {
            steps {
                sh './run-tests.sh'
            }
            post {
                always {
                    // Always run, regardless of outcome
                    sh 'docker-compose down'
                    cleanWs()  // Delete workspace to save disk space
                }
                success {
                    archiveArtifacts artifacts: 'results/**/*.xml'
                }
                failure {
                    sh 'gather-logs.sh'
                    mail to: 'team@example.com',
                         subject: "Tests failed on ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                         body: "See ${env.BUILD_URL} for details."
                }
                unstable {
                    echo 'Tests passed but coverage threshold not met'
                }
            }
        }
    }
    
    post {
        always {
            echo 'Pipeline complete. Cleanup initiated.'
            deleteDir()  // Alternative to cleanWs for complete directory removal
        }
        success {
            slackSend channel: '#deployments',
                      color: 'good',
                      message: "Release ${env.BUILD_NUMBER} deployed successfully."
        }
    }
}

The available post conditions are: always, success, failure, unstable, aborted, fixed, regression, and changed. Stage-level post blocks inherit the stage's status; top-level post blocks apply to the entire Pipeline.

6. Parallel Execution

To speed up your Pipeline, run independent stages or steps in parallel. The parallel directive achieves this easily:

pipeline {
    agent none
    
    stages {
        stage('Static Analysis and Unit Tests') {
            parallel {
                stage('Lint') {
                    agent { label 'linux' }
                    steps {
                        sh 'npm run lint'
                    }
                }
                stage('Unit Tests - Linux') {
                    agent { label 'linux' }
                    steps {
                        sh 'npm test'
                    }
                }
                stage('Unit Tests - Windows') {
                    agent { label 'windows' }
                    steps {
                        bat 'npm test'  // bat for Windows batch commands
                    }
                }
                stage('Security Scan') {
                    agent { docker { image 'aquasec/trivy' } }
                    steps {
                        sh 'trivy filesystem --severity HIGH,CRITICAL --exit-code 1 .'
                    }
                }
            }
        }
        stage('Build and Package') {
            // This runs only after all parallel stages succeed
            agent { label 'linux' }
            steps {
                sh 'npm run build'
                sh 'docker build -t myapp:${BUILD_NUMBER} .'
            }
        }
    }
}

Each parallel branch can run on a different agent, making this pattern ideal for cross-platform testing. The pipeline progresses to the next stage only after all parallel branches complete successfully.

7. Manual Approval Gates with Input

The input step pauses the Pipeline and waits for a human to approve or reject it. This is essential for production deployments and compliance workflows:

pipeline {
    agent any
    
    stages {
        stage('Deploy to Staging') {
            steps {
                sh './deploy-staging.sh'
            }
        }
        stage('Approval Gate') {
            steps {
                input(
                    message: 'Proceed with production deployment?',
                    ok: 'Deploy to Production',
                    submitter: 'ops-team, lead-developer',
                    parameters: [
                        choice(name: 'ROLLBACK_WINDOW_HOURS',
                               choices: ['1', '2', '4', '24'],
                               description: 'Rollback window in hours')
                    ]
                )
            }
        }
        stage('Deploy to Production') {
            steps {
                sh './deploy-production.sh'
            }
        }
    }
}

The submitter parameter restricts who can approve the input by specifying user IDs or group names. You can also attach parameters to the input, allowing the approver to supply additional configuration at approval time.

8. Using Triggers for Automated Execution

Triggers define when a Pipeline should run automatically. The most common trigger is polling SCM, but webhook-driven triggers (via the Multibranch Pipeline or GitHub/Bitbucket branch source plugins) are preferred for responsiveness:

pipeline {
    agent any
    
    triggers {
        // Poll SCM every 5 minutes (legacy approach; webhooks are better)
        pollSCM 'H/5 * * * *'
        
        // Cron-style scheduling: run daily at 2 AM
        cron '0 2 * * *'
        
        // Trigger when upstream job completes successfully
        upstream upstreamProjects: 'build-libs', threshold: 'SUCCESS'
    }
    
    stages {
        stage('Build') {
            steps {
                echo 'Triggered automatically!'
            }
        }
    }
}

For modern setups, use a Multibranch Pipeline with a GitHub or Bitbucket organization folder. This automatically discovers repositories, creates branches for pull requests, and triggers builds on webhook events without any manual triggers block configuration.

9. Shared Libraries for Code Reuse

When you have common functionality repeated across multiple Pipelines, extract it into a Shared Library. Configure it once in Jenkins Global Settings under "Global Pipeline Libraries," then reference it in your Jenkinsfile:

// Jenkinsfile
@Library('my-shared-lib') _

pipeline {
    agent any
    stages {
        stage('Deploy') {
            steps {
                script {
                    // Call a custom function defined in vars/deployApp.groovy
                    deployApp(
                        environment: 'staging',
                        artifactPath: 'target/app.war'
                    )
                }
            }
        }
    }
}

Inside the shared library repository, you would have a file vars/deployApp.groovy:

// vars/deployApp.groovy
def call(Map config) {
    def env = config.environment
    def artifact = config.artifactPath
    
    echo "Deploying ${artifact} to ${env}"
    
    // Common deployment logic
    sh """
        scp ${artifact} deploy-server-${env}:/opt/apps/
        ssh deploy-server-${env} 'sudo systemctl restart app'
    """
    
    // Return deployment metadata for downstream use
    return [host: "deploy-server-${env}", timestamp: new Date().time]
}

Shared libraries can also contain Groovy classes under src/ for more complex logic and step implementations, giving you full object-oriented capabilities within your Pipeline code.

10. Scripted Pipeline Essentials

While Declarative Pipeline covers 90% of use cases, there are situations where you need the full power of Groovy scripting. Scripted Pipeline gives you fine-grained control over flow, loops, and dynamic stage creation:

node('linux') {
    // Clean workspace and checkout
    deleteDir()
    checkout scm
    
    // Define stages dynamically based on discovered modules
    def modules = sh(script: 'find . -name "pom.xml" -maxdepth 2 | sed "s|./||;s|/pom.xml||"', returnStdout: true).trim().split('\n')
    
    def buildResults = [:]
    
    for (int i = 0; i < modules.size(); i++) {
        def module = modules[i]
        def moduleName = module.replace('/', '-')
        
        buildResults[moduleName] = {
            stage("Build ${moduleName}") {
                dir(module) {
                    sh 'mvn clean install'
                    stash name: moduleName, includes: 'target/**/*.jar'
                }
            }
        }
    }
    
    // Execute all module builds in parallel
    parallel buildResults
    
    stage('Aggregate') {
        for (moduleName in buildResults.keySet()) {
            unstash moduleName
        }
        sh 'mvn verify -DskipTests'
    }
    
    // Conditional deployment logic
    if (env.BRANCH_NAME == 'main') {
        stage('Deploy') {
            input 'Proceed with deployment?'
            sh './deploy.sh production'
        }
    } else {
        stage('Deploy to Preview') {
            sh './deploy.sh preview --branch=${env.BRANCH_NAME}'
        }
    }
}

Scripted Pipeline allows try/catch/finally blocks for granular error handling, dynamic parallel stages via map construction, and complex conditional branching that would be difficult in declarative syntax. Use it when you genuinely need programmatic flow control, but default to declarative for simpler, more maintainable Pipelines.

Best Practices

Following these practices will keep your Pipelines maintainable, secure, and performant over time:

stage('Integration Tests') {
    steps {
        timeout(time: 30, unit: 'MINUTES') {
            sh './run-long-tests.sh'
        }
    }
}

Conclusion

Jenkins Pipeline transforms your CI/CD process from a fragile, UI-configured sequence of jobs into a resilient, version-controlled, code-driven delivery pipeline. By mastering the Declarative Pipeline syntax, understanding agent configuration, leveraging parallel execution, incorporating manual approval gates, and extracting reusable logic into shared libraries, you can build workflows that scale across teams and projects with minimal duplication. The investment in learning Pipeline as Code pays dividends through faster feedback loops, reproducible builds, and a delivery process that evolves alongside your application code. Start by converting one freestyle job into a simple Jenkinsfile, then progressively adopt the advanced patterns covered here to unlock the full potential of continuous delivery with Jenkins.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles