Skip to content

Go CI/CD Pipelines Best Practices

Objective: Master senior-level Go CI/CD pipeline patterns for production systems. When you need to build robust, automated deployment pipelines, when you want to ensure code quality and security, when you need enterprise-grade CI/CD patternsโ€”these best practices become your weapon of choice.

Core Principles

  • Fast Feedback: Provide quick feedback on code changes
  • Quality Gates: Enforce quality standards at every stage
  • Security First: Integrate security scanning and compliance
  • Reproducible Builds: Ensure consistent, reproducible builds
  • Automated Deployment: Deploy with confidence and rollback capability

GitHub Actions Workflows

Basic CI Pipeline

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        go-version: ['1.20', '1.21', '1.22']
        os: [ubuntu-latest, windows-latest, macos-latest]

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: ${{ matrix.go-version }}

    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: |
          ${{ runner.os }}-go-

    - name: Download dependencies
      run: go mod download

    - name: Verify dependencies
      run: go mod verify

    - name: Run tests
      run: go test -v -race -coverprofile=coverage.out ./...

    - name: Run benchmarks
      run: go test -bench=. -benchmem ./...

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.out
        flags: unittests
        name: codecov-umbrella
        fail_ci_if_error: true

Advanced CI Pipeline

# .github/workflows/advanced-ci.yml
name: Advanced CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 2 * * 1'  # Weekly security scan

env:
  GO_VERSION: '1.21'
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # Quality checks
  quality:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: ${{ env.GO_VERSION }}

    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: |
          ${{ runner.os }}-go-

    - name: Download dependencies
      run: go mod download

    - name: Verify dependencies
      run: go mod verify

    - name: Run linters
      uses: golangci/golangci-lint-action@v3
      with:
        version: latest
        args: --timeout=5m

    - name: Run security scan
      uses: securecodewarrior/github-action-gosec@master
      with:
        args: './...'

    - name: Run tests
      run: go test -v -race -coverprofile=coverage.out ./...

    - name: Run benchmarks
      run: go test -bench=. -benchmem ./...

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.out
        flags: unittests
        name: codecov-umbrella
        fail_ci_if_error: true

  # Build and test
  build:
    runs-on: ubuntu-latest
    needs: quality

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: ${{ env.GO_VERSION }}

    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: |
          ${{ runner.os }}-go-

    - name: Download dependencies
      run: go mod download

    - name: Build application
      run: |
        go build -o bin/server ./cmd/server
        go build -o bin/worker ./cmd/worker

    - name: Test build artifacts
      run: |
        ./bin/server --version
        ./bin/worker --version

    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build-artifacts
        path: bin/

  # Security scanning
  security:
    runs-on: ubuntu-latest
    needs: quality

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        scan-type: 'fs'
        scan-ref: '.'
        format: 'sarif'
        output: 'trivy-results.sarif'

    - name: Upload Trivy scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

    - name: Run OWASP Dependency Check
      uses: dependency-check/Dependency-Check_Action@main
      with:
        project: 'myapp'
        path: '.'
        format: 'SARIF'
        out: 'dependency-check-report.sarif'

    - name: Upload dependency check results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'dependency-check-report.sarif'

  # Docker build and test
  docker:
    runs-on: ubuntu-latest
    needs: [quality, build]

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.DOCKER_REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
        format: 'sarif'
        output: 'trivy-results.sarif'

    - name: Upload Trivy scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

CD Pipeline

# .github/workflows/cd.yml
name: CD

on:
  push:
    branches: [ main ]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to deploy to'
        required: true
        default: 'staging'
        type: choice
        options:
        - staging
        - production

env:
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # Deploy to staging
  deploy-staging:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up kubectl
      uses: azure/setup-kubectl@v3
      with:
        version: 'v1.28.0'

    - name: Configure kubectl
      run: |
        echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
        export KUBECONFIG=kubeconfig

    - name: Deploy to staging
      run: |
        kubectl set image deployment/myapp myapp=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
        kubectl rollout status deployment/myapp

    - name: Run smoke tests
      run: |
        kubectl get pods -l app=myapp
        kubectl get services -l app=myapp

  # Deploy to production
  deploy-production:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-staging

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up kubectl
      uses: azure/setup-kubectl@v3
      with:
        version: 'v1.28.0'

    - name: Configure kubectl
      run: |
        echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
        export KUBECONFIG=kubeconfig

    - name: Deploy to production
      run: |
        kubectl set image deployment/myapp myapp=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
        kubectl rollout status deployment/myapp

    - name: Run smoke tests
      run: |
        kubectl get pods -l app=myapp
        kubectl get services -l app=myapp

    - name: Notify deployment
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        channel: '#deployments'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

GitLab CI/CD

GitLab CI Pipeline

# .gitlab-ci.yml
stages:
  - quality
  - build
  - test
  - security
  - deploy

variables:
  GO_VERSION: "1.21"
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"

# Quality checks
quality:
  stage: quality
  image: golang:${GO_VERSION}-alpine
  before_script:
    - apk add --no-cache git ca-certificates tzdata
    - go mod download
  script:
    - go mod verify
    - go vet ./...
    - go fmt ./...
    - golangci-lint run
    - gosec ./...
  artifacts:
    reports:
      junit: junit.xml
    paths:
      - coverage.out
    expire_in: 1 hour

# Build
build:
  stage: build
  image: golang:${GO_VERSION}-alpine
  before_script:
    - apk add --no-cache git ca-certificates tzdata
    - go mod download
  script:
    - go build -o bin/server ./cmd/server
    - go build -o bin/worker ./cmd/worker
  artifacts:
    paths:
      - bin/
    expire_in: 1 hour

# Test
test:
  stage: test
  image: golang:${GO_VERSION}-alpine
  before_script:
    - apk add --no-cache git ca-certificates tzdata
    - go mod download
  script:
    - go test -v -race -coverprofile=coverage.out ./...
    - go test -bench=. -benchmem ./...
  coverage: '/coverage: \d+\.\d+%/'
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    paths:
      - coverage.out
    expire_in: 1 hour

# Security scan
security:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --format json --output trivy-results.json .
  artifacts:
    reports:
      sast: trivy-results.json
    expire_in: 1 hour

# Docker build
docker-build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - main
    - develop

# Deploy to staging
deploy-staging:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - kubectl rollout status deployment/myapp
  only:
    - develop

# Deploy to production
deploy-production:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: production
    url: https://example.com
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - kubectl rollout status deployment/myapp
  when: manual
  only:
    - main

Jenkins Pipeline

Jenkinsfile

// Jenkinsfile
pipeline {
    agent any

    environment {
        GO_VERSION = '1.21'
        DOCKER_REGISTRY = 'registry.example.com'
        IMAGE_NAME = 'myapp'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Quality') {
            parallel {
                stage('Lint') {
                    steps {
                        sh 'golangci-lint run'
                    }
                }
                stage('Security') {
                    steps {
                        sh 'gosec ./...'
                    }
                }
                stage('Dependencies') {
                    steps {
                        sh 'go mod verify'
                        sh 'go mod tidy'
                    }
                }
            }
        }

        stage('Test') {
            steps {
                sh 'go test -v -race -coverprofile=coverage.out ./...'
                sh 'go test -bench=. -benchmem ./...'
            }
            post {
                always {
                    publishCoverage adapters: [
                        goCoberturaAdapter('coverage.out')
                    ], sourceFileResolver: sourceFiles('STORE_LAST_BUILD')
                }
            }
        }

        stage('Build') {
            steps {
                sh 'go build -o bin/server ./cmd/server'
                sh 'go build -o bin/worker ./cmd/worker'
            }
            post {
                always {
                    archiveArtifacts artifacts: 'bin/*', fingerprint: true
                }
            }
        }

        stage('Docker Build') {
            when {
                branch 'main'
            }
            steps {
                script {
                    def image = docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}")
                    docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
                        image.push()
                        image.push("latest")
                    }
                }
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                script {
                    sh """
                        kubectl set image deployment/myapp myapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}
                        kubectl rollout status deployment/myapp
                    """
                }
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                script {
                    sh """
                        kubectl set image deployment/myapp myapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}
                        kubectl rollout status deployment/myapp
                    """
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
        success {
            slackSend channel: '#deployments', color: 'good', message: "Build ${BUILD_NUMBER} succeeded"
        }
        failure {
            slackSend channel: '#deployments', color: 'danger', message: "Build ${BUILD_NUMBER} failed"
        }
    }
}

Build Scripts

Makefile

# Makefile
.PHONY: help build test lint security docker clean

# Default target
help: ## Show this help message
    @echo "Available targets:"
    @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

# Build targets
build: ## Build the application
    @echo "Building application..."
    go build -o bin/server ./cmd/server
    go build -o bin/worker ./cmd/worker

build-linux: ## Build for Linux
    @echo "Building for Linux..."
    GOOS=linux GOARCH=amd64 go build -o bin/server-linux ./cmd/server
    GOOS=linux GOARCH=amd64 go build -o bin/worker-linux ./cmd/worker

# Testing
test: ## Run tests
    @echo "Running tests..."
    go test -v -race -coverprofile=coverage.out ./...

test-coverage: ## Run tests with coverage
    @echo "Running tests with coverage..."
    go test -v -race -coverprofile=coverage.out ./...
    go tool cover -html=coverage.out -o coverage.html

benchmark: ## Run benchmarks
    @echo "Running benchmarks..."
    go test -bench=. -benchmem ./...

# Linting
lint: ## Run linters
    @echo "Running linters..."
    golangci-lint run

lint-fix: ## Run linters with auto-fix
    @echo "Running linters with auto-fix..."
    golangci-lint run --fix

# Security
security: ## Run security checks
    @echo "Running security checks..."
    gosec ./...

# Docker
docker: ## Build Docker image
    @echo "Building Docker image..."
    docker build -t myapp:latest .

docker-run: ## Run Docker container
    @echo "Running Docker container..."
    docker run -p 8080:8080 myapp:latest

# Cleanup
clean: ## Clean build artifacts
    @echo "Cleaning build artifacts..."
    rm -rf bin/
    rm -f coverage.out coverage.html
    go clean

# Dependencies
deps: ## Download dependencies
    @echo "Downloading dependencies..."
    go mod download
    go mod tidy

deps-update: ## Update dependencies
    @echo "Updating dependencies..."
    go get -u ./...
    go mod tidy

# Code generation
generate: ## Generate code
    @echo "Generating code..."
    go generate ./...

# Documentation
docs: ## Generate documentation
    @echo "Generating documentation..."
    godoc -http=:6060

Build Script

#!/bin/bash
# build.sh

set -e

# Configuration
APP_NAME="myapp"
VERSION="${1:-latest}"
REGISTRY="${REGISTRY:-localhost:5000}"
BUILD_CONTEXT="${BUILD_CONTEXT:-.}"
DOCKERFILE="${DOCKERFILE:-Dockerfile}"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Functions
log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# Build function
build_app() {
    log_info "Building application: ${APP_NAME}"

    # Build for multiple platforms
    GOOS=linux GOARCH=amd64 go build -o bin/server-linux ./cmd/server
    GOOS=linux GOARCH=arm64 go build -o bin/server-arm64 ./cmd/server
    GOOS=windows GOARCH=amd64 go build -o bin/server-windows.exe ./cmd/server
    GOOS=darwin GOARCH=amd64 go build -o bin/server-darwin ./cmd/server

    log_info "Application built successfully"
}

# Test function
run_tests() {
    log_info "Running tests"

    go test -v -race -coverprofile=coverage.out ./...

    log_info "Tests completed"
}

# Lint function
run_lint() {
    log_info "Running linters"

    golangci-lint run

    log_info "Linting completed"
}

# Security scan function
run_security() {
    log_info "Running security scan"

    gosec ./...

    log_info "Security scan completed"
}

# Docker build function
build_docker() {
    log_info "Building Docker image: ${APP_NAME}:${VERSION}"

    docker build -t ${APP_NAME}:${VERSION} -f ${DOCKERFILE} ${BUILD_CONTEXT}

    log_info "Docker image built successfully"
}

# Push function
push_docker() {
    log_info "Pushing Docker image to registry"

    docker tag ${APP_NAME}:${VERSION} ${REGISTRY}/${APP_NAME}:${VERSION}
    docker push ${REGISTRY}/${APP_NAME}:${VERSION}

    log_info "Docker image pushed successfully"
}

# Main execution
main() {
    log_info "Starting build process"

    # Run quality checks
    run_tests
    run_lint
    run_security

    # Build application
    build_app

    # Build Docker image
    build_docker

    # Push image
    if [ "${PUSH:-false}" = "true" ]; then
        push_docker
    fi

    log_info "Build process completed"
}

# Run main function
main "$@"

TL;DR Runbook

Quick Start

# 1. Run tests
make test

# 2. Run linters
make lint

# 3. Run security checks
make security

# 4. Build application
make build

# 5. Build Docker image
make docker

Essential Patterns

# GitHub Actions
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v4
    - run: go test ./...
// Jenkins
pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                sh 'go test ./...'
            }
        }
    }
}

This guide provides the complete machinery for building robust CI/CD pipelines for Go applications. Each pattern includes implementation examples, security considerations, and real-world usage patterns for enterprise deployment.