name: GoChat CI/CD Pipeline # Reference: Chatwoot uses CircleCI for CI/CD; this is the Go equivalent using GitHub Actions # Multi-stage pipeline: test → build → security → deploy on: push: branches: [main, develop, 'release/**'] pull_request: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: gochat/gochat SHANGWUTONG_IMAGE_NAME: gochat/shangwutong GOPROXY: https://goproxy.cn,direct jobs: # ---- Stage 1: Lint + Test ---- test: name: Test & Lint runs-on: ubuntu-latest strategy: matrix: db-mode: [postgres, sqlite] services: postgres: image: pgvector/pgvector:pg16 env: POSTGRES_DB: gochat_test POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd pg_isready --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: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.25' # Lint - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: version: v1.64 working-directory: backend # Vet - name: Run go vet working-directory: backend run: go vet ./... # Unit + Integration Tests - name: Run unit & integration tests working-directory: backend env: GOCHAT_ENV: test GOCHAT_TEST_DB: ${{ matrix.db-mode }} GOCHAT_DATABASE_DSN: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable GOCHAT_REDIS_DSN: redis://localhost:6379 GOPROXY: https://goproxy.cn,direct run: go test -v -race -coverprofile=coverage.out -timeout 180s ./internal/... ./pkg/... ./cmd/... # E2E Tests (only on postgres matrix) - name: Run e2e tests if: matrix.db-mode == 'postgres' working-directory: backend env: GOCHAT_ENV: test GOCHAT_DATABASE_DSN: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable GOCHAT_REDIS_DSN: redis://localhost:6379 run: go test -v -timeout 120s ./tests/e2e/... # Benchmark (quick sanity check, not full bench) - name: Run benchmarks working-directory: backend env: GOCHAT_ENV: test GOCHAT_TEST_DB: ${{ matrix.db-mode }} GOCHAT_DATABASE_DSN: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable run: go test -bench=. -benchtime=1s -run=^$ -timeout 60s ./internal/service/... ./pkg/crypto/... # Coverage report - name: Upload coverage if: matrix.db-mode == 'postgres' uses: codecov/codecov-action@v4 with: files: backend/coverage.out shangwutong: name: Shangwutong Connector runs-on: ubuntu-latest defaults: run: working-directory: channels/shangwutong steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.26.4' cache-dependency-path: channels/shangwutong/go.sum - name: Verify sqlc generation run: go tool sqlc generate && git diff --exit-code -- db/generated - name: Test with race detector run: go test -race ./... - name: Vet and build run: go vet ./... && go build -o /tmp/shangwutong-build-check ./cmd/shangwutong # ---- Stage 2: Security Scan ---- security: name: Security Scan needs: [test, shangwutong] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.25' # Gosec — Go security scanner - name: Run gosec uses: securego/gosec@master with: args: '-no-fail ./...' working-directory: backend # Dependency vulnerability scan - name: Run govulncheck working-directory: backend run: go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./... # Trivy filesystem scan - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'table' exit-code: '1' severity: 'CRITICAL,HIGH' # ---- Stage 3: Build Docker Image ---- build: name: Build Docker Image needs: [test, shangwutong, security] runs-on: ubuntu-latest if: github.event_name == 'push' # Only build on push, not PRs permissions: contents: read packages: write outputs: image_tag: ${{ steps.meta.outputs.tags }} image_digest: ${{ steps.build.outputs.digest }} steps: - uses: actions/checkout@v4 # Set up Docker Buildx for multi-platform builds - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 # Login to GHCR - name: Login to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Extract metadata (tags, labels) - 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 # Build and push main GoChat image - name: Build and push GoChat image id: build uses: docker/build-push-action@v5 with: context: . file: ./deploy/docker/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | VERSION=${{ github.ref_name }} COMMIT_SHA=${{ github.sha }} BUILD_DATE=${{ github.event.head_commit.timestamp }} cache-from: type=gha,scope=gochat cache-to: type=gha,mode=max,scope=gochat - name: Extract Shangwutong connector metadata id: shangwutong_meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.SHANGWUTONG_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 Shangwutong connector image id: shangwutong_build uses: docker/build-push-action@v5 with: context: . file: ./channels/shangwutong/Dockerfile push: true tags: ${{ steps.shangwutong_meta.outputs.tags }} labels: ${{ steps.shangwutong_meta.outputs.labels }} cache-from: type=gha,scope=shangwutong cache-to: type=gha,mode=max,scope=shangwutong # Scan Docker images with Trivy - name: Scan GoChat image uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} format: 'table' exit-code: '1' severity: 'CRITICAL,HIGH' - name: Scan Shangwutong connector image uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.SHANGWUTONG_IMAGE_NAME }}@${{ steps.shangwutong_build.outputs.digest }} format: 'table' exit-code: '1' severity: 'CRITICAL,HIGH'