266 lines
9.0 KiB
YAML
266 lines
9.0 KiB
YAML
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 }}
|