HeadlinesBriefing favicon HeadlinesBriefing.com

Speed Up Golang CI by Replacing actions/setup-go

Hacker News •
×

We've found a new way to speed up parallel Golang continuous integration workflows by taking advantage of the Golang build cache. Replacing Git Hub's official actions/setup-go action with a drop-in equivalent cut our test job runtimes by 69%. We're open-sourcing cloudx-io/setup-go so you can do the same.

Git Hub's official actions/setup-go step makes parallel jobs interfere with each other's performance, and it continuously loads stale cache values. Backtesting in our monorepo, which has the common situation of a few parallel Golang test jobs (one for lints, one for tests, one for builds), suggests that 86% of the work the default action does is completely unnecessary. If you manage a moderately complex Go project, you can expect similar performance improvements; see our CI measurement methodology or just try it for yourself.

We Care About Fast CIWe've been shipping a lot of new products and features, and the pace at which we do it is actually increasing over time. This is no accident — we invest heavily in the tools and processes required to make this possible. At the center of every "software factory" are the test suites and Continuous Integration (CI) workflows that ensure code changes won't break in production.

If our tests run reliably, and quickly, on every change, we can build at fantastic speed without worrying about breaking things for our customers. This is important to us, so we measure and invest in the speed of our CI jobs. If you push code to a Cloud X repository, our goal is that you get a clear answer as to its acceptability — whether it builds, its tests pass, and it abides by our linter rules — within 90 seconds.

Speed can be achieved in a number of ways, but at the end of the day if you want things to be fast you have to make algorithmic improvements. We're already using Warp Build to run our CI jobs on fast, cost-efficient machines. As our test suite has scaled with our product surface area, we realized that actions/setup-go was not setting us up for success.

How actions/setup-go fails for parallel jobs Git Hub's actions/setup-go is the Git Hub-encouraged way to install and run Go in Git Hub Actions. It uses actions/cache internally to save and restore the local Go module cache and build cache directories. In principle, that should make downloaded module source code and build/test artifacts from one job run available to all the subsequent job runs in your repo.

Here's the default actions/setup-go cache key construction: This cache key is woefully incomplete: in a typical product under active development, only a tiny minority of code changes modify the target operating system, architecture, Go version, or go.mod files. The first time a job computes this hash key, it persists the final cache state to the Git Hub cache service. Until the next change that modifies one of those key elements, every single CI run will load that first value.

As you change your application, the restored go build module archives from this first run weaken — each subsequent build does more work from scratch. The restored go test outputs go stale too, so each subsequent job reruns more tests. CI degrades until you update go.mod! Moreover, multiple parallel jobs running actions/setup-go race to write different local cache states to the Git Hub cache service — different because the final Go cache state on a runner depends both on the source code and on the commands run.

For example, you might run separate lint and test jobs in parallel: Both jobs resolve the same default cache key, then race to write its value. Suppose the lint job finishes first: it saves a value without an updated test cache state. Subsequent test jobs will keep using that stale value until the cache key changes, and therefore re-run tests unnecessarily.

Linting, building, and testing a codebase are ideal candidates for memoization: their outputs (linter messages, built binaries, and test results respective...