name: CI on: push: branches: [main, develop] pull_request: branches: [main] concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: # ───────────────────────────────────────────── # Go: build, lint, test # ───────────────────────────────────────────── go: name: Go runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.24" cache: true - name: Build run: go build ./cmd/proxy/ - name: Vet run: go vet ./... - name: Lint uses: golangci/golangci-lint-action@v6 with: version: latest args: --timeout=5m - name: Test run: go test -race -coverprofile=coverage.out ./... - name: Check coverage threshold (>= 80% on internal packages) run: | go test -race -coverprofile=coverage_internal.out -coverpkg=./internal/... ./internal/... COVERAGE=$(go tool cover -func=coverage_internal.out | grep total | awk '{print $3}' | tr -d '%') echo "Internal package coverage: ${COVERAGE}%" awk -v cov="$COVERAGE" 'BEGIN { if (cov+0 < 80) { print "Coverage " cov "% is below 80% threshold"; exit 1 } }' - name: Upload coverage uses: actions/upload-artifact@v4 if: always() with: name: go-coverage path: coverage.out # ───────────────────────────────────────────── # Python: format check, lint, test # ───────────────────────────────────────────── python: name: Python runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip cache-dependency-path: services/pii/requirements.txt - name: Install dependencies run: pip install -r services/pii/requirements.txt - name: Format check (black) run: black --check services/pii/ - name: Lint (ruff) run: ruff check services/pii/ - name: Test with coverage run: | pytest services/pii/ -v --tb=short \ --cov=services/pii \ --cov-report=term-missing \ --ignore=services/pii/tests/test_ner.py \ --cov-fail-under=75 # NER tests excluded in CI: fr_core_news_lg (~600MB) is not downloaded in the CI Python job. # The model is downloaded during Docker build (see Dockerfile) and tested in the security job. # ───────────────────────────────────────────── # Security: secret scanning + container vulnerability scan # ───────────────────────────────────────────── security: name: Security runs-on: ubuntu-latest permissions: contents: read security-events: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Full history required by gitleaks - name: gitleaks — secret scanning uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Semgrep — SAST (E10-04 + E11-11 custom rules) uses: returntocorp/semgrep-action@v1 with: config: >- p/golang p/python p/react p/secrets .semgrep.yml env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} # Non-blocking when SEMGREP_APP_TOKEN is not configured (e.g., forks). continue-on-error: ${{ secrets.SEMGREP_APP_TOKEN == '' }} - name: Build Docker image run: | docker build \ --cache-from type=registry,ref=ghcr.io/${{ github.repository }}/proxy:cache \ -t proxy:${{ github.sha }} \ . - name: Trivy — container vulnerability scan uses: aquasecurity/trivy-action@master with: image-ref: proxy:${{ github.sha }} format: sarif output: trivy-results.sarif exit-code: "1" severity: CRITICAL,HIGH ignore-unfixed: true - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v3 if: always() with: sarif_file: trivy-results.sarif # ───────────────────────────────────────────── # OWASP ZAP DAST — only on push to main (E10-06) # Starts the proxy in dev mode and runs a ZAP baseline scan. # Results are uploaded as a CI artifact (non-blocking). # ───────────────────────────────────────────── zap-dast: name: OWASP ZAP DAST runs-on: ubuntu-latest needs: [go, python, security] if: github.ref == 'refs/heads/main' && github.event_name == 'push' steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.24" cache: true - name: Start proxy (dev mode) run: | VEYLANT_SERVER_ENV=development \ VEYLANT_SERVER_PORT=8090 \ go run ./cmd/proxy/ & env: VEYLANT_SERVER_ENV: development VEYLANT_SERVER_PORT: "8090" - name: Wait for proxy to start run: | for i in $(seq 1 15); do curl -sf http://localhost:8090/healthz && exit 0 sleep 1 done echo "Proxy did not start in time" && exit 1 - name: ZAP Baseline Scan uses: zaproxy/action-baseline@v0.12.0 with: target: 'http://localhost:8090' fail_action: false artifact_name: zap-baseline-report # ───────────────────────────────────────────── # k6 smoke test — run on every push to main # Validates proxy is up and responsive before any deploy. # ───────────────────────────────────────────── load-test: name: k6 Smoke Test runs-on: ubuntu-latest needs: [go] if: github.ref == 'refs/heads/main' && github.event_name == 'push' steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.24" cache: true - name: Install k6 run: | curl -fsSL https://dl.k6.io/key.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/k6.gpg echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update && sudo apt-get install -y k6 - name: Start proxy (dev mode) run: go run ./cmd/proxy/ & env: VEYLANT_SERVER_ENV: development VEYLANT_SERVER_PORT: "8090" - name: Wait for proxy run: | for i in $(seq 1 20); do curl -sf http://localhost:8090/healthz && break sleep 1 done - name: k6 smoke scenario run: | k6 run \ --env VEYLANT_URL=http://localhost:8090 \ --env VEYLANT_TOKEN=dev-token \ --env SCENARIO=smoke \ test/k6/k6-load-test.js # ───────────────────────────────────────────── # Deploy to staging — only on push to main # Uses blue/green deployment for zero-downtime and instant rollback (< 30s). # Manual rollback: make deploy-rollback NAMESPACE=veylant ACTIVE_SLOT=blue # ───────────────────────────────────────────── deploy-staging: name: Deploy (staging — blue/green) runs-on: ubuntu-latest needs: [go, python, security, load-test] if: github.ref == 'refs/heads/main' && github.event_name == 'push' environment: staging steps: - uses: actions/checkout@v4 - name: Set up Helm uses: azure/setup-helm@v4 with: version: v3.16.0 - name: Configure kubectl run: | mkdir -p ~/.kube echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config chmod 600 ~/.kube/config - name: Blue/green deploy run: | chmod +x deploy/scripts/blue-green.sh ./deploy/scripts/blue-green.sh env: IMAGE_TAG: ${{ github.sha }} NAMESPACE: veylant VEYLANT_URL: ${{ secrets.STAGING_VEYLANT_URL }}