CI/CD for a Go Monorepo with GitHub Actions
Lint, test, and build only the affected Go modules on every push.
A Go monorepo holds many binaries and shared packages under one go.mod (or several). This recipe vets and lints the whole tree, tests, and builds the binaries via a matrix.
What the pipeline does
- set up Go with build cache
- vet with go vet ./...
- lint with golangci-lint
- test with go test ./...
- build each cmd via a matrix
The workflow
A matrix builds each cmd target in parallel. For large repos, use dorny/paths-filter to skip targets whose code did not change.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- run: go vet ./...
- uses: golangci/golangci-lint-action@v6
- run: go test ./...
build:
needs: check
runs-on: ubuntu-latest
strategy:
matrix:
cmd: [api, worker, cli]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- run: go build -o bin/${{ matrix.cmd }} ./cmd/${{ matrix.cmd }}Caching and speed
setup-go cache: true restores the build and module cache keyed on go.sum, shared across matrix legs. Building many binaries in parallel adds up; cheaper managed runners such as Latchkey $0.0025/min at 2 vCPU against $0.006 GitHub-hosted keep wide matrices affordable and auto-retry transient module-proxy failures.
Deploying
Publish each binary as an artifact or a container per cmd, push to GHCR or ECR, and deploy each service independently. Use paths-filter to deploy only the services whose code changed.
Making this reliable in CI
- Pin every tool version. An unpinned toolchain turns a runner image update into a build break on unchanged code.
- Cache what is expensive to produce, and key the cache to the tool version so a restore across a version boundary cannot poison the build.
- Assert on the produced artifact rather than on the command succeeding.
- Set an explicit timeout so a hung step fails fast instead of consuming the whole job budget.
Key takeaways
- A matrix builds each cmd target in parallel.
- paths-filter skips unaffected targets in large monorepos.
- The Go build cache is shared across matrix legs via go.sum.