Good support for continuous integration (CI) has become table stakes for online code hosting. Continuous testing and monitoring of your code can help raise and maintain code quality. However, even when you have access, configuring CI well takes effort. The goal of this post is to present a starting configuration suitable for Go projects hosted on GitLab.
The configuration below should not necessarily be taken as complete. The project used here as an example is very, very simple, and so many complexities that larger projects face will be avoided, but it hopefully will serve as a good introduction.
For this introduction, I’m assuming a basic understanding that GitLab arranges the CI as a “pipeline”, which is composed of several “jobs”. Those jobs are run stages. Although the names of the stages is configurable, the standard stages are: build, test, and deploy.
Before discussing the jobs in the build stage, my standard CI configuration contains a prefix to create a standard script executed at the start of every job.
This fragment creates a default
before_script, which will be set on every job in the pipeline. Each job can override this value, but the above is a good default. In this case, the only goal is to write some telemetry to the log. In principle, all of this information is known from the docker images run (see later), but docker images are not immutable, and this information can be useful. Additionally, note the test that you can run a command
go. Not every job will run using a docker image with Go installed, and omitting the test will cause those jobs to fail.
Next, we create a job for every version of Go that is supported by the project. This is somewhat complicated because of the introduction of modules. In practice, this means that the jobs take two different forms.
When using modules, the job definition is very simple:
The above defines a job call
golang-1.14. The docker image is
golang:1.14. The jobs will run during the build stage. Finally, the script sets the actions to be run. In the script, you can see that two commands will be executed, and the package will be built and tested. As you may have guessed, this job will build and test the package using version 1.14. With Go modules, building and testing is straightforward.
Similar jobs, except with versions 1.11 through 1.13 can be added.
When using older versions of Go, the package needs to be copied into the correct folder in the
GOPATH. There are a couple of ways of arranging this, but the simplest approach is to configure the jobs so that the repository is cloned into the correct folder.
The key is to use the
variables key to set certain environment variables. These environmental variables can control many aspect of the job, including the script. All of the build actions are supposed to occur in or below the directory specified by
$CI_BUILDS_DIR, which is predefined by the GitLab runner. We therefore use this as the
GOPATH, and use
GIT_CLONE_PATH to set the location for cloning the repository to match Go’s expectations.
Similar jobs, except with versions 1.9 and below can be added.
The jobs above are good for leaf packages (i.e. packages that rely only on the standard library). If the package is importing third party packages, then it might be worthwhile to configure caching. An example of caching is shown in a later job. Caching downloads might speed up the builds.
If building the package takes a long time, it might be worthwhile to keep certain files or directories as artifacts. Artifacts are kept after the job has been completed, and can be copied into later jobs. Again, and example of using artifacts is shown in a later job.
Because the testing was very quick, the package has already been tested many times by the jobs in the build stage. Still, there is additional testing to be done, and that testing will be done in the test stage. There are 5 jobs.
This job is used to run all of the code quality tests that are required, and which will cause the pipeline to fail. In other words, this job is for those tests that should block acceptance of any commits.
Because we are downloading third-party packages, this job is configured to use a cache. There are limitations on the location of cached files, so the
before_script is adjusted to move cached files to the correct location in the
GOPATH, and adding an
after_script. Note the use of YAML anchors to reuse the existing
Again, the package is tested using
go test. However, this time we capture a cover profile, and generate an HTML report. That HTML report is listed as an artifact. The pipeline will use it in a later job.
In this particular case, you can see that the job also runs various code analysis tools. In particular,
gosec. These are chosen mostly because they are the metrics used by Go Report Card. The exact list will obviously change between projects depending on their policy, but failure of any of these tools will cause the job to fail, and so cause the pipeline to fail.
All of the tools used above are included in GolangCI-Lint, so the above job could be simplified by simply installing that tool, and using it to run the checks. A further improvement would be to put that tool in a cache shared with the next job.
Often, there are additional code quality metrics that a project wants to track. Unlike the previous job, failing to meet these criteria are not supposed to cause the pipeline to fail. We therefore configure a job where… failure is allowed. Note the last key in the job definition.
In this particular case, GolangCI-Lint is installed, and the job runs all the linters via
--enable-all. Reviewing the build log will show any possible issued detected by the large list of linters enabled, but findings will not cause the pipeline to fail.
Take care when enabling all checks from GolangCI-Lint. The list of checks can change, so either makesure that the version is pinned, or that the tool is only allowed to run in a job that can fail, such as our dedicated lint job.
Most obviously, you should consider adding a configuration file for the linter into your repository to adjust the linters used.
The download of the linter on every run is perhaps wasteful, and could be improved by caching the executable. However, care would be need to invalidate the cache when adjusting the version.
Finally in the test stage, we configure a number of jobs provided by GitLab:
From the key name,
include, it should be clear that these lines pull in definitions from other YAML files. In particular, the three files configure 3 additional jobs.
The SAST jobs is a security scanner. For Go, the security scanner is
gosec. This is one of the tools that we included in the
test job. Including it again serves two purposes. First, this job will populate the Security Dashboard for the project. Additionally, some projects will want to disable some rules used by
gosec. That is acceptable, but this job will keep a record of all issues detected.
The two remaining jobs depend on detecting that Go modules are being used. They will not work if your project only works with GOPATH. Be sure to include both
go.sum in your repository. These jobs will also populate dashboards for the project, and review the project’s dependencies for conflicting licenses or security issues. For a simple project without any dependencies, these jobs admittedly have very little to do, but are present if the project grows.
For a package, you might think there is nothing to deploy. Certainly, there is no application or service to deploy. However, we do have our HTML report on code coverage, which should be published.
For GitLab, a job called
pages is treated specially. Artifacts from this jobs will be used to create a static website. By specifying a dependency on the
test job, the artifacts from that job will be present for the
pages job. Remember that we only had one artifact, which was the HTML report. Note that the script can access and copy
cover.html that was generated in the earlier job.
For an application, the deploy stage can also include a job to build all of the executables. Especially with Go, the job can cross-compile executables for all of the desired platforms.
Even for a very simple project, a complete CI configuration for GitLab is 13 jobs. That number can certainly be lowered by testing against fewer versions of Go, but it can also easily increase.