CI/CD Pipelines for Go Services
A Go microservice pipeline runs tests and security checks, builds a versioned binary or image, pushes to a registry, and updates Kubernetes with the same immutable tag or digest.
Search across all documentation pages
A Go microservice pipeline runs tests and security checks, builds a versioned binary or image, pushes to a registry, and updates Kubernetes with the same immutable tag or digest.
Fast feedback and reproducible artifacts matter more than exotic orchestration syntax.
Standard stages are verify (go test, lint, govulncheck), build (go build or Docker multi-stage), publish (push image with semver or SHA tag), and deploy (kubectl apply, Helm upgrade, or Argo CD sync).
Pin Go 1.26.x in CI to match production toolchains.
Promote the exact image digest tested in staging; never rebuild at deploy time with different flags.
Quick-reference recipe card - copy-paste ready.
# .github/workflows/service.yml (excerpt)
name: orders-api
on:
push:
branches: [main]
jobs:
verify:
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 install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && golangci-lint run
- run: go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...
build-push:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
VERSION=$(git describe --tags --always)
docker build \
--build-arg VERSION="$VERSION" \
--build-arg COMMIT="${{ github.sha }}" \
-t ghcr.io/myorg/orders-api:$VERSION .
docker push ghcr.io/myorg/orders-api:$VERSION
deploy-staging:
needs: build-push
runs-on: ubuntu-latest
steps:
- run: kubectl set image deployment/orders-api api=ghcr.io/myorg/orders-api:$VERSIONWhen to reach for this:
GitHub Actions workflow with test matrix, vulnerability scan, and digest-pinned deploy:
name: orders-api
on:
push:
branches: [main]
pull_request:
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/myorg/orders-api
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.26.x"
cache-dependency-path: go.sum
- run: go test -race -count=1 ./...
- run: go vet ./...
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.26.x"
- uses: golangci/golangci-lint-action@v6
with:
version: latest
vuln:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.26.x"
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
- run: govulncheck ./...
image:
needs: [test, lint, vuln]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
version: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- id: meta
run: echo "version=$(git describe --tags --always)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
build-args: |
VERSION=${{ steps.meta.outputs.version }}
COMMIT=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: image
runs-on: ubuntu-latest
environment: staging
steps:
- uses: azure/setup-kubectl@v4
- run: |
kubectl set image deployment/orders-api \
api=${{ env.IMAGE }}@${{ needs.image.outputs.digest }}
kubectl rollout status deployment/orders-api --timeout=120sWhat this demonstrates:
govulncheck blocks merges with reachable known CVEs in dependencies.-race catches data races before production.-ldflags embed version metadata for runtime logs.| Stage | Command / action | Fail criteria |
|---|---|---|
| Unit/integration | go test ./... | test or race failure |
| Static analysis | golangci-lint run | configured linters |
| Vuln scan | govulncheck ./... | reachable vulns per policy |
| Build | docker build or go build | compile error |
| Scan image | trivy/grype on pushed tag | critical CVE policy |
| Deploy | kubectl / GitOps sync | rollout timeout |
# Makefile targets local/CI parity
test:
go test -race -count=1 ./...
lint:
golangci-lint run
vuln:
govulncheck ./...
build:
CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o bin/api ./cmd/apimage targets keep CI commands identical to local developer workflows.go.sum; run go mod verify in CI to detect tampering.GOPRIVATE and .netrc or credential helpers for private modules.Instead of kubectl set image from CI, push manifest changes to a deploy repo.
Argo CD or Flux reconciles cluster state from git.
The pipeline updates kustomization.yaml image digest; humans review deploy PRs separately from app PRs.
GOFLAGS or cached modules can produce divergent binaries from CI build.main with required checks.Yes for services where concurrency matters; it adds time but catches races early.
Shard packages across matrix jobs if runtime grows large.
Run httptest handler tests in unit stage; spin testcontainers or a staging namespace for DB integration on main or nightly.
Block production promote on staging smoke tests hitting /readyz.
Use replace directives locally only; CI should build from tagged module versions.
Set GOPRIVATE=*.corp.example and configure git credentials in the runner.
Digest is strongest immutability.
Semver tags are human-friendly; combine both in CI logs: 1.4.2@sha256:....
Auto-deploy to staging on main merge; production requires manual approval or scheduled promote after error budget checks.
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 16, 2026