# Go CI/CD Pipeline # Optimized with caching, matrix testing, and deployment name: Go CI on: push: branches: [main, develop] paths-ignore: - '**.md' - 'docs/**' pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: GO_VERSION: '1.22' jobs: # Security: Secret Scanning secret-scan: name: Secret Scanning runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: TruffleHog Secret Scan uses: trufflesecurity/trufflehog@main with: path: ./ base: ${{ github.event.repository.default_branch }} head: HEAD - name: Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Security: SAST sast: name: Static Analysis (CodeQL) runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read security-events: write steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: go queries: security-and-quality - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 lint: name: Lint runs-on: ubuntu-latest needs: [secret-scan] timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Run golangci-lint uses: golangci/golangci-lint-action@v4 with: version: latest args: --timeout=5m - name: Check formatting run: | if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then echo "Please run: gofmt -s -w ." gofmt -s -l . exit 1 fi - name: Check go mod tidy run: | go mod tidy git diff --exit-code go.mod go.sum test: name: Test (Go ${{ matrix.go-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: matrix: go-version: ['1.21', '1.22'] os: [ubuntu-latest, macos-latest] fail-fast: false services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 redis: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 6379:6379 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: true - name: Download dependencies run: go mod download - name: Run unit tests env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb?sslmode=disable REDIS_URL: redis://localhost:6379 run: | go test -v -race -coverprofile=coverage.out -covermode=atomic ./... - name: Upload coverage to Codecov if: matrix.go-version == '1.22' && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v4 with: files: ./coverage.out fail_ci_if_error: false - name: Run benchmarks if: matrix.go-version == '1.22' && matrix.os == 'ubuntu-latest' run: go test -bench=. -benchmem ./... | tee benchmark.txt - name: Upload benchmark results if: matrix.go-version == '1.22' && matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 with: name: benchmark-results path: benchmark.txt security: name: Security Scanning runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Run Gosec uses: securego/gosec@master with: args: '-fmt json -out gosec-report.json ./...' continue-on-error: true - name: Run govulncheck run: | go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... - name: Upload security reports if: always() uses: actions/upload-artifact@v4 with: name: security-reports path: gosec-report.json build: name: Build runs-on: ubuntu-latest needs: [lint, test, sast, security] timeout-minutes: 15 strategy: matrix: goos: [linux, darwin, windows] goarch: [amd64, arm64] exclude: - goos: windows goarch: arm64 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Build binary env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} CGO_ENABLED: 0 run: | OUTPUT="myapp-${{ matrix.goos }}-${{ matrix.goarch }}" if [ "${{ matrix.goos }}" = "windows" ]; then OUTPUT="${OUTPUT}.exe" fi go build \ -ldflags="-s -w -X main.version=${{ github.sha }} -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o $OUTPUT \ ./cmd/myapp ls -lh $OUTPUT - name: Upload binary uses: actions/upload-artifact@v4 with: name: myapp-${{ matrix.goos }}-${{ matrix.goarch }} path: myapp-* retention-days: 7 integration-test: name: Integration Tests runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' timeout-minutes: 30 services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s ports: - 5432:5432 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Download Linux binary uses: actions/download-artifact@v4 with: name: myapp-linux-amd64 - name: Make binary executable run: chmod +x myapp-linux-amd64 - name: Run integration tests env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb?sslmode=disable BINARY_PATH: ./myapp-linux-amd64 run: go test -v -tags=integration ./tests/integration/... docker: name: Build Docker Image runs-on: ubuntu-latest needs: [build, test] if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha - name: Build and push 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 build-args: | VERSION=${{ github.sha }} BUILD_TIME=${{ github.event.head_commit.timestamp }} deploy: name: Deploy to Production runs-on: ubuntu-latest needs: [docker, integration-test] if: github.ref == 'refs/heads/main' environment: name: production url: https://api.example.com permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - name: Deploy to Cloud Run run: | gcloud run deploy myapp \ --image ghcr.io/${{ github.repository }}:${{ github.sha }} \ --region us-central1 \ --platform managed \ --allow-unauthenticated \ --memory 512Mi \ --cpu 1 \ --max-instances 10 - name: Health check run: | URL=$(gcloud run services describe myapp --region us-central1 --format 'value(status.url)') for i in {1..10}; do if curl -f $URL/health; then echo "Health check passed" exit 0 fi echo "Attempt $i failed, retrying..." sleep 10 done exit 1 release: name: Create Release runs-on: ubuntu-latest needs: [build] if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write steps: - uses: actions/checkout@v4 - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts/ - name: Create checksums run: | cd artifacts for dir in myapp-*; do cd $dir sha256sum * > checksums.txt cd .. done - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: files: artifacts/**/* generate_release_notes: true draft: false prerelease: false