Home
Software
Writings
Using GitHub Actions with Go
in: Go programming articles
go
GitHub Actions is an alternative to CI services like Travis or CircleCI. It supports building Go projects hosted on GitHub.
This article describes how to use actions to build and test Go projects.

What do Actions give you?

With actions you can run code on every checkin (and many other GitHub events).
Most common use for actions is Continuous Integration (CI) where you build and test code to make sure that new changes didn’t introduce bugs.
When things fail you get notified by an e-mail.
You can see the logs of a particular run. Here’s how the UI for browsing the logs looks like
GitHub actions support running arbitrary code so you can do much more than just build and test the code.

Basic workflow for Go

A workflow defines one or more jobs,. Each job consists of multiple steps, like checking out source code, installing Go toolchain, building and testing your Go code etc.
Workflows run on computers operated by GitHub. The service is free for public repos and pay-as-you-go for private repos.
This is the simplest workflow that builds and tests a Go package. Create .github/workflows/go.yml file in your GitHub repository:
name: Build and test Go
on: [push, pull_request]
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Set up Go 1.13
        uses: actions/setup-go@v1
        with:
          go-version: 1.13

      - name: Check out source code
        uses: actions/checkout@v1

      - name: Build
				env:
					GOPROXY: "https://proxy.golang.org"
        run: go build .

      - name: Test
				env:
					GOPROXY: "https://proxy.golang.org"
        run: go test -v .
Let’s deconstruct this.

Define the name of a workflow

name defines a name of the workflow, visible in Actions tab in GitHub’s repository view.
name: Build and test Go

Specify what triggers a workflow

Workflow is triggered by an event in your GitHub repository. There are many event types, including a checkin, a creation of pull request etc.
on: [push, pull_request]
This means: execute workflow on checkin (well, when it’s actually pushed to GitHub) and pull request.
This is a good default from most projects.
[push, pull_request] is an YAML array with 2 elements.

Specify OS executing the workflow

runs-on defines the OS of the machine running the workflow. Most common:
Full list of available options.

Define a job

A job is a sequence of steps:
jobs:
  build:
    name: Build
We define only one job with id build whose human-visible name is Build.
We can define multiple jobs. Jobs run in parallel but they start in an empty environment, so if you have multiple steps, like build and test, it’s faster to have them as part of a single job.

Install Go toolchain

- name: Set up Go 1.13
  uses: actions/setup-go@v1
  with:
    go-version: 1.13
The beauty of actions is that they are open-sourced and can be shared. actions/setup-go is an action hosted in https://github.com/actions/setup-go. If you need to tweak how an action works, you can fork it and improve it.
Actions can be either JavaScript (node.js) scripts or docker images. actions/setup-go is a node.js action.
name is human-readable name of the step.
Actions can accept arguments, which we provide using with. go-version is an argument that actions/setup-go understands. It defines which version of Go we want to install.

Checkout source code

- name: Check out source code
  uses: actions/checkout@v1
action/checkout is a built-in GitHub action (which means we can’t see how it’s implemented).
It checks out your repository to /home/work/<repo_name> directory e.g. repository [github.com/kjk/notionapi](http://github.com/kjk/notionapi) would be checked out into /home/work/notionapi directory.
You can over-ride checkout directory with path argument (see full list of arguments).

Build and test Go code

- name: Build
	env:
		GOPROXY: "https://proxy.golang.org"
	run: go build .

- name: Test
  run: go test -v .
We can execute arbitrary commands in steps.
The most common steps for Go code are
Working directory when executing commands is the source code directory.
When a step fails, the workflow stops and you get an email.
For projects using module, go build will also download dependencies recorded in go.mod and go.sum.
Downloading modules via proxy is much faster than cloning repositories with git.
We explicitly specify GOPROXY for Go 1.12. In 1.13 it’s set by default.
Module proxy is only supported by Go 1.12 and later.

Advanced features

Using secrets

Sometimes you need to use a secret that you can’t expose to public. In this example we have a project that is deployed to a Netlify account.
In GitHub UI we defined a secret NETLIFY_TOKEN needed to authenticate with Netlify:
Here’s how to use it:
- name: Netlify deploy
  env:
    NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
  run: |
    ./netlifyctl -A ${NETLIFY_TOKEN} deploy || true
    cat netlifyctl-debug.log || true    
We can access secrets in workflow .yml file as ${{ secrets.<SECRET_NAME> }}. We can pass as an argument to a command executed with run or, as in this example, set an environment variable use env.
Remember that stdout and stderr of executed commands is logged so be careful to not print secrets.
More about secrets.

Matrix builds

Sometimes you want to test on multiple operating systems or with using different versions of Go.
You can specify that with matrix builds.
Here’s a way to run workflow on every supported OS:
runs-on: ${{ matrix.os }}
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
What happened here is:
Here’s a way to run workflow with multiple Go versions:
runs-on: ubuntu-latest
strategy:
  matrix:
    goVer: [1.11 1.12 1.13]
steps:
	- name: Set up Go ${{ matrix.goVer }}
	  uses: actions/setup-go@v1
	  with:
	    go-version: ${{ matrix.goVer }}
We defined goVer variable with 3 values and we use it to tell actions/setup-go@v1 which version of Go to install.
Those can be combined:
runs-on: ${{ matrix.os }}
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    goVer: [1.12 1.13]
steps:
	- name: Set up Go ${{ matrix.goVer }}
	  uses: actions/setup-go@v1
	  with:
	    go-version: ${{ matrix.goVer }}
This will execute 6 different builds for a combination of 3 x 2 values.

Speeding up builds by caching dependencies

Re-downloading dependencies on each build is wasteful, even when using Go modules cache.
Caching action (currently in preview mode) allows to download dependencies just once. As long as dependencies don’t change, they’ll be re-used between builds.
To use it for Go:
- name: Cache Go modules
  uses: actions/cache@preview
  with:
    path: ~/go/pkg/mod
    key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }}
		restore-keys: |
      ${{ runner.OS }}-build-${{ env.cache-name }}-
      ${{ runner.OS }}-build-
      ${{ runner.OS }}-      
The key parts:
You can read more about actions cache and official docs.

Finding bugs with staticcheck

Static analysis can find bugs in your code, which is great.
staticcheck does static analysis on Go code and finds issues like unused functions and many more.
To run staticcheck locally, do:
go get -u honnef.co/go/tools/cmd/staticcheck
staticcheck ./...
To run staticcheck on GitHub Actions CI:
- name: Staticcheck
  run: |
    # add executables installed with go get to PATH
    # TODO: this will hopefully be fixed by
    # https://github.com/actions/setup-go/issues/14
    export PATH=${PATH}:`go env GOPATH`/bin
    go get -u honnef.co/go/tools/cmd/staticcheck
    staticcheck ./...    

Uploading artifacts

When a CI job is finished, the server is destroyed. Sometimes we want to preserve files created during CI run. We can do it with a built-in [upload-artifact](https://github.com/actions/upload-artifact) action:
- uses: actions/upload-artifact@master
  with:
    name: my-artifact
    path: path/to/dir/with/artifacts
Uploaded artifacts will be accessible via web UI:

Cron: running workflows periodically

You can schedule workflows to be executed periodically e.g. once a day.
Define a workflow in a new .yml file in .github/workflows directory and schedule it to be periodically executed:
on:
  schedule:
    # * is a special character in YAML so you have to quote this string
    - cron: "0 4 * * *"
cron field uses cron syntax.
The order of fields is:
In the above example we schedule the workflow to run at 4:00 am every day, UTC time.
The syntax is quirky so use crontab guru to create / verify it.

Running multiple commands

You run multiple commands in a single step with |:
- name: Staticcheck
  run: |
    # add executables installed with go get to PATH
    # TODO: this will hopefully be fixed by
    # https://github.com/actions/setup-go/issues/14
    export PATH=${PATH}:`go env GOPATH`/bin
    go get -u honnef.co/go/tools/cmd/staticcheck
    staticcheck ./...    

Making run cross-platform

Keep in mind that commands executed with run are executed in an os-specific default shell (bash on Linux / Mac OS X and cmd.exe on Windows).
Simple commands like go build work the same in both shells, but e.g. export PATH=${PATH}:go env GOPATH/bin is specific to bash.
All OS-es have Python and Node installed, so if you need to execute a longer logic, write it in Python or Node.
For example, if you have foo.js script:
- name: Run foo.js
  run: node foo.js
See more information about shells.

Debugging workflows

Workflows are running on remote servers so if something goes wrong, it might be difficult to figure out why.
Here are few tips:

More Go resources

Written on Aug 16 2021. Topics: go.
home
Found a mistake, have a comment? Let me know.

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you: