Continuous integration with Go and GitHub Actions
In this post we're going to walk through how to use GitHub Actions to create a continuous integration (CI) pipeline that automatically tests, vets and lints your Go code.
For solo projects I usually create a pre-commit Git hook to carry out these kinds of checks, but for team projects or open-source work — where you don't have control over everyone's development environment — using a CI workflow is a great way to flag up potential problems and help catch bugs before they make it into production or a versioned release.
And if you're already using GitHub to host your repository, it's nice and easy to use their built-in functionality to do this without any need for additional third-party tools or services.
To demonstrate how it works, let's run through a step-by-step example.
If you'd like to follow along, please create a new repository and clone it to your local machine. For the purpose of this post I'm going to use the private repository alexedwards/example
.
Then let's scaffold a simple Go application along with a (failing) test like so:
If you run this application it should compile correctly and print "Hi Alice"
, but executing go test .
will result in a failure. Similar to this:
Creating a workflow file
The next thing that we want to do is create a workflow file which describes what we want to do in our CI checks, and when we want them to run. By convention this file should be stored in a .github/workflow
directory in the root of your repository and should be in YAML format.
Let's create this directory along with an audit.yml
workflow file.
There's an excellent introduction to the workflow file syntax here, and there's also a collection of templates for different languages and frameworks that you can use as a starting point.
But for now, let's jump in and update the workflow file so that it looks like this:
Let's quickly step through this and explain what the different parts of the file do.
- First we use the
on
keyword to define when we want the workflow to run. In this case, I've configured the workflow so that it runs when a new commit is made to themain
branch, or a pull request is submitted. - Then we use the
jobs
keyword to define a list of the jobs that are to be run. At the moment our workflow only contains one job calledaudit
, but you can specify multiple jobs if you want and (by default) they will be executed in parallel. - An independent runner will be spun up for each job. This is essentially a virtual machine that will execute the
steps
for the job. In the file above we use theruns-on
keyword to specify that we want the runner to use Ubuntu 20.04 as a base OS, but others operating systems are available. It's also worth noting that the runner has a lot of useful software and tooling pre-installed. - In the first step for our
audit
job we use theuses
keyword to execute the community actionactions/checkout@v2
. This action will checkout our project repository to the runner so that the following steps access the code. - Then we use the
actions/setup-go@v2
action to install Go version 1.17 on the runner. - Once that's done, in the remaining steps we use the
run
keyword to execute specific commands on the runner. In this case we build our code and then audit it using the standardgo build|vet|test
commands and the additionalgolint
andstaticcheck
tools.
Now that's in place, let's commit everything and push the changes to your repository:
Once the push has completed, head to your repository and select the Actions tab. You should see that the CI 'Audit' workflow is running, similar to the screenshot below.
You can click through on the workflow name to see more details while it's running, and after a minute or two you should see that the workflow is terminated due to our failing test.
Additionally, as the owner of the repository, you should also get an email notification to tell you that the workflow failed, and everyone who browses the repository will see a red cross symbol next to the commit in the Git history.
Fixing the code
Let's fix our codebase by updating the sayHello()
function to return the correct output, like so:
If you want, you can commit this change and push it…
… and you should see that the 'Audit' job in our workflow file now completes successfully and everything has a nice green check mark next to it.
Great! That's working really well and, from now on, any time someone makes a push or pull request to the main
branch, the tests and vetting and linter checks will be automatically run.
From here, you can extend the workflow to carry out more checks or send additional notifications if you want to — or even expand it to act as a continuous deployment (CD) pipeline that builds and deploys your binaries. To give you some ideas, here are a couple of slightly more complicated workflows from my own projects: