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: anagent,stageswithsteps, andpostactions. It's validated before the build starts and is the recommended style. sh(orbat/powershellon Windows agents) runs shell commands.withCredentialsexposes 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
sortwith a comparator closure) historically misbehaved. Keep pipeline logic simple and move complex code into@NonCPSmethods 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-linterCLI 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.