CircleCI is great for running tests on every git commit. It integrates with GitHub, and marks each commit with either a ✓ or ✗ to indicate if the tests passed. It's not configured for go
out of the box, but with a little scripting it works.
Continuous Integration (CI) is used for Continuous Deployment (CD). Each time you push to the git master branch, if the tests pass, that commit is pushed live to production.
When you want to add a new feature, checkout a new branch, push commits, and then open a pull request. Hosting providers like Heroku allow review apps which are separate app deployments automatically created on each pull request and given a unique subdomain. If you merge the new feature or bug fix into the master branch, tests run, and it is automatically deployed.
The benefits of CI/CD is two-fold. First, you always have the latest version deployed. Second, you need excellent test coverage because automated tests are the only gate before pushing live.
There are a number of cloud services that offer CI/CD. Here are the ones I know about that have low cost or free plans.
I chose CircleCI for the configuration flexibility and excellent experience. It makes a difference having the build status in the favicon, the commands piped via websocket in realtime, and the result synced to GitHub.
The first step is to check you have the right go
version. I use go1.6.2
which comes with the ubuntu trusty image on Circle. You can change the machine image in the settings for the project. New projects should default to trusty.
If you need a specific go
version, you can download and install go in the machine.pre
section of the circle.yml
config.
Speaking of which, circle.yml
lets you configure the entire process. Here's the minimal go
config to get the tests running and (hopefully) passing. I use govendor
for package management, so you may need to change those lines depending on your project.
# circle.yml
machine:
environment:
GOPATH: /home/ubuntu/go
PATH: '/usr/local/go/bin:/home/ubuntu/go/bin:$PATH'
ROOTPATH: /home/ubuntu/go/src/{source-code-path}
TEST_DATABASE_NAME: testing
checkout:
post:
- mkdir -p $ROOTPATH
- cp -r "/home/ubuntu/$(basename $ROOTPATH)" $(dirname $ROOTPATH)
dependencies:
pre:
- go get github.com/kardianos/govendor
override:
- cd $ROOTPATH && govendor sync
test:
override:
- cd $ROOTPATH && govendor test +local
I chose to put the entire go
file structure into /home/ubuntu/go
. It needs to be somewhere we have privileges, and the /home/ubuntu
is the home directory.
GOPATH: /home/ubuntu/go
I want to use command line tools installed with go get
so I added the go binary command and the go bin folder to the system path.
PATH: '/usr/local/go/bin:/home/ubuntu/go/bin:$PATH'
I haven't found a way to change the working directory, so instead I set ROOTPATH
to the project folder and change into it for each go-related command. In the checkout step, I copy the source code from the default folder to where the go command expects it to be.
ROOTPATH: /home/ubuntu/go/src/namespace/project
- mkdir -p $ROOTPATH
- cp -r "/home/ubuntu/project" $(dirname $ROOTPATH)
- cd $ROOTPATH && {run-command}
You need to replace {source-code-path}
with the path to your code, often github.com/{org}/{repo}
. Circle automatically fetches the git repository into /home/ubuntu/{project}
. You can verify all this by adding commands to see what's going on.
checkout:
pre:
- echo $PWD
- ls
- ls /home/ubuntu
Circle parses jUnit XML files which give more information about which tests passed or failed, and how long each test took. To enable granular results, pipe the go test
output into the correct format.
# circle.yml
# Add dependency for reporting.
dependencies:
pre:
- go get github.com/jstemmer/go-junit-report
# Pipe output to the special reports folder.
test:
pre:
- mkdir -p $CIRCLE_TEST_REPORTS/go
override:
- cd $ROOTPATH && govendor test +local -v | go-junit-report > $CIRCLE_TEST_REPORTS/go/junit.xml
After the tests fun, Circle checks the reports folder to see if we left any jUnit XML files. The folder go
is an arbitrary name. If you had multiple languages or tests to run, you would use a different folder name per environment.
/$CIRCLE_TEST_REPORTS
/go
/junit.xml
/phantomjs
/junit.xml
The test command itself uses both the govendor
flag +local
and the go
flag -v
. The govendor
command uses flags starting with +
and passes flags starting with -
so the command expands to:
govendor +local
go test -v {package 1}
go test -v {package 2}
...
The go
reporter command line program go-junit-report
reads from stdin and writes to stdout which means I can pipe the test output through the reporter to the output file.
{test-command} | go-junit-report > {output-file}
Instead of go
test output:
=== RUN TestSignAndParse
--- PASS: TestSignAndParse (0.00s)
PASS
ok namespace/project/jwt 0.016s
We get machine output:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite tests="1" failures="0" time="0.023" name="bridgerock/s1/jwt">
<properties>
<property name="go.version" value="go1.6.2"></property>
</properties>
<testcase classname="jwt" name="TestSignAndParse" time="0.000"></testcase>
</testsuite>
</testsuites>
Circle will save the results and report which tests took the longest right in the web dashboard.
Circle comes with databases already installed. Keep in mind if you pipe output to the reporter, every other command needs to write to stderr with 1>&2
.
# bin/test
set -e -o pipefail
# Set needed env vars.
export ENVIRONMENT=test
export DATABASE_URL="postgres://localhost/$TEST_DATABASE_NAME?sslmode=disable"
# Run all migrations.
bin/migrate down 1>&2
bin/migrate up 1>&2
# Pass command line args to test command.
govendor test +local "$@"
# bin/circle
set -e -o pipefail
# Create a new database.
psql -d postgres -c "CREATE DATABASE $TEST_DATABASE_NAME WITH ENCODING 'UTF8';" 1>&2
bin/test -v | go-junit-report > $CIRCLE_TEST_REPORTS/go/junit.xml
Thanks for reading!