name: CI/CD on: push: branches: [main, develop] pull_request: branches: [main] release: types: [published] env: GO_VERSION: '1.23' NODE_VERSION: '20' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: # =========================================== # Lint & Format # =========================================== lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Setup Node uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' cache-dependency-path: web/package-lock.json - name: Install golangci-lint uses: golangci/golangci-lint-action@v6 with: version: latest - name: Run Go linters run: | cd ${{ github.workspace }} golangci-lint run ./... - name: Install frontend dependencies run: | cd web npm ci - name: Run frontend linters run: | cd web npm run lint - name: Check formatting run: | cd web npm run format -- --check # =========================================== # Test # =========================================== test: name: Test runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: go_first_test ports: 5432:5432 options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: redis:7-alpine ports: 6379:6379 options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Setup Node uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' cache-dependency-path: web/package-lock.json - name: Install Go dependencies run: go mod download - name: Run Go tests run: | go test -v -race -coverprofile=coverage.out ./... go tool cover -func=coverage.out - name: Upload Go coverage uses: codecov/codecov-action@v4 with: files: coverage.out flags: backend - name: Install frontend dependencies run: | cd web npm ci - name: Run frontend tests run: | cd web npm test -- --run --coverage - name: Upload frontend coverage uses: codecov/codecov-action@v4 with: files: web/coverage/lcov.info flags: frontend # =========================================== # Build # =========================================== build: name: Build runs-on: ubuntu-latest needs: [lint, test] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Setup Node uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' cache-dependency-path: web/package-lock.json - name: Build backend run: | CGO_ENABLED=0 go build -ldflags="-s -w" -o build/go-first ./cmd/server - name: Build frontend run: | cd web npm ci npm run build - name: Upload backend artifact uses: actions/upload-artifact@v4 with: name: go-first-backend path: build/go-first retention-days: 7 - name: Upload frontend artifact uses: actions/upload-artifact@v4 with: name: go-first-frontend path: web/dist retention-days: 7 # =========================================== # Docker Build & Push # =========================================== docker: name: Docker runs-on: ubuntu-latest needs: build if: github.event_name == 'push' || github.event_name == 'release' permissions: contents: read packages: write steps: - name: Checkout 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.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # =========================================== # Deploy Staging # =========================================== deploy-staging: name: Deploy Staging runs-on: ubuntu-latest needs: docker if: github.ref == 'refs/heads/develop' environment: staging steps: - name: Deploy to staging run: | echo "Deploying to staging..." # Add your staging deployment commands here # Example: kubectl apply -k deploy/kubernetes/overlays/staging # Or: ssh deploy@staging-server "cd /opt/go-first && ./deploy/deploy.sh staging" # =========================================== # Deploy Production # =========================================== deploy-production: name: Deploy Production runs-on: ubuntu-latest needs: docker if: github.event_name == 'release' environment: production steps: - name: Deploy to production run: | echo "Deploying to production..." # Add your production deployment commands here # Example: kubectl apply -k deploy/kubernetes/overlays/production # Or: ssh deploy@production-server "cd /opt/go-first && ./deploy/deploy.sh production" # =========================================== # Security Scan # =========================================== security: name: Security Scan runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Run Gosec uses: securego/gosec@master with: args: './...' - name: Run Trivy uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif' - name: Upload Trivy results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif'