Post

Jenkins Declarative Pipeline — Coming From an Ant/Maven Background

Jenkins declarative pipelines explained for someone who built Jenkins jobs using Ant scripts and Maven — not Groovy.

Jenkins Declarative Pipeline — Coming From an Ant/Maven Background

I came to Jenkins declarative pipelines from a very specific background: Ant scripts. Everything I knew about Jenkins automation was written in XML. My build steps were Maven commands, my deploy steps were Ant sshexec tasks that SSH’d into remote servers, and my Jenkinsfiles were written by people who apparently loved Groovy.

When I finally had to write one myself, the thing that helped most was realising that the declarative format is less like code and more like config. It’s not Groovy in the way that Groovy scripted pipelines are Groovy. You’re mostly filling in named blocks — which agent to use, which stages to run, what commands to execute in each stage. The commands inside the sh step are just shell commands, which I already knew.

Basic structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pipeline {
    agent any

    tools {
        maven 'Maven 3.8'
        jdk   'JDK 11'
    }

    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit '**/target/surefire-reports/*.xml'
                }
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh 'ant -f build.xml deploy'
            }
        }
    }
}

The tools block tells Jenkins which Maven and JDK installation to use — you configure those named installations under Manage Jenkins → Global Tool Configuration. Then sh 'mvn clean package' is just that Maven command. Nothing Groovy about it.

The post { always { junit ... } } part publishes test results to the Jenkins build — it’s how you get the test trend graph on the job page.

Running Ant scripts from pipeline stages

If you already have Ant build scripts (and I had plenty of them), you don’t throw them away — you just call them:

1
2
3
4
5
stage('Deploy') {
    steps {
        sh 'ant -f build.xml deploy'
    }
}

Or if you have environment-specific targets:

1
2
3
4
5
stage('Deploy to Dev') {
    steps {
        sh 'ant -f build.xml -Denv=dev deploy-and-restart'
    }
}

Same Ant command you’d run from the terminal. The pipeline is just orchestrating when it runs and in which order.

The when directive

Controls whether a stage executes at all. The simple cases are just config:

1
2
// Only deploy when building the main branch
when { branch 'main' }
1
2
3
4
5
6
7
// Only deploy when building specific branches
when {
    anyOf {
        branch 'main'
        branch 'release'
    }
}

These are not Groovy expressions — they’re keywords the declarative syntax understands. You don’t need to know what a closure is to use them.

Handling credentials

Passwords and SSH keys go in the Jenkins credentials store, not in the Jenkinsfile. The Jenkinsfile just references them by ID:

1
2
3
4
5
6
7
8
9
steps {
    withCredentials([usernamePassword(
        credentialsId: 'nexus-deploy-user',
        usernameVariable: 'NEXUS_USER',
        passwordVariable: 'NEXUS_PASS'
    )]) {
        sh 'mvn deploy -Dnexus.username=$NEXUS_USER -Dnexus.password=$NEXUS_PASS'
    }
}

For SSH — the same thing I used to do with Ant sshexec, but from the pipeline:

1
2
3
4
5
steps {
    sshagent(['prod-deploy-key']) {
        sh 'ssh deploy@appserver1 ". .bash_profile; ./server.sh restart;"'
    }
}

The sshagent block loads the SSH key into an agent for the duration of those steps. Inside sh, the SSH commands are the same as what I had in the Ant scripts.

Deploying to multiple servers

My old Ant scripts would loop through a server list. In a Jenkinsfile you can do it explicitly, or in a loop, but the straightforward version is just staging the steps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
stage('Deploy to Production') {
    when { branch 'main' }
    steps {
        sshagent(['prod-deploy-key']) {
            sh '''
                scp target/app.war deploy@server1:/opt/deployments/app.war
                ssh deploy@server1 ". .bash_profile; server stop;"
                ssh deploy@server1 ". .bash_profile; server start;"

                scp target/app.war deploy@server2:/opt/deployments/app.war
                ssh deploy@server2 ". .bash_profile; server stop;"
                ssh deploy@server2 ". .bash_profile; server start;"
            '''
        }
    }
}

The triple-quoted ''' shell block lets you write multi-line shell commands without backslash-escaping everything. The SSH commands inside are identical to what’s in the Ant sshexec task.

Environment variables

Define them at the top of the pipeline so every stage can see them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pipeline {
    agent any

    environment {
        APP_VERSION = '2.1.0'
        DEPLOY_PATH = '/opt/deployments'
    }

    stages {
        stage('Deploy') {
            steps {
                sh 'scp target/app.war deploy@server:$DEPLOY_PATH/app.war'
            }
        }
    }
}

Jenkins also gives you some built-in variables for free:

  • env.BUILD_NUMBER — the build number (useful for artifact naming)
  • env.BRANCH_NAME — the branch being built
  • env.BUILD_URL — link to the build (useful in notification emails)

Notifications on failure

1
2
3
4
5
6
7
post {
    failure {
        mail to: '[email protected]',
             subject: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
             body: "See: ${env.BUILD_URL}"
    }
}

This goes at the same level as stages, at the bottom of the pipeline block. post sections can go inside individual stages too, if you want stage-specific behaviour on pass/fail.

What made this click for me

The mental shift was treating the Jenkinsfile as configuration rather than code. The build commands are Maven, the deploy commands are Ant or SSH scripts I already had — the pipeline is just the glue that sequences them, handles credentials, and decides which branch triggers which stage.

The Groovy scripted pipeline syntax is the alternative where you write actual Groovy code and have full programmatic control. I never needed that. Everything I wanted to do was possible in the declarative format, and it’s much easier to read three months later.

This post is licensed under CC BY 4.0 by the author.