GitHub Actions for Go Repos
GitHub Actions runs the same go test, lint, and module hygiene checks on every push and pull request.
Well-designed workflows cache modules, pin Go versions, and gate merges before bad go.mod changes reach trunk.
Summary
A minimal Go CI workflow sets up Go, downloads modules with caching, runs tests and vet, and fails if go mod tidy would change files.
Add golangci-lint, race detection, and release jobs that create semver tags when maintainers need automation.
Keep secrets scoped to GOPRIVATE module fetch and publishing credentials.
Recipe
Quick-reference recipe card - copy-paste ready.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26.x'
cache: true
- run: go test ./...
- run: go vet ./...
- run: go mod tidy && git diff --exit-code go.mod go.sumWhen to reach for this:
- New Go module repo needing PR checks before merge.
- Replacing ad hoc local-only
go testculture. - Adding release automation after manual
git tagdrift. - Enforcing tidy module graphs on monorepos with path filters.
Working Example
Complete CI plus lint job for github.com/myorg/widget:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
env:
GOTOOLCHAIN: local
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: |
go.sum
**/go.sum
- name: Test
run: go test -count=1 ./...
- name: Vet
run: go vet ./...
- name: Mod tidy check
run: go mod tidy && git diff --exit-code go.mod go.sum
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- uses: golangci/golangci-lint-action@v6
with:
version: latestWhat this demonstrates:
go-version-file: go.modaligns CI with the module's declared toolchain floor.- Cache paths include nested
go.sumfiles for multi-module repos. - Separate lint job keeps feedback parallel without blocking test startup.
Deep Dive
How It Works
actions/checkoutclones the PR merge ref or push SHA.setup-goinstalls the toolchain and warms module/build cache.- Test job compiles all packages (
./...) under the module root. - Tidy check ensures committed
go.mod/go.summatch imports. - Optional jobs publish tags, binaries, or module indexes.
Workflow Triggers
| Trigger | Typical use |
|---|---|
pull_request | Gate merges to trunk |
push to main | Post-merge validation + release |
push tags v* | Release artifacts or proxy smoke tests |
workflow_dispatch | Manual rebuild or hotfix pipeline |
Private Modules
env:
GOPRIVATE: github.com/myorg/*
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf "https://github.com/"
if: env.GOPRIVATE != ''Use a fine-scoped PAT or GitHub App token readable only by private deps.
Document the same GOPRIVATE prefix for developers locally.
Release Job Sketch
release:
if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go test ./...
- run: goreleaser release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Tag push triggers build after tests pass; goreleaser is optional but common for Go binaries.
Monorepo Path Filters
on:
pull_request:
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- '.github/workflows/**'Skip doc-only jobs when no module-impacting paths change, but never skip when go.mod changes.
Gotchas
- Pinning an old Go patch in CI while
go.modsays 1.26 - Toolchain mismatch errors. Fix: usego-version-file: go.modor explicit matching patch. - Missing tidy check - Trunk accumulates orphan requires. Fix: always
go mod tidy && git diff --exit-code. - Caching without
go.sumkey - Stale module cache after dependency bumps. Fix: includego.sumincache-dependency-path. - Overbroad
GOPRIVATE- Accidentally bypasses sumdb for public modules. Fix: scope to org prefixes only. - Running tests without
-count=1- Flaky package-level cache hides failures. Fix: use-count=1in CI or clean test cache step.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| GitLab CI / CircleCI | Org standard is non-GitHub | You already standardized on Actions for Go |
| Nightly full race/matrix | Deep concurrency coverage | Every PR (too slow) |
| Self-hosted runners | Custom hardware, air-gapped | Simple public library |
| Pre-commit hooks only | Fast local feedback | You need enforced server-side gates |
FAQs
Should CI use the same Go version as production?
CI should meet or exceed the go directive in go.mod.
Production runtime version is separate from toolchain used to compile.
How run tests with race detector?
Add a job: go test -race ./... on 64-bit Linux.
Slower; often nightly or main-only to save PR time.
Do I need a separate job per module in monorepos?
Either matrix over module directories or one job from repo root with go work in CI.
Validate with GOWORK=off before release tags.
How cache modules in Actions?
actions/setup-go with cache: true uses go.sum hashes.
For workspaces, list every go.sum in cache-dependency-path.
Should lint fail on warnings?
Team choice.
Start with errors only; tighten as debt burns down.
How test tag releases in CI?
Trigger workflow on push: tags: ['v*'], run full test, then attach artifacts or run goreleaser.
What permissions does CI need?
contents: read for test jobs.
Write or id-token only for release, signing, or deployment jobs.
Can workflows bump go.mod automatically?
Possible with bots, but review is essential.
Blind tidy PRs can hide malicious transitive deps.
How long should CI take?
Under 10 minutes for median PR keeps trunk flow healthy.
Split lint/matrix if longer; use path filters carefully.
Should integration tests hit real services?
Use service containers or dedicated workflows.
Keep default PR job hermetic with go test -short where applicable.
How align workflow Go version with GOTOOLCHAIN?
Set GOTOOLCHAIN: local or auto explicitly in env to avoid surprise downloads in CI.
Do fork PRs need special handling?
Use pull_request with default GITHUB_TOKEN read-only permissions.
Avoid passing secrets to untrusted fork workflows without approval gates.
Related
- Branching Strategies & Trunk-Based Flow - CI gates trunk expects
- Signed Tags & Go Module Releases - Tag-triggered release pipelines
- Code Review with GitHub PRs - What reviewers expect from green CI
- Monorepo Git Patterns with go work - Multi-module workflow paths
- Git & GitHub Best Practices - Team-wide Git and CI habits
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).