Back to Software Engineering & Delivery Mastery Series CI/CD Platform Deep Dive

CI/CD Platform Deep Dive: Jenkins

May 14, 2026 Wasil Zafar 46 min read

Master Jenkins from architecture to modernization — declarative and scripted pipelines, shared libraries, agent management, the plugin ecosystem, credentials, Blue Ocean, Jenkins Configuration as Code, and migrating legacy installations to modern practices.

Table of Contents

  1. Introduction
  2. Architecture
  3. Declarative vs Scripted Pipelines
  4. Agent Management
  5. Pipeline Syntax Deep Dive
  6. Shared Libraries
  7. Plugin Ecosystem
  8. Credentials Management
  9. Blue Ocean
  10. Configuration as Code
  11. Multibranch Pipelines
  12. Scaling Jenkins
  13. Modernizing Jenkins
  14. Security
  15. Exercises

Introduction

Jenkins is the grandfather of CI/CD. Born as Hudson in 2005 at Sun Microsystems, forked to Jenkins in 2011 after an Oracle naming dispute, it became the default CI server for an entire generation of developers. Over two decades later, Jenkins still powers an estimated 50%+ of enterprise CI/CD infrastructure — a testament to its flexibility, extensibility, and the sheer inertia of production systems.

Jenkins operates on a fundamentally different model than GitHub Actions or GitLab CI/CD. Where those platforms are hosted services that manage infrastructure for you, Jenkins is a self-managed application you install, configure, maintain, and scale yourself. This gives you total control — and total responsibility.

Key Insight: Jenkins' greatest strength is its greatest weakness: unlimited flexibility. With 1,800+ plugins, you can make Jenkins do anything. But that flexibility means every Jenkins installation is unique — what works in one organisation may be completely different in another. There's no "standard" Jenkins setup.

Why Jenkins Still Matters

Despite the rise of cloud-native CI/CD platforms, Jenkins remains critical for several reasons:

  • Legacy investment — Thousands of enterprises have years of pipeline logic, custom plugins, and institutional knowledge in Jenkins
  • Air-gapped environments — Classified government, defense, and financial systems that cannot use cloud services
  • Custom hardware — Embedded systems, FPGA programming, hardware-in-the-loop testing that requires specific physical machines
  • Maximum control — Organisations that need complete ownership of their CI/CD infrastructure for compliance or security
  • Plugin ecosystem — Integrations with legacy tools (mainframes, proprietary build systems) that cloud platforms don't support

Architecture

Jenkins follows a controller-agent architecture (formerly called master-slave). The controller manages configuration, scheduling, and the web UI. Agents execute the actual build jobs.

Jenkins Controller-Agent Architecture
flowchart TD
    subgraph Controller
        A[Web UI / API]
        B[Job Scheduler]
        C[Plugin Manager]
        D[Credential Store]
    end
    
    subgraph Agents
        E[Linux Agent 1
Docker Executor] F[Linux Agent 2
Shell Executor] G[Windows Agent
.NET Builds] H[macOS Agent
iOS Builds] end B -->|SSH| E B -->|JNLP| F B -->|JNLP| G B -->|SSH| H style A fill:#3B9797,color:#fff style B fill:#132440,color:#fff style C fill:#16476A,color:#fff style D fill:#16476A,color:#fff style E fill:#3B9797,color:#fff style F fill:#3B9797,color:#fff style G fill:#BF092F,color:#fff style H fill:#132440,color:#fff

Communication Protocols

  • SSH — Controller initiates SSH connection to agents. Most common for Linux/macOS agents. Requires SSH key management.
  • JNLP (Java Network Launch Protocol) — Agent initiates connection to controller. Essential when agents are behind firewalls or NAT. Also called "inbound agents."
  • WebSocket — Modern alternative to JNLP. Agent connects via WebSocket through standard HTTPS. Works with reverse proxies without special configuration.
Critical Security Rule: Never run builds on the Jenkins controller. The controller has access to all credentials, all job configurations, and the Jenkins home directory. A compromised build job running on the controller gives an attacker full access to your entire CI/CD system. Always use agents for job execution.

Declarative vs Scripted Pipelines

Jenkins supports two pipeline syntaxes, both defined in a Jenkinsfile stored in your repository root. Declarative is the modern, recommended syntax — structured and opinionated. Scripted is the original Groovy DSL — powerful but harder to maintain.

Declarative Pipeline

// Jenkinsfile (Declarative)
pipeline {
    agent any
    
    options {
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timestamps()
    }
    
    environment {
        APP_NAME = 'my-service'
        DOCKER_REGISTRY = 'registry.example.com'
        VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT.take(7)}"
    }
    
    parameters {
        choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Deploy target')
        booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: 'Skip test stage')
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_AUTHOR = sh(script: 'git log -1 --format=%an', returnStdout: true).trim()
                }
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
            }
        }
        
        stage('Test') {
            when {
                expression { !params.SKIP_TESTS }
            }
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit'
                    }
                    post {
                        always {
                            junit 'reports/junit/*.xml'
                        }
                    }
                }
                stage('Integration Tests') {
                    agent {
                        docker {
                            image 'node:20'
                            args '--network=ci-network'
                        }
                    }
                    steps {
                        sh 'npm run test:integration'
                    }
                }
            }
        }
        
        stage('Docker Build') {
            steps {
                script {
                    docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-creds') {
                        def image = docker.build("${DOCKER_REGISTRY}/${APP_NAME}:${VERSION}")
                        image.push()
                        image.push('latest')
                    }
                }
            }
        }
        
        stage('Deploy') {
            when {
                branch 'main'
            }
            input {
                message "Deploy to ${params.ENVIRONMENT}?"
                ok "Deploy"
                submitter "admin,deployers"
            }
            steps {
                withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
                    sh """
                        kubectl set image deployment/${APP_NAME} \
                            app=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} \
                            --namespace=${params.ENVIRONMENT}
                    """
                }
            }
        }
    }
    
    post {
        success {
            slackSend(color: 'good', message: "Build #${BUILD_NUMBER} succeeded: ${env.BUILD_URL}")
        }
        failure {
            slackSend(color: 'danger', message: "Build #${BUILD_NUMBER} FAILED: ${env.BUILD_URL}")
        }
        always {
            cleanWs()
        }
    }
}

Scripted Pipeline

// Jenkinsfile (Scripted) — full Groovy power
node('linux') {
    def version = ''
    
    try {
        stage('Checkout') {
            checkout scm
            version = sh(script: 'git describe --tags --always', returnStdout: true).trim()
        }
        
        stage('Build') {
            sh 'make build'
        }
        
        stage('Test') {
            parallel(
                'unit': {
                    sh 'make test-unit'
                },
                'integration': {
                    sh 'make test-integration'
                }
            )
        }
        
        if (env.BRANCH_NAME == 'main') {
            stage('Deploy') {
                input message: 'Deploy to production?', ok: 'Deploy'
                withCredentials([string(credentialsId: 'deploy-token', variable: 'TOKEN')]) {
                    sh "deploy --version=${version} --token=${TOKEN}"
                }
            }
        }
        
        currentBuild.result = 'SUCCESS'
    } catch (e) {
        currentBuild.result = 'FAILURE'
        throw e
    } finally {
        // Always runs
        junit allowEmptyResults: true, testResults: '**/test-results/*.xml'
        cleanWs()
    }
}
When to Use Each Syntax

Declarative: Use for 90% of pipelines. Structured, easier to read, better error messages, supports the Blue Ocean visual editor, built-in post conditions, and when directives. Start here.

Scripted: Use when you need full Groovy power — complex conditionals, dynamic stage generation, calling Java APIs, or sophisticated error handling that declarative can't express. Most teams never need this.

Hybrid: Declarative pipelines support script { } blocks for escaping into scripted mode when needed. This gives you the best of both worlds.

Agent Management

Agents determine where pipeline stages execute. Jenkins supports multiple agent types for different use cases:

// Agent types in declarative pipelines
pipeline {
    // Run on any available agent
    agent any
    
    stages {
        stage('Build on Linux') {
            agent { label 'linux && docker' }
            steps { sh 'make build' }
        }
        
        stage('Build on Windows') {
            agent { label 'windows && vs2022' }
            steps { bat 'msbuild /p:Configuration=Release' }
        }
        
        stage('Docker Agent') {
            agent {
                docker {
                    image 'maven:3.9-eclipse-temurin-21'
                    args '-v $HOME/.m2:/root/.m2'  // Mount Maven cache
                    label 'docker-capable'          // Run on agents with Docker
                }
            }
            steps { sh 'mvn clean verify' }
        }
        
        stage('Kubernetes Agent') {
            agent {
                kubernetes {
                    yaml '''
                        apiVersion: v1
                        kind: Pod
                        spec:
                          containers:
                          - name: maven
                            image: maven:3.9-eclipse-temurin-21
                            command: ['sleep', '99d']
                            volumeMounts:
                            - name: m2-cache
                              mountPath: /root/.m2
                          - name: docker
                            image: docker:24-dind
                            securityContext:
                              privileged: true
                          volumes:
                          - name: m2-cache
                            persistentVolumeClaim:
                              claimName: maven-cache
                    '''
                    defaultContainer 'maven'
                }
            }
            steps {
                sh 'mvn clean package'
                container('docker') {
                    sh 'docker build -t myapp .'
                }
            }
        }
    }
}

Pipeline Syntax Deep Dive

Declarative pipeline directives give you fine-grained control over execution flow:

pipeline {
    agent none  // No default agent — each stage declares its own
    
    triggers {
        // Poll SCM every 5 minutes
        pollSCM('H/5 * * * *')
        // Or use webhook: GitHub hook trigger for GITScm polling
        // Or cron: cron('0 6 * * 1-5')
    }
    
    tools {
        jdk 'OpenJDK-21'
        maven 'Maven-3.9'
        nodejs 'Node-20'
    }
    
    environment {
        // Static values
        SERVICE_NAME = 'payment-service'
        // Credentials (automatically masked in logs)
        AWS_CREDS = credentials('aws-access-key')  // Sets AWS_CREDS, AWS_CREDS_USR, AWS_CREDS_PSW
        // Computed values
        GIT_SHORT_SHA = "${env.GIT_COMMIT?.take(7) ?: 'unknown'}"
    }
    
    stages {
        stage('Parallel Testing') {
            parallel {
                stage('Unit') {
                    agent { label 'linux' }
                    steps { sh 'make test-unit' }
                }
                stage('Lint') {
                    agent { label 'linux' }
                    steps { sh 'make lint' }
                }
                stage('Security') {
                    agent { label 'linux' }
                    steps { sh 'make security-scan' }
                }
            }
            // fail-fast: if one parallel stage fails, abort others
            failFast true
        }
        
        stage('Conditional Deploy') {
            when {
                allOf {
                    branch 'main'
                    not { changeRequest() }
                    expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
                }
            }
            steps {
                echo "Deploying ${SERVICE_NAME}..."
            }
        }
    }
}

Shared Libraries

Shared libraries are Jenkins' answer to code reuse across pipelines. They let you write custom pipeline steps, utility functions, and organisational standards in a separate Git repository that all pipelines can import.

Shared Library Structure
flowchart TD
    A[Shared Library Repo] --> B[vars/]
    A --> C[src/]
    A --> D[resources/]
    
    B --> E[deployService.groovy
Custom pipeline step] B --> F[buildDocker.groovy
Docker build step] B --> G[notifySlack.groovy
Notification step] C --> H[com/myorg/Pipeline.groovy
Groovy classes] C --> I[com/myorg/Docker.groovy
Helper classes] D --> J[templates/
Config templates] style A fill:#132440,color:#fff style B fill:#3B9797,color:#fff style C fill:#16476A,color:#fff style D fill:#BF092F,color:#fff
// vars/deployService.groovy — Custom pipeline step
// Called as: deployService(service: 'payment', env: 'staging')

def call(Map config) {
    def service = config.service
    def environment = config.env ?: 'staging'
    def timeout = config.timeout ?: 300
    
    echo "Deploying ${service} to ${environment}..."
    
    withCredentials([file(credentialsId: "${environment}-kubeconfig", variable: 'KUBECONFIG')]) {
        sh """
            kubectl rollout restart deployment/${service} -n ${environment}
            kubectl rollout status deployment/${service} -n ${environment} --timeout=${timeout}s
        """
    }
    
    // Verify health check
    def url = "https://${service}.${environment}.example.com/health"
    retry(3) {
        sleep(time: 10, unit: 'SECONDS')
        def response = httpRequest(url: url, validResponseCodes: '200')
        echo "Health check passed: ${response.status}"
    }
}
// vars/buildDocker.groovy — Reusable Docker build step
def call(Map config) {
    def imageName = config.image ?: env.JOB_NAME.toLowerCase()
    def registry = config.registry ?: 'registry.example.com'
    def tag = config.tag ?: env.BUILD_NUMBER
    def dockerfile = config.dockerfile ?: 'Dockerfile'
    
    def fullImage = "${registry}/${imageName}:${tag}"
    
    docker.withRegistry("https://${registry}", 'docker-registry-creds') {
        def image = docker.build(fullImage, "-f ${dockerfile} .")
        image.push()
        image.push('latest')
        return fullImage
    }
}
// Jenkinsfile — Using shared library
@Library('my-org-pipeline-lib@main') _

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'npm ci && npm run build'
            }
        }
        stage('Docker') {
            steps {
                script {
                    env.IMAGE = buildDocker(
                        image: 'payment-service',
                        tag: "${BUILD_NUMBER}-${GIT_COMMIT.take(7)}"
                    )
                }
            }
        }
        stage('Deploy Staging') {
            steps {
                deployService(service: 'payment-service', env: 'staging')
            }
        }
        stage('Deploy Production') {
            when { branch 'main' }
            input { message 'Deploy to production?' }
            steps {
                deployService(service: 'payment-service', env: 'production')
            }
        }
    }
    post {
        failure {
            notifySlack(channel: '#deployments', status: 'FAILED')
        }
    }
}

Plugin Ecosystem

Jenkins' 1,800+ plugins are both its superpower and its Achilles heel. The plugin ecosystem provides integrations with virtually every tool in existence — but creates a maintenance burden of compatibility issues, security vulnerabilities, and upgrade conflicts.

Essential Jenkins Plugins (2026)

Pipeline — The foundation. Provides Jenkinsfile support, declarative/scripted syntax, and pipeline visualization. Required.

Git / GitHub / GitLab — SCM integration. Webhook triggers, PR builders, commit status updates.

Docker Pipeline — Build/push Docker images, use containers as agents. docker.build(), docker.image().inside().

Credentials Binding — Inject credentials into builds as environment variables. withCredentials() step.

Kubernetes — Dynamic pod-based agents. Scales to hundreds of concurrent builds.

Job DSL — Define jobs programmatically in Groovy. Enables "jobs-as-code" for managing hundreds of pipelines.

Configuration as Code (JCasC) — Manage Jenkins configuration via YAML. Reproducible, versionable setup.

Warnings Next Generation — Parse build logs for warnings from any tool (compiler, linter, scanner).

// plugins.txt — Pin plugin versions for reproducible Jenkins
// Used with: jenkins-plugin-cli --plugin-file plugins.txt
workflow-aggregator:596.v8c21c963d92d
git:5.2.1
docker-workflow:572.v950f58993843
credentials-binding:677.vdc9c15f0f428
kubernetes:4203.v1dd44f5b_1cf9
job-dsl:1.87
configuration-as-code:1775.v810dc950b_514
warnings-ng:10.7.0
pipeline-utility-steps:2.16.2
slack:684.v833089650554
The Plugin Dependency Problem: Jenkins plugins depend on other plugins, which depend on others. Upgrading one plugin can break five others. Always: (1) test upgrades in a staging Jenkins instance, (2) pin plugin versions explicitly, (3) use the Plugin Health Score to evaluate plugins before installing, (4) minimise installed plugins — every plugin is a potential security vulnerability.

Credentials Management

Jenkins stores credentials in an encrypted credential store. Credentials are injected into builds at runtime and automatically masked in console output.

// Using credentials in pipelines
pipeline {
    agent any
    
    environment {
        // Username + password credential: sets VAR, VAR_USR, VAR_PSW
        DOCKER_CREDS = credentials('docker-registry')
        // Secret text: sets single variable
        SLACK_TOKEN = credentials('slack-bot-token')
    }
    
    stages {
        stage('Deploy') {
            steps {
                // withCredentials for scoped access
                withCredentials([
                    // SSH key
                    sshUserPrivateKey(
                        credentialsId: 'deploy-ssh-key',
                        keyFileVariable: 'SSH_KEY',
                        usernameVariable: 'SSH_USER'
                    ),
                    // File credential (kubeconfig, certificates)
                    file(credentialsId: 'prod-kubeconfig', variable: 'KUBECONFIG'),
                    // Username + password
                    usernamePassword(
                        credentialsId: 'nexus-creds',
                        usernameVariable: 'NEXUS_USER',
                        passwordVariable: 'NEXUS_PASS'
                    )
                ]) {
                    sh '''
                        # SSH_KEY, KUBECONFIG, NEXUS_USER, NEXUS_PASS available here
                        kubectl --kubeconfig=$KUBECONFIG apply -f k8s/
                    '''
                }
                // Variables are NOT available outside withCredentials block
            }
        }
    }
}

Blue Ocean

Blue Ocean was Jenkins' attempt at a modern UI — visual pipeline editor, better visualisation, and a cleaner experience for developers who don't want to learn Jenkins' classic interface. Launched in 2016, it provided significant UX improvements but was officially deprecated in 2024 due to maintenance challenges.

Key Blue Ocean features (still functional but unmaintained):

  • Pipeline visualization — Graphical representation of pipeline stages with parallel execution clearly shown
  • Visual pipeline editor — Create pipelines without writing Jenkinsfile YAML/Groovy
  • GitHub/Bitbucket integration — One-click repository scanning and pipeline creation
  • Personalized dashboard — Shows only pipelines relevant to you
Blue Ocean Status (2026): Deprecated but still included in Jenkins distributions. No new features, only critical security patches. For new installations, use the classic UI with the Pipeline Graph View plugin for visualization. For teams that depend on Blue Ocean, plan migration to either the classic UI or a different CI/CD platform.

Jenkins Configuration as Code (JCasC)

JCasC eliminates manual UI configuration by defining your entire Jenkins setup in YAML files. This makes Jenkins instances reproducible, versionable, and recoverable — critical for disaster recovery and environment parity.

# jenkins.yaml — Complete Jenkins configuration
jenkins:
  systemMessage: "Jenkins configured via JCasC"
  numExecutors: 0  # No builds on controller!
  
  securityRealm:
    ldap:
      configurations:
        - server: "ldap.example.com"
          rootDN: "dc=example,dc=com"
          userSearch: "uid={0}"
  
  authorizationStrategy:
    roleBased:
      roles:
        global:
          - name: "admin"
            permissions:
              - "Overall/Administer"
            assignments:
              - "admin-group"
          - name: "developer"
            permissions:
              - "Job/Build"
              - "Job/Read"
              - "Job/Workspace"
            assignments:
              - "dev-group"
  
  nodes:
    - permanent:
        name: "linux-agent-01"
        remoteFS: "/var/jenkins"
        launcher:
          ssh:
            host: "agent01.example.com"
            credentialsId: "agent-ssh-key"
            sshHostKeyVerificationStrategy:
              knownHostsFileKeyVerificationStrategy:
                knownHostsFile: "/var/jenkins_home/.ssh/known_hosts"
        labelString: "linux docker"
        numExecutors: 4

  clouds:
    - kubernetes:
        name: "k8s"
        serverUrl: "https://kubernetes.default"
        namespace: "jenkins"
        jenkinsUrl: "http://jenkins.jenkins.svc:8080"
        templates:
          - name: "default"
            label: "k8s-agent"
            containers:
              - name: "jnlp"
                image: "jenkins/inbound-agent:latest"
                resourceLimitCpu: "500m"
                resourceLimitMemory: "512Mi"

unclassified:
  location:
    url: "https://jenkins.example.com/"
  
  slackNotifier:
    teamDomain: "mycompany"
    tokenCredentialId: "slack-token"
    room: "#ci-notifications"

  globalLibraries:
    libraries:
      - name: "my-org-lib"
        defaultVersion: "main"
        retriever:
          modernSCM:
            scm:
              git:
                remote: "https://github.com/my-org/jenkins-shared-lib.git"
                credentialsId: "github-token"

credentials:
  system:
    domainCredentials:
      - credentials:
          - usernamePassword:
              id: "docker-registry"
              username: "deploy-bot"
              password: "${DOCKER_PASSWORD}"  # From environment variable
          - string:
              id: "slack-token"
              secret: "${SLACK_TOKEN}"
# Docker-based Jenkins with JCasC
# Dockerfile
FROM jenkins/jenkins:lts-jdk21

# Skip setup wizard
ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false"
ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc_configs

# Install plugins
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt

# Copy JCasC configuration
COPY jenkins.yaml /var/jenkins_home/casc_configs/jenkins.yaml

Multibranch Pipelines

Multibranch pipelines automatically discover branches in your repository and create pipeline jobs for each branch that contains a Jenkinsfile. This is Jenkins' equivalent of GitHub Actions' per-branch workflow execution.

// Jenkinsfile with branch-specific behavior
pipeline {
    agent any
    
    stages {
        stage('Build') {
            steps {
                sh 'make build'
            }
        }
        
        stage('Test') {
            steps {
                sh 'make test'
            }
        }
        
        stage('Deploy to Dev') {
            when {
                branch 'develop'
            }
            steps {
                sh 'make deploy-dev'
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'main'
            }
            steps {
                sh 'make deploy-staging'
            }
        }
        
        stage('Deploy to Production') {
            when {
                // Only on tagged releases
                buildingTag()
            }
            steps {
                input message: 'Deploy to production?'
                sh 'make deploy-production'
            }
        }
        
        stage('PR Validation') {
            when {
                changeRequest()  // Only on pull requests
            }
            steps {
                sh 'make validate-pr'
                // Post status back to GitHub/GitLab
            }
        }
    }
}

Scaling Jenkins

Jenkins scaling follows a predictable path as organisations grow:

Jenkins Scaling Journey
flowchart LR
    A[Single Controller
Few agents] --> B[Controller +
Static Agents] B --> C[Controller +
Cloud Agents] C --> D[Controller +
Kubernetes Agents] D --> E[Multiple Controllers
+ Shared Agents] style A fill:#3B9797,color:#fff style B fill:#16476A,color:#fff style C fill:#132440,color:#fff style D fill:#BF092F,color:#fff style E fill:#132440,color:#fff
# Kubernetes-based autoscaling with Jenkins Operator
apiVersion: jenkins.io/v1alpha2
kind: Jenkins
metadata:
  name: jenkins-prod
  namespace: jenkins
spec:
  configurationAsCode:
    configurations:
      - name: jenkins-config
  master:
    containers:
      - name: jenkins-master
        image: jenkins/jenkins:lts-jdk21
        resources:
          limits:
            cpu: "2"
            memory: "4Gi"
          requests:
            cpu: "1"
            memory: "2Gi"
    plugins:
      - name: kubernetes
        version: "4203.v1dd44f5b_1cf9"
      - name: workflow-aggregator
        version: "596.v8c21c963d92d"
      - name: configuration-as-code
        version: "1775.v810dc950b_514"

Modernizing Jenkins

Most Jenkins installations accumulate technical debt: freestyle jobs, unmaintained plugins, manual configuration, and tribal knowledge. Here's a systematic approach to modernisation:

Migration Path: Freestyle → Pipeline

// Step 1: Convert freestyle job to scripted pipeline (1:1 mapping)
node('linux') {
    stage('Checkout') {
        git url: 'https://github.com/org/repo.git', branch: 'main'
    }
    stage('Build') {
        sh './gradlew clean build'
    }
    stage('Archive') {
        archiveArtifacts artifacts: 'build/libs/*.jar'
        junit 'build/test-results/**/*.xml'
    }
}

// Step 2: Refactor to declarative (structured, maintainable)
pipeline {
    agent { label 'linux' }
    stages {
        stage('Build') {
            steps {
                sh './gradlew clean build'
            }
            post {
                always {
                    junit 'build/test-results/**/*.xml'
                    archiveArtifacts artifacts: 'build/libs/*.jar'
                }
            }
        }
    }
}

// Step 3: Extract shared logic to library, add Docker agents, add JCasC

Jenkins → GitHub Actions Migration

# Jenkins Declarative:
# pipeline {
#     agent { docker { image 'node:20' } }
#     stages {
#         stage('Test') { steps { sh 'npm test' } }
#         stage('Deploy') { when { branch 'main' } steps { sh './deploy.sh' } }
#     }
# }

# Equivalent GitHub Actions:
name: CI/CD
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    container: node:20
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
Migration Decision Framework

Stay on Jenkins when: Air-gapped environments, custom hardware requirements, deep plugin dependencies (mainframe integrations, proprietary tools), regulatory requirements for self-hosted CI, or massive existing investment that can't justify rewrite cost.

Migrate to GitHub Actions when: Code already on GitHub, team wants zero-infrastructure CI, open-source projects, standard build/test/deploy workflows, need for OIDC cloud authentication.

Migrate to GitLab CI when: Want integrated DevOps platform, need built-in security scanning, want container registry + CI + CD in one tool, value merge trains and review apps.

Security

Jenkins security requires defence in depth — the controller is a high-value target that stores credentials, controls deployments, and has access to source code across all projects.

// Groovy sandbox restrictions
// Jenkins pipelines run in a Groovy sandbox by default.
// Dangerous operations require admin approval via "Script Approval"

// BLOCKED by sandbox (requires approval):
new File('/etc/passwd').text           // File system access
'rm -rf /'.execute()                    // OS command execution
System.getenv('SECRET')                 // Environment access outside pipeline

// ALLOWED in sandbox:
env.BUILD_NUMBER                        // Pipeline environment variables
sh 'echo hello'                         // Pipeline steps (already controlled)
currentBuild.result = 'SUCCESS'         // Build status

Critical Security Measures:

  • RBAC — Use Matrix Authorization or Role-Based Strategy plugin. Never give developers admin access to Jenkins.
  • Groovy Sandbox — Keep enabled. Review script approvals carefully — each approval is a potential security hole.
  • CSRF Protection — Enabled by default since Jenkins 2.x. Never disable it.
  • Agent-to-Controller Security — Enable "Agent → Controller Access Control" in Global Security. Prevents agents from accessing controller file system.
  • Credential Scope — Use folder-scoped credentials. Don't put all credentials at global scope.
  • Audit Trail — Install the Audit Trail plugin. Log all configuration changes, job runs, and credential access.
  • Plugin Security — Subscribe to Jenkins Security Advisories. Update plugins promptly. Remove unused plugins.
Zero-Trust Jenkins: Treat your Jenkins controller like a production database server. It should have: restricted network access (no direct internet), TLS everywhere, regular backups, monitoring for unauthorized access, and a documented incident response plan. A compromised Jenkins controller means compromised credentials, compromised deployments, and potentially compromised production systems.

Exercises

Exercise 1: Production Declarative Pipeline

Write a declarative Jenkinsfile for a Java application with: parallel stages (unit tests + integration tests + security scan), Docker agent for the build stage, withCredentials for deployment, manual approval gate before production, and a post block that sends Slack notifications on success/failure and always cleans the workspace.

Exercise 2: Shared Library

Create a Jenkins shared library with three custom steps: buildDocker(image, tag, registry) for building and pushing Docker images, deployK8s(service, namespace, image) for Kubernetes deployments, and notifyTeams(channel, status, message) for Microsoft Teams notifications. Write a consumer Jenkinsfile that uses all three.

Exercise 3: Jenkins Configuration as Code

Create a complete JCasC YAML file that configures: LDAP authentication, role-based authorization (admin, developer, viewer roles), two Kubernetes cloud agent templates (one for Java builds, one for Node.js builds), a global shared library, and Slack notification settings. Create a Dockerfile that bakes this configuration into a Jenkins image.

Exercise 4: Migration Plan

Given a Jenkins installation with 50 freestyle jobs, 10 pipeline jobs, and 5 shared libraries: design a migration plan to GitHub Actions. Map Jenkins concepts to GitHub Actions equivalents (agents → runners, shared libraries → reusable workflows, credentials → secrets + OIDC). Identify which jobs can be directly migrated and which require redesign.