GitHub Actions for Go Repos
GitHub Actions runs the same go test, lint, and module hygiene checks on every push and pull request.
Search across all documentation pages
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.
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.
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:
go test culture.git tag drift.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.mod aligns CI with the module's declared toolchain floor.go.sum files for multi-module repos.actions/checkout clones the PR merge ref or push SHA.setup-go installs the toolchain and warms module/build cache../...) under the module root.go.mod / go.sum match imports.| 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 |
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:
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.
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.
go.mod says 1.26 - Toolchain mismatch errors. Fix: use go-version-file: go.mod or explicit matching patch.go mod tidy && git diff --exit-code.go.sum key - Stale module cache after dependency bumps. Fix: include go.sum in cache-dependency-path.GOPRIVATE - Accidentally bypasses sumdb for public modules. Fix: scope to org prefixes only.-count=1 - Flaky package-level cache hides failures. Fix: use -count=1 in CI or clean test cache step.| 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 |
CI should meet or exceed the go directive in go.mod.
Production runtime version is separate from toolchain used to compile.
Add a job: go test -race ./... on 64-bit Linux.
Slower; often nightly or main-only to save PR time.
Either matrix over module directories or one job from repo root with go work in CI.
Validate with GOWORK=off before release tags.
actions/setup-go with cache: true uses go.sum hashes.
For workspaces, list every go.sum in cache-dependency-path.
Team choice.
Start with errors only; tighten as debt burns down.
Trigger workflow on push: tags: ['v*'], run full test, then attach artifacts or run goreleaser.
contents: read for test jobs.
Write or id-token only for release, signing, or deployment jobs.
Possible with bots, but review is essential.
Blind tidy PRs can hide malicious transitive deps.
Under 10 minutes for median PR keeps trunk flow healthy.
Split lint/matrix if longer; use path filters carefully.
Use service containers or dedicated workflows.
Keep default PR job hermetic with go test -short where applicable.
Set GOTOOLCHAIN: local or auto explicitly in env to avoid surprise downloads in CI.
Use pull_request with default GITHUB_TOKEN read-only permissions.
Avoid passing secrets to untrusted fork workflows without approval gates.
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).
Reviewed by Chris St. John·Last updated Jul 19, 2026