CI Quality Gates: Lint, Test, Race, Vuln
Production Go services need a CI pipeline template that blocks merge until format, static analysis, tests, race detection, and vulnerability scans pass.
This page is a copy-paste starting point teams can adapt for GitHub Actions, GitLab CI, or Buildkite.
Recipe
Quick-reference recipe card - copy-paste ready.
# .github/workflows/go-quality.yml
name: go-quality
on: [pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26.x'
cache: true
- run: test -z "$(gofmt -l .)"
- run: go vet ./...
- run: go test -race -count=1 ./...
- uses: golangci/golangci-lint-action@v6
with:
version: v2.1.6
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
- run: govulncheck ./...When to reach for this:
- Creating the first workflow for a new Go microservice.
- Auditing whether race and vuln scans are optional when they should be required.
- Platform team publishing a golden pipeline for dozens of repos.
- Post-incident action item: "add govulncheck to CI."
Working Example
Full GitHub Actions workflow with module tidy check:
name: go-quality
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Verify module tidy
run: |
go mod tidy
git diff --exit-code go.mod go.sum
- name: Format
run: test -z "$(gofmt -l .)"
- name: Vet
run: go vet ./...
- name: Test with race detector
run: go test -race -count=1 -timeout=10m ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v2.1.6
args: --timeout=5m ./...
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...Branch protection (document for admins):
- Require
go-quality / gatestatus before merge. - Dismiss stale reviews on new pushes.
- Require linear history if your team uses squash merge.
What this demonstrates:
- Tidy check prevents dependency drift without review.
- Race runs on every PR, not only nightly.
- govulncheck uses the same module graph as tests.
- go-version-file tracks
go.modautomatically.
Deep Dive
How It Works
- CI clones a clean tree (no local hook skips).
go test -raceinstruments memory accesses; failures include goroutine stacks.- golangci-lint loads
.golangci.ymlfrom the repo root. - govulncheck reports reachable vulnerable symbols, not every transitive CVE headline.
Gate Order and Parallelism
+--> format + vet (fast)
|
PR ----+--> go test -race
|
+--> golangci-lint
|
+--> govulncheckRun independent jobs in parallel matrix for monorepos:
strategy:
matrix:
module: [services/api, services/worker, libs/auth]
steps:
- run: cd ${{ matrix.module }} && go test -race ./...Gate Reference Table
| Gate | Command | Fails on | Typical duration |
|---|---|---|---|
| Format | gofmt -l . | Unformatted .go | seconds |
| Vet | go vet ./... | Suspicious constructs | seconds-minutes |
| Test | go test ./... | Assertion failures | minutes |
| Race | go test -race ./... | Data races | minutes (slower) |
| Lint | golangci-lint run | Linter findings | minutes |
| Vuln | govulncheck ./... | Reachable CVEs | minutes |
| Tidy | go mod tidy + diff | Drift in go.mod/sum | seconds |
Go Notes
# Shard tests in large repos
go test -race -count=1 $(go list ./... | head -20)
# Cache modules in CI (setup-go handles on GitHub)
GOMODCACHE=$RUNNER_TEMP/gomodcache go test ./...Set GOTOOLCHAIN=local or pin go-version explicitly for reproducibility.
Gotchas
- Race only on main branch - PRs merge races into production. Fix: Require
-raceon every PR job. - govulncheck warnings ignored - Teams mute without upgrade path. Fix: Triage with severity; bump modules or document accepted risk with expiry.
- golangci-lint timeout 1m default - Large monorepos fail flakey. Fix:
--timeout=5mand module sharding. - Skipping vet because lint includes govet - Broken when lint job skipped. Fix: Keep vet in fast job or enforce lint as required check.
- Integration tests need Docker - Job passes unit tests only. Fix: Separate
integrationjob with services; still block merge. - Unpinned govulncheck@latest - CI behavior shifts silently. Fix: Pin version in workflow or
tools.go.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Nightly full scan + PR diff lint | Huge monorepo cost pressure | Security-critical services without nightly |
| Trunk-based without branch protection | Solo maintainer | Teams with multiple merge paths |
| Self-hosted runners with cache | Massive private modules | You cannot secure runner supply chain |
| SonarQube / external SAST | Org mandates central dashboard | Replacing go-native tools entirely |
FAQs
Must every job run on every PR?
Yes for merge gates that protect main.
Optional jobs (benchmarks, fuzz) can be informational with separate status.
How do I handle flaky tests?
Fix or quarantine with owner and deadline.
Do not disable race globally for flakes.
Should govulncheck fail on unreachable CVEs?
Team policy choice.
Many fail only on reachable symbols; others warn on all module hits.
What Go version should CI use?
Match go directive in go.mod or use go-version-file.
Matrix test N and N-1 for published libraries.
How long should the quality job take?
Target under 10 minutes for developer flow.
Shard modules or use remote cache if longer.
Do forks need different workflow permissions?
Yes.
Pull requests from forks may not have secrets; use pull_request_target carefully or limit to public checks.
Where does go test -cover fit?
Informational job or coverage gate on critical packages.
Not a substitute for race or lint.
Should we run go fix in CI?
Run on upgrade branches, commit results.
Not on every feature PR unless modernizers are the PR goal.
How do workspaces affect CI?
Check out repo root; run go test ./... from workspace root or matrix per go.mod.
Can one failing linter warn instead of fail?
Configure severity in .golangci.yml.
Keep security linters as errors.
Related
- Code Quality Basics - local equivalents of CI gates
- golangci-lint Configuration - lint job config
- govulncheck for Dependency Vulnerabilities - vuln triage
- Pre-commit Hooks & Review Checklists - local fast path
- staticcheck & golangci-lint in CI - analyzer depth
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).