Build Go projects with Github Actions
Feb 11, 2020 UPDATE: Post has been updated to incorporate Github Actions YAML syntax
Github Actions is the hot new platform Github introduced in late 2018 that aims to be a generic workflow automation tool. Indeed an interesting move by Github, a company that stayed devoted to improvement of collaboration tools for developers, to expand into CI/CD space. Being a Github user for quite some time (10 years!) i got really excited about mysterious Actions project, which rolled out in Beta only.
Overview
Before we dig into Actions and Workflows, lets undestand why this thing even exist in the first place. To get any sort of automation for the repo, like running tests or creating artifacts (packages, binaries, etc), you'd have to use an external service or roll your own. Basically, your app would receive events from Github using webhooks and perform certain tasks. With Github Actions you could set up all these tasks without any third-party tools, all powered by Docker containers under the hood. Actions could also be shared using public Docker images or as part of a git repo.
Actions are not just simple tasks, they are part of the workflows that define the execution order and flow. One of the common use cases for Actions would be some kind of push -> analyze -> lint -> build -> release process, or sending notifications to third-party services. In case of this blog post - we want to cross compile Go binaries for all operating systems using a single action, and upload the artifacts to S3.
Workflow Setup
For the sake of simplicity we'll use a dummy Go project with a single file like this:
package main
import(
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
Save it under your GOPATH, like ~/go/src/github.com/username/dummy/main.go
.
That way we can easily run go build
and go install
. In the end we'll have a binary
that just prints out Hello World!
, nothing else.
Next, we'd want to cross compile binaries for OSX/Linux/Windows (386/x64). Pretty straightforward process (on OSX/Linux at least) and could be done with a command:
GOOS=linux GOARCH=amd64 go build -o dummy_linux_amd64
Let's setup a Github Workflow file .github/workflows/main.yml
. Our new worflow will
only include a single task for cross-compilation:
name: Build Go binaries
on:
push:
# We want to run the workflow on all branches.
# But you can restrict the runs if necessary.
branches:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Make binaries
uses: sosedoff/actions/golang-build@master
# Uncomment to following piece to restrict targets
# with:
# args: linux/amd64 darwin/amd64
When you commit and push out your local changes, Github will pick up the workflow
and trigger a new run using the action repository specified with uses
keyword.
Action Script
I've created an action that uses the standard golang Docker image with a few tweaks:
FROM golang:1.13
RUN \
apt-get update && \
apt-get install -y ca-certificates openssl zip && \
update-ca-certificates && \
rm -rf /var/lib/apt
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
As for the actual entrypoint.sh
script that cross-compiles the binaries, let's have a look:
#!/bin/bash
set -e
if [[ -z "$GITHUB_WORKSPACE" ]]; then
echo "Set the GITHUB_WORKSPACE env variable."
exit 1
fi
if [[ -z "$GITHUB_REPOSITORY" ]]; then
echo "Set the GITHUB_REPOSITORY env variable."
exit 1
fi
root_path="/go/src/github.com/$GITHUB_REPOSITORY"
release_path="$GITHUB_WORKSPACE/.release"
repo_name="$(echo $GITHUB_REPOSITORY | cut -d '/' -f2)"
targets=${@-"darwin/amd64 darwin/386 linux/amd64 linux/386 windows/amd64 windows/386"}
echo "----> Setting up Go repository"
mkdir -p $release_path
mkdir -p $root_path
cp -a $GITHUB_WORKSPACE/* $root_path/
cd $root_path
for target in $targets; do
os="$(echo $target | cut -d '/' -f1)"
arch="$(echo $target | cut -d '/' -f2)"
output="${release_path}/${repo_name}_${os}_${arch}"
echo "----> Building project for: $target"
GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build -o $output
zip -j $output.zip $output > /dev/null
done
echo "----> Build is complete. List of files at $release_path:"
cd $release_path
ls -al
Once the action run is complete you'll see something similar in your logs:
----> Setting up Go repository
----> Building project for: darwin/amd64
----> Building project for: darwin/386
----> Building project for: linux/amd64
----> Building project for: linux/386
----> Building project for: windows/amd64
----> Building project for: windows/386
----> Build is complete. List of files at /github/workspace/.release:
total 16436
drwxr-xr-x 2 root root 4096 Feb 5 00:03 .
drwxr-xr-x 5 root root 4096 Feb 5 00:02 ..
-rwxr-xr-x 1 root root 1764764 Feb 5 00:02 test-go-action_darwin_386
-rw-r--r-- 1 root root 978566 Feb 5 00:02 test-go-action_darwin_386.zip
-rwxr-xr-x 1 root root 2003480 Feb 5 00:02 test-go-action_darwin_amd64
-rw-r--r-- 1 root root 1008819 Feb 5 00:02 test-go-action_darwin_amd64.zip
-rwxr-xr-x 1 root root 1676585 Feb 5 00:02 test-go-action_linux_386
-rw-r--r-- 1 root root 918555 Feb 5 00:02 test-go-action_linux_386.zip
-rwxr-xr-x 1 root root 1906945 Feb 5 00:02 test-go-action_linux_amd64
-rw-r--r-- 1 root root 952985 Feb 5 00:02 test-go-action_linux_amd64.zip
-rwxr-xr-x 1 root root 1728000 Feb 5 00:03 test-go-action_windows_386
-rw-r--r-- 1 root root 930942 Feb 5 00:03 test-go-action_windows_386.zip
-rwxr-xr-x 1 root root 1957376 Feb 5 00:02 test-go-action_windows_amd64
-rw-r--r-- 1 root root 972286 Feb 5 00:02 test-go-action_windows_amd64.zip
All compiled and compressed binares are saved under $GITHUB_WORKFLOW/.release
and thus could be used with any further actions as the files under $GITHUB_WORKSPACE
directory are persisted for the duration of the run.
Upload
Now that we've produced the binaries, the next logical step in the chain of actions is to upload them somewhere. That could be either to Amazon S3, Github Releases or any other service of your choice.
name: Build Go binaries
on:
push:
# We want to run the workflow on all branches.
# But you can restrict the runs if necessary.
branches:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Make binaries
uses: sosedoff/actions/golang-build@master
- name: Upload to Amazon S3
uses: ItsKarma/aws-cli@v1.70.0
with:
args: s3 sync .release s3://my-bucket-name
env:
# Make sure to add the secrets in the repo settings page
# AWS_REGION is set to us-east-1 by default
AWS_ACCESS_KEY_ID: $
AWS_SECRET_ACCESS_KEY: $
AWS_REGION: us-east-1
If everything in the workflow setup correctly you should be able to see a successfull run screen like this:
Extras
Developing actions using live environment is very slow and tends to be error prone. I would recommend looking into nektos/act, a tool to test the actions locally. While it does not 100% replicate the real environment it provides just enough to get the scripts flushed out. Also check out Actions Marketplace, there's a high chance that someone else have already come up with an Action of your interest.