Skip to content
AgileLogics
7 min read

Jenkins Pipelines

Prerequisites: Building domain-specific languages

Jenkins is a widely used CI/CD server. Its pipelines are defined in a Jenkinsfile at the root of a repository, written in a Groovy-based DSL.

Declarative pipeline

// illustrative — Jenkinsfile
pipeline {
    agent any

    options {
        timeout(time: 30, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '20'))
    }

    environment {
        APP_ENV = 'staging'
    }

    stages {
        stage('Build') {
            steps {
                sh './gradlew assemble'
            }
        }
        stage('Test') {
            steps {
                sh './gradlew test'
            }
            post {
                always {
                    junit 'build/test-results/test/*.xml'
                }
            }
        }
        stage('Deploy') {
            when { branch 'main' }
            steps {
                withCredentials([string(credentialsId: 'deploy-token', variable: 'TOKEN')]) {
                    sh './deploy.sh --env "$APP_ENV"'
                }
            }
        }
    }

    post {
        failure {
            echo "Build ${env.BUILD_NUMBER} failed"
        }
    }
}
  • Declarative syntax (pipeline { }) has a fixed structure: an agent, stages with steps, and post actions. It's validated before the build starts and is the recommended style.
  • sh (or bat/powershell on Windows agents) runs shell commands.
  • withCredentials exposes secrets as environment variables and masks them in logs. Note the single quotes around the shell command: the shell expands $APP_ENV, so secrets never pass through Groovy string interpolation (which Jenkins warns about).

Parallel stages and matrices

// illustrative — Jenkinsfile (inside stages { })
stage('Checks') {
    parallel {
        stage('Unit tests') { steps { sh './gradlew test' } }
        stage('Lint') { steps { sh './gradlew codenarcMain' } }
    }
}

Scripted pipeline and script blocks

// illustrative — Jenkinsfile (scripted style)
node {
    stage('Checkout') {
        checkout scm
    }
    def services = ['api', 'worker', 'web']
    stage('Build') {
        services.each { svc ->
            sh "docker build -t shop/${svc}:${env.BUILD_NUMBER} ${svc}"
        }
    }
}

Scripted pipelines are plain Groovy inside node { } — full flexibility, less structure. In declarative pipelines, a script { ... } block allows small pieces of scripted code.

Pipeline Groovy is special

Jenkins runs pipeline code in a CPS (continuation-passing style) interpreter, so a build can pause and survive a Jenkins restart. This has consequences:

  • Some Groovy features behave differently or aren't supported in CPS code — closures passed to certain GDK methods (like sort with a comparator closure) historically misbehaved. Keep pipeline logic simple and move complex code into @NonCPS methods or, better, into scripts the pipeline calls.
  • Pipelines run in the Script Security sandbox: many methods need administrator approval.
  • Objects stored in variables must be serializable, because the pipeline state is saved to disk.

Shared libraries

// illustrative — vars/gradleBuild.groovy in a shared library repository
def call(Map config = [:]) {
    def tasks = config.tasks ?: 'build'
    sh "./gradlew ${tasks} --no-daemon"
    if (config.publishTests != false) {
        junit allowEmptyResults: true, testResults: '**/build/test-results/**/*.xml'
    }
}
// illustrative — Jenkinsfile using the library
@Library('ci-shared') _

pipeline {
    agent any
    stages {
        stage('Build') {
            steps { gradleBuild tasks: 'check' }
        }
    }
}

A shared library puts common steps in one repository: files in vars/ become global steps, classes in src/ are ordinary Groovy classes. It keeps each team's Jenkinsfile short and consistent.

Tips

  • Validate a declarative Jenkinsfile before committing with the Pipeline Linter (the declarative-linter CLI command or the "Declarative Directive Generator" in the UI), and lint Groovy style with npm-groovy-lint.
  • Keep heavy logic in build tools and scripts (Gradle, shell, Groovy scripts run with sh 'groovy ci/report.groovy'), not in the pipeline itself — they're easier to test and run locally.