Thursday, 13 December 2018

Nightly build steps with Jenkins declarative pipeline

Jenkins declarative pipeline is a new(ish) pipeline syntax for Jenkins that allows you to write pipeline-as-code in a Jeninsfile. Unlike a traditional pipeline, instead of writing Groovy code, a declarative pipeline uses a custom DSL. A simple example that builds a C++ application is below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pipeline {
    agent any
    stages {
        stage('Clean Old Builds') {
            steps {
                sh 'rm -fr _builds'
            }
        }
 
        stage('Build') {
            sh 'cmake -H. -B_builds/default'
            sh 'cmake --build _builds/default'
        }
 
        stage('Test')
            sh 'cmake --build _builds/default --target test'
        }
    }
}

The above example, will have 3 stages that can run on any build machine.

  • Stage 1, Clean Old Builds, will remove any old builds.
  • Stage 2, Build, will run cmake and make.
  • Stage 3, Test, will run any unit tests.

This can be cleaner and easier to read than the older Groovy based pipeline syntax.

Nightly Builds

In a previous post, I discussed how to run nightly builds using Jenkins Groovy pipelines. I tried to use the same isJobStartedByTimer function with declarative pipeline, as follows,

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
def isTimerBuild = isJobStartedByTimer()
pipeline {
...
    triggers {
        cron(env.BRANCH_NAME == 'master' ? 'H H(0-2) * * *' : '')
    }
    stages {
        ...
        stage('Nightly') {
            when {
                expression {
                    isTimerBuild == true
                }
            }
            steps {
                sh 'cmake --build _builds/default --target cppcheck
            }
        }
    }
}
// define isJobStartedByTimer function as before
@NonCPS
def isStartedByTimer() {
  ....
}

Unfortunately this failed because of an unauthorized error. I was unable to add the required values to the In Process Script Approval as they never appeared in the approval window.

Improved Method

While looking for a solution to the above problem I came across the following feature request in Jenkins. This was added in the pipeline workflow api plugin in v2.22. The core of this change is that it allows access to the build causes via the currentBuild class, instead of the sandboxed currentBuild.rawBuild class.

1
2
currentBuild.getBuildCauses()
currentBuild.getBuildCauses(String superClassName)

When using these functions you do not have to enable any In Script approvals. To get this to work I updated my isJobStartedByTimer as follows:

1
2
3
4
5
6
7
8
9
10
11
12
@NonCPS
def isJobStartedByTimer() {
    try {
        if(currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause').size() > 0) {
            echo "build started by timer"
            return true
        }
    } catch(theError) {
        echo "Error getting build cause: ${theError}"
    }
    return false
}

Using this updated function, and the example pipeline from earlier, allows me to have build steps that only run at night.

Alternative Improved Method

In some cases I found that the above improved method caused a ClassNotFoundException saying that the TimerTrigger class did not exist. An alternative example, that could be used if you have receive this error, is to use the currentBuild.getBuildCauses() version of the function. This works as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@NonCPS
def isJobStartedByTimer() {
    try {
        for ( buildCause in currentBuild.getBuildCauses() ) {
            if (buildCause != null) {
                if (buildCause.shortDescription.contains("Started by timer")) {
                    echo "build started by timer"
                    return true
                }
            }
        }
    } catch(theError) {
        echo "Error getting build cause: ${theError}"
    }
    return false
}

No comments:

Post a Comment