CI your Go with GitHub actions
GitHub releases actions relatively recently (end of year 2019) to be able to automate, customize, and execute your software development workflows right in your repository.
A well detailed documentation, some guides and a marketplace will help you to create workflows you need, or same develop some actions.
But before all, let’s begin by a few reminders about what is needed to test and build a Go application.
Testing and compiling source code in Go
Most Go tools are available with command line go <command> [arguments]
Documentation: https://golang.org/doc/cmd
In our case, the most interesting commands are:
- mod is used to manage module dependencies
https://golang.org/ref/mod - vet examines Go source code and reports suspicious constructs
https://pkg.go.dev/cmd/vet - test automates testing and benchmarking on the packages named by the import paths
https://pkg.go.dev/cmd/go/internal/test - build compiles the packages named by the import paths
https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies
NB: You can see in documentation that the go test
command implicitly call the go vet
command (can be disabled with -vet=off
argument).
NB: The go vet
command uses GCC (GNU Compiler Collection) by default, but it can be used without having GCC installed by setting environment variable (CGO_ENABLED=0
).
cgo
Be careful, cgo (enabled by default) is used for some functionnality:
- it enables the creation of Go packages that call C code
https://pkg.go.dev/cmd/cgo - it changes the behaviour of Domain Name resolution
https://pkg.go.dev/net#hdr-Name_Resolution - it is used for native builds on systems where you build on, or for cross-compilation
- …
Ok, now it’s time to implement it ! Silence ! Actions !!
Scene 1: Demo Time
From actions availables on GitHub marketplace, you can create a minimalist workflow.
NB: On GitHub hosted runners, GCC is installed by default like other tools (see documentation on runners here).
The step “See built file information” is only there to have some information about the generated binary 😉.
It is pretty cool, we have our first binary 😁. We can notice that our generated binary is linked to some system libraries.
But we can also create a little more detailed workflow that follows step by step everything that is done by the previous one.
So now we have a binary file, let’s try it on some Linux distribution, the step “Make the built file downloadable” permits to download binary file as an artifact.
Scene 2: Test time
In most of cases, binary are used in Docker image with small Linux system distribution like the debian buster slim, or alpine, … So to test in the same condition, we can use Docker command with a mounted volume to have access to our binary and run it for the Linux system.
Try with Debian slim
docker run --rm -v <your_local_folder_containing_my_app>:/app -ti debian:buster-slim /bin/bash
And then run it.
/app/my_app
No error occurs, the binary works well. Nice 😎!
Try with Alpine
docker run --rm -v <your_local_folder_containing_my_app>:/app -ti alpine:3.14 /bin/sh
And then run it.
/app/my_app
This time an error occurs 😱. The problem is that our binary use glibc, but alpine distribution uses musl libc, so he can not work like that.
Many solutions come to us:
- to have glibc installed
- use cross-compilation to avoid the link with glibc
- build our binary on an alpine Linux distribution
Scene 3: Solution time
Solution 1: glibc installed
apk add --no-cache libc6-compat
/app/my_app
This time, no error occurs. The goal to use alpine distribution is to have a simple, small and secure Linux distribution, with all the minimal requirement. The additional installation of compatible libraries for glibc increases the overall size, and may result in loss of efficiency and security.
Solution 2: cross-compilation
To cross-compile, you must set environment variable CGO_ENABLED=0
. In the step “See built file information”, we continue on error because the command ldd my_app
generates an error saying that the file is not a dynamic executable and it is … normal 😋.
No more errors. This solution works fine, but you can no longer use the C libraries, nor the cgo-based resolver that calls C library routines such as getaddrinfo and getnameinfo, and others things…
Solution 3: Build for the expected Linux system
The step “See built file information” show us that the generated binary is well linked with musl libc.
And the binary runs without error 😎.
The end
As we have seen, several solutions are available to us to compile a Go binary according to what we want or the conditions. Keep in mind that in Go, the binary will be dependent on the target system on which you want to run it, and in this case it is better to build it on this same system.
I hope you enjoyed this article, but above all that it will be useful to you😊. Feel free to give claps if the article helped you! 😻