Open-source project
mvdan/github-actions-golang avatar
mvdan/github-actions-golang

github-actions-golang is a pattern library for Go CI on GitHub Actions

GitHub Actions as CI for Go

1,047 stars74 forksGoBSD-3-Clause

At a glance

What is it?
The repository is documentation rather than a tool: a set of GitHub Actions workflow patterns for Go projects, covering version matrices, environment variables, caching, conditionals, secrets, private modules and the Windows line ending trap that bites Go testdata. It is BSD-3-Clause licensed and the last push was on 2026-10-12.
Who is it for?
Adopt github-actions-golang as a reference when you set up or repair continuous integration for a Go repository, because it captures the small decisions that separate a workflow that works from one that quietly caches the wrong thing or corrupts testdata on Windows.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 11 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A document with a Go module in the same tree

The repository describes itself as GitHub Actions as CI for Go, and the framing in the README is the first thing to understand. It is not a runner, an action, or a tool you invoke. It is a collection of documented patterns for making GitHub Actions work well for Go, and the evidence is in the layout and in the prose.

The opening paragraph says that GitHub Actions includes CI/CD for free for open source repositories, and that this document contains information on making it work well for Go. The word document is doing the work. Everything in the README is a workflow file, an explanation, or a link to the official GitHub documentation.

That said, the repository is not only prose. The file listing shows actions.go and actions_test.go alongside a go.mod and a go.sum, and a .github/ directory that presumably holds the workflow used to test the repository itself. The go.mod declares the module as mod.test/actions with a Go 1.26.0 language version and depends on rsc.io/quote, which is the package that quotes Go source. So there is a small Go program in the tree, and the module path is a test path rather than a published one.

The audience is a Go developer setting up CI for the first time, or a maintainer inheriting a workflow written by someone else. The value is not novelty, since all of this is in the GitHub documentation, but curation. The README decides what to document and in what order, and it documents the Go specific parts first.

The reference workflow is a matrix of Go versions and operating systems

The README opens with a complete workflow, which it describes as being runnable in action. The shape is the canonical one, and it is worth reading closely because every later pattern builds on it.

yaml
on: [push, pull_request]
name: Test
permissions:
  contents: read
jobs:
  test:
    strategy:
      matrix:
        go-version: [1.26.x, 1.27.x]
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@v7
    - uses: actions/setup-go@v7

The workflow triggers on push and pull request, it declares a permissions block that limits the workflow token to read-only access, and the single job runs a matrix. The matrix has two Go versions and three operating systems, so the job fans out into six concurrent runs.

The summary section explains the vocabulary. Each workflow file has a number of jobs, which get run on specified events, and run concurrently with each other, and you can add workflow status badges. Each job runs on a configuration matrix, and the example given is testing two major Go versions on three operating systems.

One detail in that matrix is worth copying rather than transcribing literally. The Go versions are written as 1.26.x and 1.27.x, but the README also notes that actions/setup-go accepts stable and oldstable to always test the latest two Go versions. Those two spellings answer different questions. A fixed list pins the test to specific releases, while stable and oldstable keeps the test on the newest two versions as they are released. Neither is universally right, and the choice depends on whether you want to catch a regression in a new Go release or to test a fixed compatibility floor.

The permissions block is the other line to keep. The README says the permissions limit the workflow's token to read-only access, which is all it needs.

Environment variables have two mechanisms, and the second one is deferred

The FAQ answers the environment variable question twice, and the two answers cover genuinely different situations.

The first is static configuration. Variables can be set for an entire workflow, for a job, or for each step:

yaml
env:
  GOPROXY: "https://proxy.company.com"
jobs:
  [...]

The second is runtime configuration, using environment files. The README notes you can use them to set environment variables or add an element to $PATH:

yaml
steps:
- name: Set env vars
  run: |
      echo "CGO_ENABLED=0" >> $GITHUB_ENV
      echo "${HOME}/goroot/bin" >> $GITHUB_PATH

The important caveat is timing. Those take effect for future steps in the job, so a step that writes to $GITHUB_ENV cannot change its own environment, only the environment of the steps that follow it. This is the behaviour that surprises people, because the write looks immediate in the log.

The platform difference is called out too. The default shell on Windows is PowerShell, which needs $env:GITHUB_ENV instead, or you can use shell: bash on the step. Since a Go project frequently runs the same workflow on all three operating systems, a step written in the POSIX style will fail on Windows unless the shell is specified.

The GOPROXY example in the first snippet is a good example of the static case, since a corporate module proxy is known when the workflow file is written, whereas a value discovered during the build, such as a version number or a computed path, belongs in the runtime case.

Caching is automatic unless you turn it off

The caching section answers a question that many Go workflows get wrong by hand rolling it.

actions/setup-go caches GOCACHE and GOMODCACHE automatically, hashing go.mod into the cache key. The key detail is that the hash is taken from go.mod, so the cache changes when the dependency set changes and stays put when it does not.

You can turn that off with cache: false, and then use your own caching. The README gives an example that keeps only the module cache:

yaml
- uses: actions/cache@v6
  with:
    path: ~/go/pkg/mod
    key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod') }}
    restore-keys: |
      ${{ runner.os }}-go-

The restore-keys line is the part that makes this pattern work. It gives the cache action a prefix to fall back on, so a run whose exact key misses can still restore a partial cache from a previous build. Without it, every dependency change is a cold cache and the saving disappears exactly when you most want it.

The key is scoped by runner.os as well as the go.mod hash, which avoids a Linux build restoring a cache written on Windows or macOS, where the module cache contents are not interchangeable. The README links to the official dependency caching guide for the rest of the details.

The general lesson for Go projects is that the build cache and the module cache are two different things with two different invalidation rules, and setup-go handles both by default. Disabling it is a decision to take deliberately.

Conditionals, fail-fast and the shape of a matrix run

Three related mechanisms are documented in this part of the README, and they are worth separating.

Conditionals. A step can be guarded with an if expression using the custom expression language, for example:

yaml
- if: github.event_name == 'push' && matrix.os == 'ubuntu-latest'
  run: go run ./endtoend

This is the mechanism for the cases where a step only makes sense in one environment, such as an end to end test that only runs on Linux, or only on a push rather than on a pull request from a fork.

Matrix control. Extra matrix jobs can be included and specific ones excluded. And by default, one failing job cancels the rest of the matrix, which can be changed by setting fail-fast to false so that all of them finish. That default is a deliberate trade-off: it saves minutes of compute on a broken change, and it costs you the information from the combinations that would have failed for a different reason. A project with a large matrix, or one where each combination tests a different platform specific behaviour, often wants fail-fast off.

Multiline scripts. A step that runs several commands uses the block scalar form:

yaml
- name: Series of commands
  run: |
    go test ./...
    go test -race ./...

The race detector is the natural second command here, and running it as a separate invocation rather than as a flag on the first keeps the fast run and the slow run distinguishable in the log.

The README also has a short question on whether to use two workflows or two jobs on one workflow, and the answer is that the biggest difference is the user interface, since workflow results are shown separately, with grouping useful when you want to customise triggers or set up dependencies via needs.

Private modules need a token, GOPRIVATE and a git rewrite

The private modules section is the most operationally specific part of the document, and the one most likely to be needed the day a Go project adds a dependency on a private repository.

It is possible to install modules from private GitHub repositories without running your own proxy. The requirements are a personal access token with read access to the contents of the repositories, added as a secret environment variable, and a configured GOPRIVATE setting.

The git configuration step rewrites the remote URL to include the token:

yaml
- name: Configure git for private modules
  env:
    TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
  run: git config --global url."https://user:${TOKEN}@github.com".insteadOf "https://github.com"

And GOPRIVATE is set the same way as any other environment variable:

yaml
env:
  GOPRIVATE: "github.com/yourorg"
jobs:
  [...]

Two constraints are attached. The GITHUB_TOKEN provided to workflows can only access the repository running the workflow, so it cannot be used to reach a private dependency. And you can define anything as the username in the URL, since it is not taken into account by GitHub.

That last point is worth internalising. The token is doing the authentication and the username is decoration, so the placeholder user in the URL is not a mistake to be fixed.

The secret itself is referenced the way the README shows for any secret, a step with an env block mapping the name to the secrets context, so the token is never written into the workflow file.

The CRLF trap on Windows is the caveat that matters

The caveats section is short and points at a specific issue tracker entry, and it is worth reading before you adopt the rest of the document.

The problem is that git config core.autocrlf defaults to true, so you need to be careful about CRLF endings in your plaintext testdata files on Windows. The recommended workaround is a .gitattributes file:

gitattributes
* -text

This is not a Go problem, it is a Go testing problem. Go testdata directories hold golden files, expected outputs that a test compares byte for byte against what the program produced. On Windows, if git checks out those files with CRLF line endings while the program writes LF, the comparison fails, and it fails only on Windows, which is the worst possible way for a test to break.

The fix disables git's line ending conversion for every file in the repository, so the bytes in the working tree are the bytes in the object store.

This single entry is the most valuable thing in the README for a team that has never hit the problem, and it is also the kind of thing that gets lost when a workflow is written from scratch rather than copied from a source that has already been bitten by it. The other FAQ entries are conveniences, the environment file trick, the cache restore keys, the fail-fast switch. This one prevents a test failure that costs an afternoon.

The quick links section points at the GitHub concepts page, the usage limits page, the syntax and fields reference, and the page on GitHub hosted runners, which is where to check what your matrix costs against the runner limits.

Editorial conclusion

Adopt github-actions-golang as a reference when you set up or repair continuous integration for a Go repository, because it captures the small decisions that separate a workflow that works from one that quietly caches the wrong thing or corrupts testdata on Windows. Do not adopt it as a dependency, because there is nothing to install and the repository is a document, and do not copy the example workflow verbatim into a project that needs a specific Go version, since the matrix there tests the two most recent releases rather than your minimum supported version. Before you copy anything, read the caveats section about git config core.autocrlf on Windows, check whether your module uses private dependencies and therefore needs GOPRIVATE and a personal access token, and confirm the permissions block in your own workflow really only needs contents read.

Frequently asked questions

What is the github-actions-golang repository?

It is a document of GitHub Actions patterns for Go projects rather than a tool you install. It covers workflow structure, version matrices, environment variables, caching, conditionals, secrets, private modules and a Windows line ending caveat.

How do I test multiple Go versions and operating systems in GitHub Actions?

Use a build matrix in the job, with go-version and os as the matrix axes, and set runs-on from the os value. The README also notes that actions/setup-go accepts stable and oldstable to always test the latest two Go versions.

Does actions/setup-go cache the Go build and module caches?

Yes. It caches GOCACHE and GOMODCACHE automatically, hashing go.mod into the cache key. You can disable that with cache: false and use actions/cache yourself, as the README shows with a path of ~/go/pkg/mod and a restore-keys prefix.

How do I install private Go modules in a GitHub Actions workflow?

Add a personal access token with read access as a secret, set GOPRIVATE to the host and path, and rewrite the git remote URL with git config url.insteadOf so the token is used for github.com. The built in GITHUB_TOKEN cannot be used, since it can only access the repository running the workflow.

Why do my Go tests fail only on Windows runners?

The README warns that git config core.autocrlf defaults to true, so plaintext testdata files can get CRLF endings on Windows and no longer match the expected output. The recommended fix is a .gitattributes file containing * -text.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. mvdan/github-actions-golang on GitHub
  4. README

Was this useful?