Jenkins Declarative Pipeline
The Jenkinsfile
(or rather, declarative pipelines in general) has been one of the best improvements to CI/CD in the past few years. I love Jenkins,
but for whatever reason, I always struggle to do anything with its pipelines beyond the most basic use-case.
I've spent way too long putting together my Jenkinsfile
for ubsub.io, so I thought I'd share it, so that there is another
example out there for someone to look at. I've obfuscated some bits of it, but it will still be a valuable example.
I'll make one last note before we dive into the code: The way I currently have ubsub's source set up is not a best-practice. Everything is lumped together under the same source tree, and I may split it up at some point, but this is working for now. If you're with a team or large group, I'd highly recommend breaking up your different pieces into different repos rather than copying what I've done.
The Example
I've added a bunch of inline comments below. One thing to call out is my primary use-case for this builds many different components in a single pipeline, some of which require different languages and platforms. This is reflected in my stages and sub-stages below.
Jenkinsfile
pipeline {
agent {
// My docker "agent" is on a VM because the host system isn't a new enough kernel to support docker
label 'docker'
}
environment {
// Some environment variables to be used later
ARTIFACTPATH = "output"
OUTPUT = "bundle.tar.gz"
}
stages {
// Just a small stage to notify bitbucket that we're under way
stage('Notify') {
steps {
bitbucketStatusNotify 'INPROGRESS'
}
}
// We could parallelize it, but I've chosen not to, mostly due to resource restrictions
// The first build-pass will be a golang build environment
stage('Docker:Go') {
agent {
// Use golang
docker {
image 'golang:1.9.2'
// Use the same node as the rest of the build
reuseNode true
// Do go-platform stuff and put my app into the right directory
args '-v $WORKSPACE/components/udpgo:/go/src/udpgo'
}
}
steps {
// While not technically necessary to have the "script" section here
// it is more consistent with what I do below
script {
// You could split this up into multiple stages if you wanted to
stage('Compile:UDP') {
sh 'cd $GOPATH/src/udpgo && go get -t'
sh 'cd $GOPATH/src/udpgo && go test'
sh 'cd $GOPATH/src/udpgo && go build'
}
}
}
}
stage('Docker:Node') {
agent {
// This dockerfile is built from a custom `Dockerfile` in the directory `build/` (See below for its contents)
dockerfile {
dir 'build/'
// Use the same node as the rest of the build
reuseNode true
}
}
steps {
// Now, all the stages for this node
script {
stage('Node:Setup') {
sh 'npm install'
sh 'pip install --user -r requirements.txt'
}
stage('Install') {
sh 'npm install'
}
stage('Lint') {
try {
sh 'eslint --quiet -f junit -o test-results.xml .'
} finally {
junit allowEmptyResults: true, testResults: '**/test-results.xml'
}
}
stage('UI') {
try {
sh 'npm run pm2:start:router'
sleep 10
// Currently jenkins doesn't support `dir` in docker, so I cd before each command
sh 'cd ui && npm run gulp'
sh 'cd ui && npm run webpack:prod'
} finally {
sh 'npm run pm2:stop'
}
}
stage('Router Tests') {
sh 'cd router && npm run test'
}
stage('Integration Tests') {
try {
sh 'npm run pm2:start:router && npm run pm2:start:jsvm'
sleep 10 // Wait for things to start
sh 'npm run test'
} finally {
sh 'npm run pm2:stop'
}
}
stage('Prune') {
// Prune my modules before sending it off to prod
sh './each.sh npm prune --production'
}
}
}
}
stage('Bundle') {
steps {
// Archive everything to be sent to the server later
sh 'rm -rf $ARTIFACTPATH'
sh 'mkdir -p $ARTIFACTPATH'
// Use `.bundleignore` as a file similar to .gitignore so different components can exclude pieces from being sent to prod
sh 'tar czf $ARTIFACTPATH/$OUTPUT --exclude $ARTIFACTPATH --exclude-ignore .bundleignore .'
archiveArtifacts "${env.ARTIFACTPATH}/*"
}
}
}
post {
// Finally let bitbucket know
success {
bitbucketStatusNotify 'SUCCESSFUL'
}
failure {
bitbucketStatusNotify 'FAILED'
}
always {
// And cleanup
deleteDir()
}
}
}