Building a CI system for Go, with Jenkins
Before continuing, why Checkout Stage ?
Well, just because we’re triggering the build whenever a change is pushed to BitBucket, Jenkins is smart enough to clone the branch that changed onto a predefined path (typically JENKINS_HOME/project-name). We don’t want that, we want the repository to be cloned inside our new workspace. It would be possible to instruct jenkins with a relative path on the job configuration, but it’s preferred to make the pipeline as portable as possible, so we will just add checkout scm
Copying files from that predetermined path to our workspace is a TERRIBLE idea! Concurrent builds break and it’s a hack, rather than a proper solution.
This gives me the opportunity to state some important stuff about the project structure that this CI pipeline is aimed at : Although you can change the GOPATH to whatever subdirectory you want, you may face some problems with relative imports of your project. This pipeline suits a project that sits on one repository as a whole, not by having multiple subpackages as different repositories — typically to be used as libraries. Depending on what you want to achieve, you are free to edit GOPATH as you wish, add more parameters to the commands, as well as perform any directory changes, or changing the way dependencies are managed. Most problems should be able to be tackled by a few shell-fu commands.
Long story short, your favorite butler Jenkins is going to clone your repository on the new workspace and it’s your business to move it around and set an appropriate GOPATH in order for the upcoming commands to work.
Pre Test
In this part, all the dependencies are pulled. I am using the new dep tool (and you should as well). Again, the code speaks for itself — notice that I also add GOPATH/bin to the PATH variable, in order to be able to use the downloaded binaries further down the road:
(Yes, I follow the convention of having the starting point of my programs under GOPATH/src/cmd/ , just in case I want to use anything as a library.)
Notice the last line: dep ensure (as well as the commands you are going to see below, at the testing stage) does not accept parameters as path targets so we have to cd
into the desired directory and just perform the command from there.
Testing
Testing, Linting, Vetting and Race Condition Detection take place.
If any of these sh commands exits with a non-zero value, the build will abort/fail. That means that you can use go test without the need to export the test results to Jenkins. If you do want to have a view on these test results though, you can pipe the test output to go2xunit , which I have included in the pre test phase. That tool can export xUnit xml test reports, which is a format that Jenkins supports.
You should avoid testing anything inside the vendor directory. That’s why instead of just doing go test
, we do go test $(go list ./.. | grep -v /vendor/)
. In this particular example, because my project is self-contained and I check-in the vendor directory in the repository (in order to make dep ensure faster — if I ever want to update, I can change the Jenkinsfile to run it with the -update flag), I want to avoid linting, testing and vetting anything that’s not mine. That’s how I came up with this command: (Remember, I use BitBucket) go list ./.. | grep -v /vendor/ | grep -v github.com | grep -v golang.org
. This will effectively list all the directories with a path relative to $GOPATH/src that your project contains, without golang stuff, or libraries pulled from github. You can mess around further by chaining greps or using other options (perhaps to allow particular github projects/libs getting tested). You can tell that I redirect the output to a file, in order to print the file relative to just $GOPATH, which is the place where we are going to run the testing, linting commands from. Correct paths are stored in a variable, for productivity, reusability and readability’s sake.
Then, I proceed into printing each line of the file, prefixed with ./src/
in order to have the commands run on the correct files. One more thing to notice would be the triple-double quotes sh """content"""
that got used in order to make Jenkins escape, and not try to parse the contents of the command, due to weird symbols.
Building
Basically build the thing. You can build for any platform you want. I took the extra mile and added -s as a flag, which strips off the debugging symbols. This is some extra source code protection, because our repository is private. This is going to be super fast because necessary files are already produced by running go test
!
Actually building is only needed for the next phase (publishing binaries). That’s because if something was to fail at compile-time, tests would not be able to run.
Publishing to BitBucket Downloads
Locate the produced executables and push to BitBucket via a POST request (yes, we still use curl). You can produce archives of your builds, you can build for different platforms and also choose which branches should be built/uploaded. For example, you might only want to publish your release and/or snapshot branches. We’ll just find out how to get the branch name & commit hash, tarball the project and shoot it up to BitBucket Downloads with a simple POST request, performed by cURL (Authentication either with user:pass or key). The fail
flag makes curl exit with a non-zero exit code if the response code isn’t proper (200).
I chose BitBucket Downloads but you can upload to your own website via ftp/ssh. This would also be the stage of publishing the produced docker image, if you are using Docker.