Skip to content
Latchkey

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.

.github/workflows/ci.yml
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.

Frequently asked questions

CI/CD for a Go Monorepo with GitHub Actions?
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.
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.
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.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card