# GitLab CI/CD Pipeline for Go # Optimized with caching, parallel execution, and deployment stages: - security - validate - test - build - deploy variables: GO_VERSION: "1.22" GOPATH: "$CI_PROJECT_DIR/.go" GOCACHE: "$CI_PROJECT_DIR/.cache/go-build" # Global cache configuration cache: key: files: - go.mod - go.sum paths: - .go/pkg/mod/ - .cache/go-build/ policy: pull # Reusable configuration .go_template: image: golang:${GO_VERSION} before_script: - go version - go env - mkdir -p .go - export PATH=$GOPATH/bin:$PATH # Validation stage lint: extends: .go_template stage: validate cache: key: files: - go.mod - go.sum paths: - .go/pkg/mod/ - .cache/go-build/ policy: pull-push script: # Install golangci-lint - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin - golangci-lint run --timeout=5m format-check: extends: .go_template stage: validate script: - test -z "$(gofmt -s -l .)" - go mod tidy - git diff --exit-code go.mod go.sum only: - merge_requests - main - develop # Security: Secret Scanning secret-scan:trufflehog: stage: security image: trufflesecurity/trufflehog:latest script: - trufflehog filesystem . --json --fail > trufflehog-report.json || true - | if [ -s trufflehog-report.json ]; then echo "❌ Secrets detected!" cat trufflehog-report.json exit 1 fi artifacts: when: always paths: - trufflehog-report.json expire_in: 30 days allow_failure: false only: - merge_requests - main - develop secret-scan:gitleaks: stage: security image: zricethezav/gitleaks:latest script: - gitleaks detect --source . --report-format json --report-path gitleaks-report.json artifacts: when: always paths: - gitleaks-report.json expire_in: 30 days allow_failure: true only: - merge_requests - main - develop # Security: SAST sast:semgrep: stage: security image: returntocorp/semgrep script: - semgrep scan --config=auto --sarif --output=semgrep.sarif . - semgrep scan --config=p/owasp-top-ten --json --output=semgrep-owasp.json . artifacts: reports: sast: semgrep.sarif paths: - semgrep.sarif - semgrep-owasp.json expire_in: 30 days allow_failure: false only: - merge_requests - main - develop security:gosec: extends: .go_template stage: security script: - go install github.com/securego/gosec/v2/cmd/gosec@latest - gosec -fmt json -out gosec-report.json ./... || true - gosec ./... # Fail on findings artifacts: when: always paths: - gosec-report.json expire_in: 30 days allow_failure: false only: - merge_requests - main - develop security:govulncheck: extends: .go_template stage: security script: - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck ./... allow_failure: false only: - merge_requests - main - develop # Test stage with matrix test: extends: .go_template stage: test parallel: matrix: - GO_VERSION: ["1.21", "1.22"] services: - postgres:15 - redis:7-alpine variables: POSTGRES_DB: testdb POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass DATABASE_URL: "postgresql://testuser:testpass@postgres:5432/testdb?sslmode=disable" REDIS_URL: "redis://redis:6379" script: - go mod download - go test -v -race -coverprofile=coverage.out -covermode=atomic ./... - go tool cover -func=coverage.out coverage: '/total:.*?(\d+\.\d+)%/' artifacts: when: always reports: coverage_report: coverage_format: cobertura path: coverage.out paths: - coverage.out expire_in: 30 days benchmark: extends: .go_template stage: test script: - go test -bench=. -benchmem ./... | tee benchmark.txt artifacts: paths: - benchmark.txt expire_in: 7 days only: - main - merge_requests # Build stage - multi-platform build:linux:amd64: extends: .go_template stage: build variables: GOOS: linux GOARCH: amd64 CGO_ENABLED: "0" script: - | go build \ -ldflags="-s -w -X main.version=$CI_COMMIT_SHORT_SHA -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o myapp-linux-amd64 \ ./cmd/myapp - ls -lh myapp-linux-amd64 artifacts: paths: - myapp-linux-amd64 expire_in: 7 days only: - main - develop - tags build:linux:arm64: extends: .go_template stage: build variables: GOOS: linux GOARCH: arm64 CGO_ENABLED: "0" script: - | go build \ -ldflags="-s -w -X main.version=$CI_COMMIT_SHORT_SHA -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o myapp-linux-arm64 \ ./cmd/myapp - ls -lh myapp-linux-arm64 artifacts: paths: - myapp-linux-arm64 expire_in: 7 days only: - main - develop - tags build:darwin:amd64: extends: .go_template stage: build variables: GOOS: darwin GOARCH: amd64 CGO_ENABLED: "0" script: - | go build \ -ldflags="-s -w -X main.version=$CI_COMMIT_SHORT_SHA -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o myapp-darwin-amd64 \ ./cmd/myapp - ls -lh myapp-darwin-amd64 artifacts: paths: - myapp-darwin-amd64 expire_in: 7 days only: - tags build:darwin:arm64: extends: .go_template stage: build variables: GOOS: darwin GOARCH: arm64 CGO_ENABLED: "0" script: - | go build \ -ldflags="-s -w -X main.version=$CI_COMMIT_SHORT_SHA -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o myapp-darwin-arm64 \ ./cmd/myapp - ls -lh myapp-darwin-arm64 artifacts: paths: - myapp-darwin-arm64 expire_in: 7 days only: - tags build:windows:amd64: extends: .go_template stage: build variables: GOOS: windows GOARCH: amd64 CGO_ENABLED: "0" script: - | go build \ -ldflags="-s -w -X main.version=$CI_COMMIT_SHORT_SHA -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o myapp-windows-amd64.exe \ ./cmd/myapp - ls -lh myapp-windows-amd64.exe artifacts: paths: - myapp-windows-amd64.exe expire_in: 7 days only: - tags # Build Docker image build:docker: stage: build image: docker:24-cli services: - docker:24-dind variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA before_script: - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY script: # Multi-stage build with Go - docker pull $CI_REGISTRY_IMAGE:latest || true - | docker build \ --cache-from $CI_REGISTRY_IMAGE:latest \ --tag $IMAGE_TAG \ --tag $CI_REGISTRY_IMAGE:latest \ --build-arg GO_VERSION=$GO_VERSION \ --build-arg VERSION=$CI_COMMIT_SHORT_SHA \ --build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ . - docker push $IMAGE_TAG - docker push $CI_REGISTRY_IMAGE:latest - echo "IMAGE_FULL_NAME=$IMAGE_TAG" >> build.env artifacts: reports: dotenv: build.env only: - main - develop - tags tags: - docker # Integration tests integration-test: extends: .go_template stage: test needs: [build:linux:amd64] dependencies: - build:linux:amd64 services: - postgres:15 variables: POSTGRES_DB: testdb POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass DATABASE_URL: "postgresql://testuser:testpass@postgres:5432/testdb?sslmode=disable" BINARY_PATH: "./myapp-linux-amd64" script: - chmod +x myapp-linux-amd64 - go test -v -tags=integration ./tests/integration/... only: - main - merge_requests # Deploy to staging deploy:staging: stage: deploy image: bitnami/kubectl:latest needs: [build:docker] environment: name: staging url: https://staging.example.com on_stop: stop:staging before_script: - kubectl config use-context staging-cluster script: - kubectl set image deployment/myapp myapp=$IMAGE_FULL_NAME --namespace=staging --record - kubectl rollout status deployment/myapp --namespace=staging --timeout=5m # Health check - | POD=$(kubectl get pod -n staging -l app=myapp -o jsonpath="{.items[0].metadata.name}") kubectl exec -n staging $POD -- /myapp version kubectl exec -n staging $POD -- curl -f http://localhost:8080/health || exit 1 only: - develop when: manual stop:staging: stage: deploy image: bitnami/kubectl:latest environment: name: staging action: stop script: - kubectl scale deployment/myapp --replicas=0 --namespace=staging when: manual only: - develop # Deploy to production deploy:production: stage: deploy image: bitnami/kubectl:latest needs: [build:docker, integration-test] environment: name: production url: https://api.example.com before_script: - kubectl config use-context production-cluster script: - echo "Deploying to production..." - kubectl set image deployment/myapp myapp=$IMAGE_FULL_NAME --namespace=production --record - kubectl rollout status deployment/myapp --namespace=production --timeout=5m # Health check - sleep 10 - | for i in {1..10}; do POD=$(kubectl get pod -n production -l app=myapp -o jsonpath="{.items[0].metadata.name}") if kubectl exec -n production $POD -- curl -f http://localhost:8080/health; then echo "Health check passed" exit 0 fi echo "Attempt $i failed, retrying..." sleep 10 done echo "Health check failed" exit 1 only: - main when: manual # Deploy to Cloud Run deploy:cloudrun: stage: deploy image: google/cloud-sdk:alpine needs: [build:docker] environment: name: production url: https://myapp.run.app before_script: - echo $GCP_SERVICE_KEY | base64 -d > ${HOME}/gcp-key.json - gcloud auth activate-service-account --key-file ${HOME}/gcp-key.json - gcloud config set project $GCP_PROJECT_ID script: - | gcloud run deploy myapp \ --image $IMAGE_FULL_NAME \ --region us-central1 \ --platform managed \ --allow-unauthenticated \ --memory 512Mi \ --cpu 1 \ --max-instances 10 # Health check - | URL=$(gcloud run services describe myapp --region us-central1 --format 'value(status.url)') curl -f $URL/health || exit 1 only: - main when: manual # Create release release: stage: deploy image: registry.gitlab.com/gitlab-org/release-cli:latest needs: - build:linux:amd64 - build:linux:arm64 - build:darwin:amd64 - build:darwin:arm64 - build:windows:amd64 dependencies: - build:linux:amd64 - build:linux:arm64 - build:darwin:amd64 - build:darwin:arm64 - build:windows:amd64 before_script: - apk add --no-cache coreutils script: # Create checksums - sha256sum myapp-* > checksums.txt - cat checksums.txt release: tag_name: $CI_COMMIT_TAG description: | Go Binary Release Binaries for multiple platforms: - Linux (amd64, arm64) - macOS (amd64, arm64/Apple Silicon) - Windows (amd64) Docker Image: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG Checksums: See attached checksums.txt assets: links: - name: 'Linux AMD64' url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/myapp-linux-amd64?job=build:linux:amd64' - name: 'Linux ARM64' url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/myapp-linux-arm64?job=build:linux:arm64' - name: 'macOS AMD64' url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/myapp-darwin-amd64?job=build:darwin:amd64' - name: 'macOS ARM64' url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/myapp-darwin-arm64?job=build:darwin:arm64' - name: 'Windows AMD64' url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/myapp-windows-amd64.exe?job=build:windows:amd64' - name: 'Checksums' url: '${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/checksums.txt?job=release' only: - tags # GitLab built-in security templates include: - template: Security/Dependency-Scanning.gitlab-ci.yml - template: Security/SAST.gitlab-ci.yml # Override GitLab template stages dependency_scanning: stage: security sast: stage: security # Workflow rules workflow: rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_BRANCH == "develop"' - if: '$CI_COMMIT_TAG' # Interruptible jobs .interruptible_template: interruptible: true lint: extends: [.go_template, .interruptible_template] test: extends: [.go_template, .interruptible_template] integration-test: extends: [.go_template, .interruptible_template]