diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml new file mode 100644 index 000000000..fe0b2950a --- /dev/null +++ b/.github/workflows/accessibility.yml @@ -0,0 +1,194 @@ +name: Accessibility + +on: + pull_request: + branches: [main] + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + # =========================================================================== + # Job 1: axe-playwright — WCAG AA violations via @axe-core/playwright + # =========================================================================== + axe-playwright: + name: axe-playwright + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json + + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache-dependency-path: veza-backend-api/go.sum + + - name: Install dependencies + run: npm ci + + - name: Add veza.fr to hosts + run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts + + - name: Start backend services (Postgres, Redis, RabbitMQ) + run: | + docker-compose up -d postgres redis rabbitmq + echo "Waiting for Postgres..." + for i in $(seq 1 30); do + if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then + echo "Postgres ready" + break + fi + sleep 2 + done + docker-compose ps + + - name: Run database migrations + env: + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + run: | + cd veza-backend-api + go run cmd/migrate_tool/main.go + + - name: Start backend API + env: + APP_ENV: development + APP_PORT: "18080" + DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable + REDIS_URL: redis://localhost:16379 + JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }} + COOKIE_SECURE: "false" + CORS_ALLOWED_ORIGINS: http://veza.fr:5174,http://localhost:5174 + RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }} + DISABLE_RATE_LIMIT_FOR_TESTS: "true" + run: | + cd veza-backend-api + go build -o veza-api ./cmd/api/main.go + ./veza-api & + sleep 10 + curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1) + jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1) + echo "Health check OK" + + - name: Install Playwright Browsers (chromium only) + run: npx playwright install --with-deps chromium + + - name: Run @a11y E2E tests + run: npx playwright test --config=tests/e2e/playwright.config.ts --grep @a11y --project=chromium + env: + PORT: "5174" + VITE_API_URL: "/api/v1" + VITE_DOMAIN: veza.fr + VITE_BACKEND_PORT: "18080" + PLAYWRIGHT_BASE_URL: "http://localhost:5174" + + - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + if: failure() + with: + name: axe-playwright-report + path: | + tests/e2e/playwright-report/ + tests/e2e/test-results/ + retention-days: 7 + + # =========================================================================== + # Job 2: lighthouse-ci — Performance & Accessibility scoring + # =========================================================================== + lighthouse-ci: + name: lighthouse-ci + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json + + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache-dependency-path: veza-backend-api/go.sum + + - name: Install dependencies + run: npm ci + + - name: Add veza.fr to hosts + run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts + + - name: Start backend services (Postgres, Redis, RabbitMQ) + run: | + docker-compose up -d postgres redis rabbitmq + echo "Waiting for Postgres..." + for i in $(seq 1 30); do + if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then + echo "Postgres ready" + break + fi + sleep 2 + done + docker-compose ps + + - name: Run database migrations + env: + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + run: | + cd veza-backend-api + go run cmd/migrate_tool/main.go + + - name: Start backend API + env: + APP_ENV: development + APP_PORT: "18080" + DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable + REDIS_URL: redis://localhost:16379 + JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }} + COOKIE_SECURE: "false" + CORS_ALLOWED_ORIGINS: http://veza.fr:5174,http://localhost:5174 + RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }} + DISABLE_RATE_LIMIT_FOR_TESTS: "true" + run: | + cd veza-backend-api + go build -o veza-api ./cmd/api/main.go + ./veza-api & + sleep 10 + curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1) + jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1) + echo "Health check OK" + + - name: Start Vite dev server + run: | + cd apps/web + npm run dev -- --host 127.0.0.1 --port 5174 & + echo "Waiting for Vite dev server..." + for i in $(seq 1 30); do + if curl -sf http://localhost:5174 >/dev/null 2>&1; then + echo "Vite dev server ready" + break + fi + sleep 2 + done + curl -sf http://localhost:5174 >/dev/null || (echo "Vite dev server failed to start"; exit 1) + env: + VITE_API_URL: "/api/v1" + VITE_DOMAIN: veza.fr + VITE_BACKEND_PORT: "18080" + + - name: Run Lighthouse CI + uses: treosh/lighthouse-ci-action@v12 + with: + configPath: ./.lighthouserc.js + uploadArtifacts: true + temporaryPublicStorage: true diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index dbbc0b197..929a38d8c 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -1,153 +1,157 @@ name: Backend API CI on: - push: - paths: - - "veza-backend-api/**" - - ".github/workflows/backend-ci.yml" - pull_request: - paths: - - "veza-backend-api/**" - - ".github/workflows/backend-ci.yml" + push: + paths: + - "veza-backend-api/**" + - ".github/workflows/backend-ci.yml" + pull_request: + paths: + - "veza-backend-api/**" + - ".github/workflows/backend-ci.yml" + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - test-unit: - runs-on: ubuntu-latest + test-unit: + runs-on: ubuntu-latest - defaults: - run: - working-directory: veza-backend-api + defaults: + run: + working-directory: veza-backend-api - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: "1.24" - cache: true + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache: true - - name: Download deps - run: go mod download + - name: Download deps + run: go mod download - - name: Go vet and format check - run: | - go vet ./... - test -z "$(gofmt -l .)" - working-directory: veza-backend-api + - name: Go vet and format check + run: | + go vet ./... + test -z "$(gofmt -l .)" + working-directory: veza-backend-api - - name: Run govulncheck - run: | - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck ./... + - name: Run govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... - - name: Run unit tests with coverage - run: > - go test - ./internal/handlers/... - ./internal/services/... - ./internal/core/... - ./internal/middleware/... - -short -coverprofile=coverage.out -covermode=atomic -timeout 5m + - name: Run unit tests with coverage + run: > + go test + ./internal/handlers/... + ./internal/services/... + ./internal/core/... + ./internal/middleware/... + -short -coverprofile=coverage.out -covermode=atomic -timeout 5m - - name: Enforce coverage threshold (>= 70%) - run: | - COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%') - echo "Total coverage: ${COVERAGE}%" - if [ -z "$COVERAGE" ]; then - echo "::warning::Could not parse coverage percentage" - exit 0 - fi - # Compare as integers (remove decimal) - COV_INT=$(echo "$COVERAGE" | cut -d. -f1) - if [ "$COV_INT" -lt 70 ]; then - echo "::error::Coverage ${COVERAGE}% is below the 70% threshold" - exit 1 - fi - echo "::notice::Coverage ${COVERAGE}% meets the >= 70% threshold" + - name: Enforce coverage threshold (>= 70%) + run: | + COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%') + echo "Total coverage: ${COVERAGE}%" + if [ -z "$COVERAGE" ]; then + echo "::warning::Could not parse coverage percentage" + exit 0 + fi + # Compare as integers (remove decimal) + COV_INT=$(echo "$COVERAGE" | cut -d. -f1) + if [ "$COV_INT" -lt 75 ]; then + echo "::error::Coverage ${COVERAGE}% is below the 75% threshold" + exit 1 + fi + echo "::notice::Coverage ${COVERAGE}% meets the >= 75% threshold" - - name: Upload coverage report - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: go-coverage - path: veza-backend-api/coverage.out + - name: Upload coverage report + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: go-coverage + path: veza-backend-api/coverage.out - - name: Generate coverage badge - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: | - COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%') - COV_INT=$(echo "$COVERAGE" | cut -d. -f1) - if [ "$COV_INT" -ge 80 ]; then - COLOR="brightgreen" - elif [ "$COV_INT" -ge 70 ]; then - COLOR="green" - elif [ "$COV_INT" -ge 50 ]; then - COLOR="yellow" - else - COLOR="red" - fi - echo "{\"schemaVersion\":1,\"label\":\"Go coverage\",\"message\":\"${COVERAGE}%\",\"color\":\"${COLOR}\"}" > coverage-badge.json - echo "Coverage badge: ${COVERAGE}% (${COLOR})" + - name: Generate coverage badge + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: | + COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%') + COV_INT=$(echo "$COVERAGE" | cut -d. -f1) + if [ "$COV_INT" -ge 80 ]; then + COLOR="brightgreen" + elif [ "$COV_INT" -ge 70 ]; then + COLOR="green" + elif [ "$COV_INT" -ge 50 ]; then + COLOR="yellow" + else + COLOR="red" + fi + echo "{\"schemaVersion\":1,\"label\":\"Go coverage\",\"message\":\"${COVERAGE}%\",\"color\":\"${COLOR}\"}" > coverage-badge.json + echo "Coverage badge: ${COVERAGE}% (${COLOR})" - - name: Upload coverage badge - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: go-coverage-badge - path: veza-backend-api/coverage-badge.json + - name: Upload coverage badge + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: go-coverage-badge + path: veza-backend-api/coverage-badge.json - test-integration: - runs-on: ubuntu-latest + test-integration: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: veza_test + 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 - services: - postgres: - image: postgres:16-alpine env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: veza_test - 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 + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/veza_test?sslmode=disable + REDIS_URL: redis://localhost:6379 + JWT_SECRET: test-jwt-secret-for-ci + APP_ENV: test - env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/veza_test?sslmode=disable - REDIS_URL: redis://localhost:6379 - JWT_SECRET: test-jwt-secret-for-ci - APP_ENV: test + defaults: + run: + working-directory: veza-backend-api - defaults: - run: - working-directory: veza-backend-api + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache: true - - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: "1.24" - cache: true + - name: Download deps + run: go mod download - - name: Download deps - run: go mod download + - name: Run migrations + run: go run cmd/migrate_tool/main.go + continue-on-error: true - - name: Run migrations - run: go run cmd/migrate_tool/main.go - continue-on-error: true - - - name: Run integration tests - run: go test -tags=integration ./internal/... -timeout 15m + - name: Run integration tests + run: go test -tags=integration ./internal/... -timeout 15m diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 42b60a82d..8d9893c5e 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,165 +1,168 @@ name: Veza CD on: - push: - branches: [ "main" ] - workflow_dispatch: - inputs: - environment: - description: 'Deployment environment' - required: true - default: 'staging' - type: choice - options: - - staging - - production + push: + branches: ["main"] + workflow_dispatch: + inputs: + environment: + description: "Deployment environment" + required: true + default: "staging" + type: choice + options: + - staging + - production + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - build: - name: Build and push images - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + build: + name: Build and push images + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - # Push to registry: set repo secrets DOCKER_REGISTRY, DOCKER_REGISTRY_USERNAME, DOCKER_REGISTRY_PASSWORD - # Example: DOCKER_REGISTRY=ghcr.io/org/repo or registry.example.com/veza - - name: Build Backend Docker Image - run: | - docker build -t veza-backend-api:${{ github.sha }} -f veza-backend-api/Dockerfile.production veza-backend-api/ + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - name: Build Frontend Docker Image - run: | - docker build -t veza-frontend:${{ github.sha }} -f apps/web/Dockerfile.production apps/web/ + # Push to registry: set repo secrets DOCKER_REGISTRY, DOCKER_REGISTRY_USERNAME, DOCKER_REGISTRY_PASSWORD + # Example: DOCKER_REGISTRY=ghcr.io/org/repo or registry.example.com/veza + - name: Build Backend Docker Image + run: | + docker build -t veza-backend-api:${{ github.sha }} -f veza-backend-api/Dockerfile.production veza-backend-api/ - - name: Build Stream Server Docker Image - run: | - docker build -t veza-stream-server:${{ github.sha }} -f veza-stream-server/Dockerfile.production veza-stream-server/ + - name: Build Frontend Docker Image + run: | + docker build -t veza-frontend:${{ github.sha }} -f apps/web/Dockerfile.production apps/web/ - - name: Trivy vulnerability scan - uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 - with: - image-ref: 'veza-backend-api:${{ github.sha }}' - format: 'table' - exit-code: '1' - severity: 'CRITICAL,HIGH' + - name: Build Stream Server Docker Image + run: | + docker build -t veza-stream-server:${{ github.sha }} -f veza-stream-server/Dockerfile.production veza-stream-server/ - - name: Trivy scan frontend - uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 - with: - image-ref: 'veza-frontend:${{ github.sha }}' - format: 'table' - exit-code: '1' - severity: 'CRITICAL,HIGH' + - name: Trivy vulnerability scan + uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 + with: + image-ref: "veza-backend-api:${{ github.sha }}" + format: "table" + exit-code: "1" + severity: "CRITICAL,HIGH" - - name: Trivy scan stream server - uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 - with: - image-ref: 'veza-stream-server:${{ github.sha }}' - format: 'table' - exit-code: '1' - severity: 'CRITICAL,HIGH' + - name: Trivy scan frontend + uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 + with: + image-ref: "veza-frontend:${{ github.sha }}" + format: "table" + exit-code: "1" + severity: "CRITICAL,HIGH" - - name: Generate SBOM - run: | - mkdir -p sbom - for svc in veza-backend-api veza-frontend veza-stream-server; do - trivy image --format cyclonedx --output "sbom/${svc}-${{ github.sha }}.json" "${svc}:${{ github.sha }}" - done - - name: Upload SBOM artifacts - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: sbom - path: sbom/ + - name: Trivy scan stream server + uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 + with: + image-ref: "veza-stream-server:${{ github.sha }}" + format: "table" + exit-code: "1" + severity: "CRITICAL,HIGH" - - name: Push Images to Registry - if: vars.DOCKER_REGISTRY != '' - run: | - echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin - for svc in veza-backend-api veza-frontend veza-stream-server; do - docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" - docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:latest" - docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" - docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:latest" - done + - name: Generate SBOM + run: | + mkdir -p sbom + for svc in veza-backend-api veza-frontend veza-stream-server; do + trivy image --format cyclonedx --output "sbom/${svc}-${{ github.sha }}.json" "${svc}:${{ github.sha }}" + done + - name: Upload SBOM artifacts + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: sbom + path: sbom/ - - name: Install cosign - if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true' - uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 - with: - cosign-release: 'v2.2.0' - - name: Sign images with cosign - if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true' - env: - COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} - COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} - run: | - for svc in veza-backend-api veza-frontend veza-stream-server; do - cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" - cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:latest" - done + - name: Push Images to Registry + if: vars.DOCKER_REGISTRY != '' + run: | + echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin + for svc in veza-backend-api veza-frontend veza-stream-server; do + docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" + docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:latest" + docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" + docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:latest" + done - - name: Build Summary - run: | - echo "## Build Summary" >> $GITHUB_STEP_SUMMARY - echo "- Backend: veza-backend-api:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "- Frontend: veza-frontend:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "- Stream Server: veza-stream-server:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + - name: Install cosign + if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true' + uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 + with: + cosign-release: "v2.2.0" + - name: Sign images with cosign + if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true' + env: + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + run: | + for svc in veza-backend-api veza-frontend veza-stream-server; do + cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" + cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:latest" + done - deploy: - name: Deploy to ${{ github.event.inputs.environment || 'staging' }} - runs-on: ubuntu-latest - needs: build - if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' - environment: ${{ github.event.inputs.environment || 'staging' }} - steps: - - name: Deploy to Kubernetes - if: vars.KUBE_CONFIG_SET == 'true' - run: | - KUBECONFIG="${{ runner.temp }}/kubeconfig" - echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG" - chmod 600 "$KUBECONFIG" - export KUBECONFIG - for svc in veza-backend-api veza-stream-server; do - kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" \ - -n veza --record || echo "Skipping ${svc} (deployment not found)" - done - kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true - rm -f "$KUBECONFIG" + - name: Build Summary + run: | + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY + echo "- Backend: veza-backend-api:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- Frontend: veza-frontend:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- Stream Server: veza-stream-server:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - - name: Deployment Summary - run: | - echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY - echo "- Environment: ${{ github.event.inputs.environment || 'staging' }}" >> $GITHUB_STEP_SUMMARY + deploy: + name: Deploy to ${{ github.event.inputs.environment || 'staging' }} + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + environment: ${{ github.event.inputs.environment || 'staging' }} + steps: + - name: Deploy to Kubernetes + if: vars.KUBE_CONFIG_SET == 'true' + run: | + KUBECONFIG="${{ runner.temp }}/kubeconfig" + echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG" + chmod 600 "$KUBECONFIG" + export KUBECONFIG + for svc in veza-backend-api veza-stream-server; do + kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" \ + -n veza --record || echo "Skipping ${svc} (deployment not found)" + done + kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true + rm -f "$KUBECONFIG" - smoke-post-deploy: - name: Smoke tests post-deploy - runs-on: ubuntu-latest - needs: deploy - if: vars.STAGING_URL != '' - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Deployment Summary + run: | + echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "- Environment: ${{ github.event.inputs.environment || 'staging' }}" >> $GITHUB_STEP_SUMMARY - - name: Set up Node - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: '20' - cache: 'npm' + smoke-post-deploy: + name: Smoke tests post-deploy + runs-on: ubuntu-latest + needs: deploy + if: vars.STAGING_URL != '' + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Install dependencies - run: npm ci + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" - - name: Install Playwright - run: npx playwright install chromium --with-deps + - name: Install dependencies + run: npm ci - - name: Run smoke tests - env: - PLAYWRIGHT_BASE_URL: ${{ vars.STAGING_URL }} - run: | - cd apps/web - npx playwright test --config=playwright.config.smoke.ts + - name: Install Playwright + run: npx playwright install chromium --with-deps + - name: Run smoke tests + env: + PLAYWRIGHT_BASE_URL: ${{ vars.STAGING_URL }} + run: | + cd apps/web + npx playwright test --config=playwright.config.smoke.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 222ed401d..17234ee18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,270 +1,387 @@ name: Veza CI/CD on: - push: - branches: [ "main", "remediation/*", "feature/mvp-complete" ] - pull_request: - branches: [ "main", "feature/mvp-complete" ] - workflow_dispatch: + push: + branches: ["main", "remediation/*", "feature/mvp-complete"] + pull_request: + branches: ["main", "feature/mvp-complete"] + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - # =========================================================================== - # TMT Vital — Backend (Go) - # =========================================================================== - vital-backend: - name: TMT Vital — Backend (Go) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 + # =========================================================================== + # TMT Vital — Backend (Go) + # =========================================================================== + vital-backend: + name: TMT Vital — Backend (Go) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - - name: Check VERSION matches git tag - run: | - current_tag=$(git describe --tags --exact-match 2>/dev/null || true) - if [ -n "$current_tag" ]; then - version_file=$(cat VERSION) - tag_version=${current_tag#v} - if [ "$version_file" != "$tag_version" ]; then - echo "VERSION mismatch: VERSION=$version_file, current tag=$current_tag" - exit 1 - fi - fi + - name: Check VERSION matches git tag + run: | + current_tag=$(git describe --tags --exact-match 2>/dev/null || true) + if [ -n "$current_tag" ]; then + version_file=$(cat VERSION) + tag_version=${current_tag#v} + if [ "$version_file" != "$tag_version" ]; then + echo "VERSION mismatch: VERSION=$version_file, current tag=$current_tag" + exit 1 + fi + fi - - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: '1.24' - cache: true + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache: true - - name: Install Go tools - run: | - go install golang.org/x/vuln/cmd/govulncheck@latest - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + - name: Install Go tools + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - name: Install TMT - run: pip install tmt + - name: Install TMT + run: pip install tmt - - name: Run TMT Vital Backend - run: tmt --root tmt run plan --name /vital-backend + - name: Run TMT Vital Backend + run: tmt --root tmt run plan --name /vital-backend - # =========================================================================== - # TMT Vital — Rust Services (Stream) - # =========================================================================== - vital-services: - name: TMT Vital — Rust Services - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # =========================================================================== + # TMT Vital — Rust Services (Stream) + # =========================================================================== + vital-services: + name: TMT Vital — Rust Services + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Rust - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - with: - components: rustfmt, clippy + - name: Set up Rust + uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + components: rustfmt, clippy - - name: Cache Cargo registry - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Cache Cargo registry + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install cargo-audit - run: cargo install cargo-audit + - name: Install cargo-audit + run: cargo install cargo-audit - - name: Install TMT - run: pip install tmt + - name: Install TMT + run: pip install tmt - - name: Run TMT Vital Services - run: tmt --root tmt run plan --name /vital-services + - name: Run TMT Vital Services + run: tmt --root tmt run plan --name /vital-services - # =========================================================================== - # TMT Vital — Frontend (Web) - # =========================================================================== - vital-frontend: - name: TMT Vital — Frontend (Web) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # =========================================================================== + # TMT Vital — Frontend (Web) + # =========================================================================== + vital-frontend: + name: TMT Vital — Frontend (Web) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Use Node.js - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: package-lock.json + - name: Use Node.js + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json - - name: Install Dependencies - run: npm ci + - name: Install Dependencies + run: npm ci - - name: Cache Generated Types - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: apps/web/src/types/generated - key: ${{ runner.os }}-generated-types-${{ hashFiles('veza-backend-api/openapi.yaml') }} - restore-keys: | - ${{ runner.os }}-generated-types- + - name: Cache Generated Types + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: apps/web/src/types/generated + key: ${{ runner.os }}-generated-types-${{ hashFiles('veza-backend-api/openapi.yaml') }} + restore-keys: | + ${{ runner.os }}-generated-types- - - name: Install TMT - run: pip install tmt + - name: Install TMT + run: pip install tmt - - name: Run TMT Vital Frontend - run: tmt --root tmt run plan --name /vital-frontend + - name: Run TMT Vital Frontend + run: tmt --root tmt run plan --name /vital-frontend - # =========================================================================== - # Storybook Audit (kept outside TMT — tier 3 candidate) - # =========================================================================== - storybook: - name: Storybook Audit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # =========================================================================== + # Storybook Audit (kept outside TMT — tier 3 candidate) + # =========================================================================== + storybook: + name: Storybook Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Node - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: package-lock.json + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Build Storybook - run: npm run build-storybook - working-directory: apps/web + - name: Build Storybook + run: npm run build-storybook + working-directory: apps/web - - name: Serve Storybook and run audit - run: | - npx serve -s storybook-static -l 6007 & - for i in $(seq 1 30); do - if curl -sf http://localhost:6007 >/dev/null; then - echo "Storybook ready" - break - fi - sleep 2 - done - curl -sf http://localhost:6007 >/dev/null || (echo "Storybook failed to start"; exit 1) - npm run test:storybook - working-directory: apps/web + - name: Serve Storybook and run audit + run: | + npx serve -s storybook-static -l 6007 & + for i in $(seq 1 30); do + if curl -sf http://localhost:6007 >/dev/null; then + echo "Storybook ready" + break + fi + sleep 2 + done + curl -sf http://localhost:6007 >/dev/null || (echo "Storybook failed to start"; exit 1) + npm run test:storybook + working-directory: apps/web - # =========================================================================== - # E2E (Playwright) — kept outside TMT (complex infra setup) - # =========================================================================== - e2e: - name: E2E (Playwright) - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # =========================================================================== + # E2E Critical (Playwright) — fast smoke, blocks PR + # =========================================================================== + e2e-critical: + name: E2E Critical (@critical) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Node - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: package-lock.json + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json - - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: '1.24' - cache-dependency-path: veza-backend-api/go.sum + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache-dependency-path: veza-backend-api/go.sum - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Add veza.fr to hosts (for Vite proxy) - run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts + - name: Add veza.fr to hosts (for Vite proxy) + run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts - - name: Start backend services (Postgres, Redis, RabbitMQ) - run: | - docker-compose up -d postgres redis rabbitmq - echo "Waiting for Postgres..." - for i in $(seq 1 30); do - if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then - echo "Postgres ready" - break - fi - sleep 2 - done - docker-compose ps + - name: Start backend services (Postgres, Redis, RabbitMQ) + run: | + docker-compose up -d postgres redis rabbitmq + echo "Waiting for Postgres..." + for i in $(seq 1 30); do + if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then + echo "Postgres ready" + break + fi + sleep 2 + done + docker-compose ps - - name: Run database migrations - env: - DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable - run: | - cd veza-backend-api - go run cmd/migrate_tool/main.go + - name: Run database migrations + env: + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + run: | + cd veza-backend-api + go run cmd/migrate_tool/main.go - - name: Create E2E test user - env: - DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable - TEST_EMAIL: e2e@test.com - TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} - TEST_USERNAME: e2e - run: | - cd veza-backend-api - go run cmd/tools/create_test_user/main.go + - name: Create E2E test user + env: + DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable + TEST_EMAIL: e2e@test.com + TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} + TEST_USERNAME: e2e + run: | + cd veza-backend-api + go run cmd/tools/create_test_user/main.go - - name: Start backend API - env: - APP_ENV: development - APP_PORT: "18080" - DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable - REDIS_URL: redis://localhost:16379 - JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }} - COOKIE_SECURE: "false" - CORS_ALLOWED_ORIGINS: http://veza.fr:5173,http://veza.fr:5174,http://localhost:5173,http://localhost:5174 - RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }} - DISABLE_RATE_LIMIT_FOR_TESTS: "true" - ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "e2e@test.com" - run: | - cd veza-backend-api - go build -o veza-api ./cmd/api/main.go - ./veza-api & - sleep 10 - curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1) - jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1) - echo "Health check OK (status, DB/Redis connectivity verified)" + - name: Start backend API + env: + APP_ENV: development + APP_PORT: "18080" + DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable + REDIS_URL: redis://localhost:16379 + JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }} + COOKIE_SECURE: "false" + CORS_ALLOWED_ORIGINS: http://veza.fr:5173,http://veza.fr:5174,http://localhost:5173,http://localhost:5174 + RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }} + DISABLE_RATE_LIMIT_FOR_TESTS: "true" + ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "e2e@test.com" + run: | + cd veza-backend-api + go build -o veza-api ./cmd/api/main.go + ./veza-api & + sleep 10 + curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1) + jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1) + echo "Health check OK (status, DB/Redis connectivity verified)" - - name: Install Playwright Browsers - run: npx playwright install --with-deps - working-directory: apps/web + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium - - name: Run E2E tests - run: npx playwright test - working-directory: apps/web - env: - PORT: "5174" - VITE_API_URL: '/api/v1' - VITE_DOMAIN: veza.fr - VITE_BACKEND_PORT: "18080" - PLAYWRIGHT_BASE_URL: 'http://localhost:5174' - TEST_EMAIL: e2e@test.com - TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} + - name: Run @critical E2E tests + run: npx playwright test --config=tests/e2e/playwright.config.ts --grep @critical --project=chromium + env: + PORT: "5174" + VITE_API_URL: "/api/v1" + VITE_DOMAIN: veza.fr + VITE_BACKEND_PORT: "18080" + PLAYWRIGHT_BASE_URL: "http://localhost:5174" + TEST_EMAIL: e2e@test.com + TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + if: failure() + with: + name: playwright-critical-report + path: tests/e2e/playwright-report/ + retention-days: 7 + + # =========================================================================== + # E2E Full (Playwright) — sharded across 4 runners, all browsers + # =========================================================================== + e2e-full: + name: E2E Full (shard ${{ matrix.shard }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + shard: [1/4, 2/4, 3/4, 4/4] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json + + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache-dependency-path: veza-backend-api/go.sum + + - name: Install dependencies + run: npm ci + + - name: Add veza.fr to hosts (for Vite proxy) + run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts + + - name: Start backend services (Postgres, Redis, RabbitMQ) + run: | + docker-compose up -d postgres redis rabbitmq + echo "Waiting for Postgres..." + for i in $(seq 1 30); do + if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then + echo "Postgres ready" + break + fi + sleep 2 + done + docker-compose ps + + - name: Run database migrations + env: + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + run: | + cd veza-backend-api + go run cmd/migrate_tool/main.go + + - name: Create E2E test user + env: + DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable + TEST_EMAIL: e2e@test.com + TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} + TEST_USERNAME: e2e + run: | + cd veza-backend-api + go run cmd/tools/create_test_user/main.go + + - name: Start backend API + env: + APP_ENV: development + APP_PORT: "18080" + DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable + REDIS_URL: redis://localhost:16379 + JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }} + COOKIE_SECURE: "false" + CORS_ALLOWED_ORIGINS: http://veza.fr:5173,http://veza.fr:5174,http://localhost:5173,http://localhost:5174 + RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }} + DISABLE_RATE_LIMIT_FOR_TESTS: "true" + ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "e2e@test.com" + run: | + cd veza-backend-api + go build -o veza-api ./cmd/api/main.go + ./veza-api & + sleep 10 + curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1) + jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1) + echo "Health check OK (status, DB/Redis connectivity verified)" + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run E2E tests (sharded) + run: npx playwright test --config=tests/e2e/playwright.config.ts --shard=${{ matrix.shard }} + env: + PORT: "5174" + VITE_API_URL: "/api/v1" + VITE_DOMAIN: veza.fr + VITE_BACKEND_PORT: "18080" + PLAYWRIGHT_BASE_URL: "http://localhost:5174" + TEST_EMAIL: e2e@test.com + TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} + + - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + if: always() + with: + name: playwright-report-shard-${{ strategy.job-index }} + path: | + tests/e2e/playwright-report/ + tests/e2e/test-results/ + retention-days: 7 + + # =========================================================================== + # Notify on failure + # =========================================================================== + notify-failure: + name: Notify on failure + needs: + [ + vital-backend, + vital-services, + vital-frontend, + storybook, + e2e-critical, + e2e-full, + ] if: failure() - with: - name: playwright-report - path: apps/web/playwright-report/ - retention-days: 7 - - # =========================================================================== - # Notify on failure - # =========================================================================== - notify-failure: - name: Notify on failure - needs: [vital-backend, vital-services, vital-frontend, storybook, e2e] - if: failure() - runs-on: ubuntu-latest - steps: - - name: Slack notification - if: secrets.SLACK_WEBHOOK_URL != '' - run: | - curl -X POST -H 'Content-type: application/json' \ - --data "{\"text\":\"CI failed on ${{ github.repository }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ - "${{ secrets.SLACK_WEBHOOK_URL }}" + runs-on: ubuntu-latest + steps: + - name: Slack notification + if: secrets.SLACK_WEBHOOK_URL != '' + run: | + curl -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"CI failed on ${{ github.repository }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ + "${{ secrets.SLACK_WEBHOOK_URL }}" diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 000000000..c8a66e41f --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,36 @@ +name: Commit Lint + +on: + pull_request: + branches: [main, "feature/**"] + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + commitlint: + name: Validate commit messages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json + + - name: Install commitlint + run: npm ci --ignore-scripts + + - name: Validate PR commit messages + run: | + # Validate all commits in the PR + npx commitlint \ + --from=${{ github.event.pull_request.base.sha }} \ + --to=${{ github.event.pull_request.head.sha }} \ + --verbose diff --git a/.github/workflows/container-scan.yml b/.github/workflows/container-scan.yml index bed972930..02665b644 100644 --- a/.github/workflows/container-scan.yml +++ b/.github/workflows/container-scan.yml @@ -1,84 +1,88 @@ name: Container Image Scan on: - push: - branches: [main] - paths: - - 'veza-backend-api/Dockerfile*' - - 'apps/web/Dockerfile*' - - 'veza-stream-server/Dockerfile*' - pull_request: - branches: [main] - paths: - - 'veza-backend-api/Dockerfile*' - - 'apps/web/Dockerfile*' - - 'veza-stream-server/Dockerfile*' - workflow_dispatch: + push: + branches: [main] + paths: + - "veza-backend-api/Dockerfile*" + - "apps/web/Dockerfile*" + - "veza-stream-server/Dockerfile*" + pull_request: + branches: [main] + paths: + - "veza-backend-api/Dockerfile*" + - "apps/web/Dockerfile*" + - "veza-stream-server/Dockerfile*" + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - scan-backend: - name: Scan Backend Image - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + scan-backend: + name: Scan Backend Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Build backend image - run: docker build -t veza-backend:scan -f veza-backend-api/Dockerfile.production veza-backend-api/ + - name: Build backend image + run: docker build -t veza-backend:scan -f veza-backend-api/Dockerfile.production veza-backend-api/ - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 - with: - image-ref: 'veza-backend:scan' - format: 'table' - exit-code: '1' - severity: 'CRITICAL,HIGH' - ignore-unfixed: true + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 + with: + image-ref: "veza-backend:scan" + format: "table" + exit-code: "1" + severity: "CRITICAL,HIGH" + ignore-unfixed: true - scan-stream-server: - name: Scan Stream Server Image - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + scan-stream-server: + name: Scan Stream Server Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Build stream server image - run: docker build -t veza-stream:scan -f veza-stream-server/Dockerfile . + - name: Build stream server image + run: docker build -t veza-stream:scan -f veza-stream-server/Dockerfile . - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 - with: - image-ref: 'veza-stream:scan' - format: 'table' - exit-code: '1' - severity: 'CRITICAL,HIGH' - ignore-unfixed: true + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 + with: + image-ref: "veza-stream:scan" + format: "table" + exit-code: "1" + severity: "CRITICAL,HIGH" + ignore-unfixed: true - scan-frontend: - name: Scan Frontend Image - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + scan-frontend: + name: Scan Frontend Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Check if frontend Dockerfile exists - id: check - run: | - if [ -f "apps/web/Dockerfile" ] || [ -f "apps/web/Dockerfile.production" ]; then - echo "exists=true" >> $GITHUB_OUTPUT - else - echo "exists=false" >> $GITHUB_OUTPUT - fi + - name: Check if frontend Dockerfile exists + id: check + run: | + if [ -f "apps/web/Dockerfile" ] || [ -f "apps/web/Dockerfile.production" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi - - name: Build frontend image - if: steps.check.outputs.exists == 'true' - run: | - DOCKERFILE=$([ -f "apps/web/Dockerfile.production" ] && echo "apps/web/Dockerfile.production" || echo "apps/web/Dockerfile") - docker build -t veza-frontend:scan -f "$DOCKERFILE" apps/web/ + - name: Build frontend image + if: steps.check.outputs.exists == 'true' + run: | + DOCKERFILE=$([ -f "apps/web/Dockerfile.production" ] && echo "apps/web/Dockerfile.production" || echo "apps/web/Dockerfile") + docker build -t veza-frontend:scan -f "$DOCKERFILE" apps/web/ - - name: Run Trivy vulnerability scanner - if: steps.check.outputs.exists == 'true' - uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 - with: - image-ref: 'veza-frontend:scan' - format: 'table' - exit-code: '1' - severity: 'CRITICAL,HIGH' - ignore-unfixed: true + - name: Run Trivy vulnerability scanner + if: steps.check.outputs.exists == 'true' + uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0 + with: + image-ref: "veza-frontend:scan" + format: "table" + exit-code: "1" + severity: "CRITICAL,HIGH" + ignore-unfixed: true diff --git a/.github/workflows/contract-testing.yml b/.github/workflows/contract-testing.yml new file mode 100644 index 000000000..e1d479160 --- /dev/null +++ b/.github/workflows/contract-testing.yml @@ -0,0 +1,101 @@ +name: Contract Testing (Schemathesis) + +on: + pull_request: + paths: + - "veza-backend-api/**.go" + - "veza-backend-api/openapi.yaml" + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + contract-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: veza_test + 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 + + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/veza_test?sslmode=disable + REDIS_URL: redis://localhost:6379 + JWT_SECRET: test-jwt-secret-for-ci + APP_ENV: test + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache: true + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install schemathesis + run: pip install schemathesis + + - name: Download Go deps + run: cd veza-backend-api && go mod download + + - name: Run migrations + run: cd veza-backend-api && go run cmd/migrate_tool/main.go + continue-on-error: true + + - name: Start backend API + run: | + cd veza-backend-api && go run cmd/api/main.go & + # Wait for API to be ready + for i in $(seq 1 30); do + if curl -sf http://localhost:18080/api/v1/health > /dev/null 2>&1; then + echo "API is ready" + break + fi + echo "Waiting for API... ($i/30)" + sleep 2 + done + + - name: Run schemathesis contract tests + run: > + st run + --checks all + veza-backend-api/openapi.yaml + --base-url http://localhost:18080 + --hypothesis-max-examples=50 + --request-timeout=10000 + continue-on-error: true + + - name: Upload schemathesis report + if: always() + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: schemathesis-report + path: .schemathesis/ + retention-days: 14 diff --git a/.github/workflows/flaky-report.yml b/.github/workflows/flaky-report.yml new file mode 100644 index 000000000..871e27741 --- /dev/null +++ b/.github/workflows/flaky-report.yml @@ -0,0 +1,100 @@ +name: Flaky Test Report + +on: + schedule: + - cron: "0 6 * * 1" # Every Monday 6am + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + flaky-report: + name: Detect flaky tests + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json + + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache-dependency-path: veza-backend-api/go.sum + + - name: Install dependencies + run: npm ci + + - name: Add veza.fr to hosts + run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts + + - name: Start backend services + run: | + docker-compose up -d postgres redis rabbitmq + for i in $(seq 1 30); do + if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then break; fi + sleep 2 + done + + - name: Run database migrations + env: + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + run: cd veza-backend-api && go run cmd/migrate_tool/main.go + + - name: Start backend API + env: + APP_ENV: test + APP_PORT: "18080" + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + REDIS_URL: redis://localhost:16379 + JWT_SECRET: flaky-test-jwt-secret + COOKIE_SECURE: "false" + CORS_ALLOWED_ORIGINS: http://localhost:5174 + DISABLE_RATE_LIMIT_FOR_TESTS: "true" + ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "user@veza.music,artist@veza.music,admin@veza.music" + run: | + cd veza-backend-api && go build -o veza-api ./cmd/api/main.go && ./veza-api & + sleep 10 + + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + + - name: Run E2E tests (3x retries to detect flakiness) + run: | + npx playwright test \ + --config=tests/e2e/playwright.config.ts \ + --project=chromium \ + --retries=3 \ + --reporter=json + continue-on-error: true + env: + PORT: "5174" + VITE_API_URL: "/api/v1" + PLAYWRIGHT_BASE_URL: "http://localhost:5174" + + - name: Generate flaky report + run: node scripts/flaky-detection.mjs > flaky-report.md + + - name: Upload flaky report + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: flaky-test-report + path: flaky-report.md + retention-days: 30 + + - name: Create/update issue with flaky tests + if: always() + run: | + REPORT=$(cat flaky-report.md) + FLAKY_COUNT=$(grep -c "^|" flaky-report.md | head -1 || echo "0") + if [ "$FLAKY_COUNT" -gt 2 ]; then + echo "Found flaky tests — check artifact for details" + fi diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index f281c7db2..dea8284ea 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -1,51 +1,54 @@ name: Frontend CI on: - push: - paths: - - "apps/web/**" - - ".github/workflows/frontend-ci.yml" - pull_request: - paths: - - "apps/web/**" - - ".github/workflows/frontend-ci.yml" + push: + paths: + - "apps/web/**" + - ".github/workflows/frontend-ci.yml" + pull_request: + paths: + - "apps/web/**" + - ".github/workflows/frontend-ci.yml" + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - test: - runs-on: ubuntu-latest + test: + runs-on: ubuntu-latest - defaults: - run: - working-directory: apps/web + defaults: + run: + working-directory: apps/web - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Node - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: "20" - cache: 'npm' - cache-dependency-path: apps/web/package-lock.json + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: apps/web/package-lock.json - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Lint - run: npm run lint + - name: Lint + run: npm run lint - - name: TypeScript check - run: npx tsc --noEmit + - name: TypeScript check + run: npx tsc --noEmit - - name: Build - run: npm run build + - name: Build + run: npm run build - - name: Bundle size gate - run: node scripts/check-bundle-size.mjs + - name: Bundle size gate + run: node scripts/check-bundle-size.mjs - - name: Audit dependencies - run: npm audit --audit-level=critical - - - name: Run tests - run: npm run test -- --run + - name: Audit dependencies + run: npm audit --audit-level=critical + - name: Run tests + run: npm run test -- --run diff --git a/.github/workflows/go-fuzz.yml b/.github/workflows/go-fuzz.yml new file mode 100644 index 000000000..fdf6c309b --- /dev/null +++ b/.github/workflows/go-fuzz.yml @@ -0,0 +1,42 @@ +name: Go Fuzz Tests + +on: + schedule: + - cron: "0 2 * * *" # Nightly at 2am UTC + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + fuzz: + runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + working-directory: veza-backend-api + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache: true + + - name: Download deps + run: go mod download + + - name: Run fuzz tests + run: go test -fuzz=Fuzz -fuzztime=60s ./internal/handlers/... + + - name: Upload fuzz corpus + if: always() + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: fuzz-corpus + path: veza-backend-api/testdata/fuzz/ + retention-days: 30 diff --git a/.github/workflows/load-test-nightly.yml b/.github/workflows/load-test-nightly.yml index 91b669397..e914fa401 100644 --- a/.github/workflows/load-test-nightly.yml +++ b/.github/workflows/load-test-nightly.yml @@ -1,81 +1,85 @@ name: Load Tests (Nightly) on: - schedule: - - cron: '0 2 * * *' - workflow_dispatch: + schedule: + - cron: "0 2 * * *" + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - load-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + load-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Install k6 - run: | - sudo gpg -k - sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 - echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] 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: Install k6 + run: | + sudo gpg -k + sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] 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 infrastructure - run: | - docker-compose -f docker-compose.yml up -d postgres redis rabbitmq - sleep 15 + - name: Start infrastructure + run: | + docker-compose -f docker-compose.yml up -d postgres redis rabbitmq + sleep 15 - - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: "1.24" - cache: true + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache: true - - name: Run migrations - working-directory: veza-backend-api - env: - DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable - REDIS_URL: redis://localhost:16379 - JWT_SECRET: test-jwt-secret-for-load-test - APP_ENV: test - run: | - go mod download - go run cmd/migrate_tool/main.go || true + - name: Run migrations + working-directory: veza-backend-api + env: + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + REDIS_URL: redis://localhost:16379 + JWT_SECRET: test-jwt-secret-for-load-test + APP_ENV: test + run: | + go mod download + go run cmd/migrate_tool/main.go || true - - name: Start backend API - working-directory: veza-backend-api - env: - DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable - REDIS_URL: redis://localhost:16379 - RABBITMQ_URL: amqp://veza:devpassword@localhost:15672/ - JWT_SECRET: test-jwt-secret-for-load-test - APP_ENV: test - PORT: 8080 - run: | - go run cmd/api/main.go & - sleep 15 + - name: Start backend API + working-directory: veza-backend-api + env: + DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable + REDIS_URL: redis://localhost:16379 + RABBITMQ_URL: amqp://veza:devpassword@localhost:15672/ + JWT_SECRET: test-jwt-secret-for-load-test + APP_ENV: test + PORT: 8080 + run: | + go run cmd/api/main.go & + sleep 15 - - name: Wait for backend - run: | - for i in 1 2 3 4 5 6 7 8 9 10; do - if curl -sf http://localhost:8080/health; then - echo "Backend ready" - exit 0 - fi - sleep 3 - done - echo "Backend not ready" - exit 1 + - name: Wait for backend + run: | + for i in 1 2 3 4 5 6 7 8 9 10; do + if curl -sf http://localhost:8080/health; then + echo "Backend ready" + exit 0 + fi + sleep 3 + done + echo "Backend not ready" + exit 1 - - name: Run smoke load test - run: k6 run loadtests/smoke.js + - name: Run smoke load test + run: k6 run loadtests/smoke.js - - name: Run backend load test - run: | - k6 run --out json=load-results.json loadtests/backend/full.js || true - continue-on-error: true + - name: Run backend load test + run: | + k6 run --out json=load-results.json loadtests/backend/full.js || true + continue-on-error: true - - name: Upload results - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: load-test-results - path: load-results.json - if: always() + - name: Upload results + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: load-test-results + path: load-results.json + if: always() diff --git a/.github/workflows/mutation-testing.yml b/.github/workflows/mutation-testing.yml new file mode 100644 index 000000000..242500605 --- /dev/null +++ b/.github/workflows/mutation-testing.yml @@ -0,0 +1,40 @@ +name: Mutation Testing + +on: + schedule: + - cron: "0 3 * * 0" # Weekly on Sunday at 3am UTC + workflow_dispatch: + +permissions: + contents: read + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + mutation: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: apps/web/package-lock.json + + - name: Install dependencies + run: cd apps/web && npm ci + + - name: Run Stryker mutation testing + run: cd apps/web && npx stryker run + + - name: Upload mutation report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutation-report + path: apps/web/reports/mutation/ + retention-days: 30 diff --git a/.github/workflows/openapi-lint.yml b/.github/workflows/openapi-lint.yml new file mode 100644 index 000000000..3767ffb5b --- /dev/null +++ b/.github/workflows/openapi-lint.yml @@ -0,0 +1,21 @@ +name: OpenAPI Lint + +on: + pull_request: + paths: + - "veza-backend-api/openapi.yaml" + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Lint OpenAPI spec + run: npx --yes @redocly/cli lint veza-backend-api/openapi.yaml diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 000000000..02001856c --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,43 @@ +name: Performance + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + bundle-size: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: apps/web/package-lock.json + + - name: Install dependencies + run: cd apps/web && npm ci + + - name: Build + run: cd apps/web && npm run build + + - name: Check bundle size + run: cd apps/web && npx size-limit + + - name: Upload stats artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: bundle-stats + path: apps/web/stats.html + if-no-files-found: ignore + retention-days: 14 diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 1a34bea15..e876ab2fb 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -1,52 +1,56 @@ name: Rust CI on: - push: - branches: [main] - paths: - - 'veza-stream-server/**' - pull_request: - branches: [main] - paths: - - 'veza-stream-server/**' + push: + branches: [main] + paths: + - "veza-stream-server/**" + pull_request: + branches: [main] + paths: + - "veza-stream-server/**" + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - test-and-lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - with: - components: clippy + test-and-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + components: clippy - - name: Clippy lint - run: cargo clippy -- -D warnings - working-directory: veza-stream-server + - name: Clippy lint + run: cargo clippy -- -D warnings + working-directory: veza-stream-server - - name: Run tests - run: cargo test --workspace --timeout 300 - working-directory: veza-stream-server + - name: Run tests + run: cargo test --workspace --timeout 300 + working-directory: veza-stream-server - - name: Install cargo-tarpaulin - run: cargo install cargo-tarpaulin + - name: Install cargo-tarpaulin + run: cargo install cargo-tarpaulin - - name: Measure coverage - run: cargo tarpaulin --out json --output-dir target/coverage --timeout 300 --skip-clean - working-directory: veza-stream-server + - name: Measure coverage + run: cargo tarpaulin --out json --output-dir target/coverage --timeout 300 --skip-clean + working-directory: veza-stream-server - - name: Enforce coverage threshold (>= 50%) - run: | - COVERAGE=$(cat target/coverage/tarpaulin-report.json | python3 -c "import sys,json; print(f'{json.load(sys.stdin).get(\"coverage\", 0):.1f}')") - echo "Rust coverage: ${COVERAGE}%" - COV_INT=$(echo "$COVERAGE" | cut -d. -f1) - if [ "$COV_INT" -lt 50 ]; then - echo "::error::Rust coverage ${COVERAGE}% is below the 50% threshold" - exit 1 - fi - echo "::notice::Rust coverage ${COVERAGE}% meets the >= 50% threshold" - working-directory: veza-stream-server + - name: Enforce coverage threshold (>= 50%) + run: | + COVERAGE=$(cat target/coverage/tarpaulin-report.json | python3 -c "import sys,json; print(f'{json.load(sys.stdin).get(\"coverage\", 0):.1f}')") + echo "Rust coverage: ${COVERAGE}%" + COV_INT=$(echo "$COVERAGE" | cut -d. -f1) + if [ "$COV_INT" -lt 50 ]; then + echo "::error::Rust coverage ${COVERAGE}% is below the 50% threshold" + exit 1 + fi + echo "::notice::Rust coverage ${COVERAGE}% meets the >= 50% threshold" + working-directory: veza-stream-server - - name: Upload coverage report - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: rust-coverage - path: veza-stream-server/target/coverage/tarpaulin-report.json + - name: Upload coverage report + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: rust-coverage + path: veza-stream-server/target/coverage/tarpaulin-report.json diff --git a/.github/workflows/rust-mutation.yml b/.github/workflows/rust-mutation.yml new file mode 100644 index 000000000..fac1eb94c --- /dev/null +++ b/.github/workflows/rust-mutation.yml @@ -0,0 +1,46 @@ +name: Rust Mutation Testing + +on: + schedule: + - cron: "0 4 * * 0" # Weekly on Sunday at 4am UTC + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + mutants: + runs-on: ubuntu-latest + timeout-minutes: 90 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + + - name: Cache cargo registry + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + veza-stream-server/target + key: ${{ runner.os }}-cargo-mutants-${{ hashFiles('veza-stream-server/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-mutants- + + - name: Install cargo-mutants + run: cargo install cargo-mutants + + - name: Run mutation testing + run: cd veza-stream-server && cargo mutants --timeout 120 -- --lib + continue-on-error: true + + - name: Upload mutation results + if: always() + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: mutation-results + path: veza-stream-server/mutants.out/ + retention-days: 30 diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml index 28b07feaa..610d2d5b4 100644 --- a/.github/workflows/sast.yml +++ b/.github/workflows/sast.yml @@ -1,22 +1,26 @@ name: CodeQL SAST on: - push: - branches: [main] - pull_request: - branches: [main] + push: + branches: [main] + pull_request: + branches: [main] + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - analyze: - runs-on: ubuntu-latest - permissions: - security-events: write - strategy: - matrix: - language: [go, javascript-typescript] - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: github/codeql-action/init@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18 - with: - languages: ${{ matrix.language }} - - uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18 - - uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18 + analyze: + runs-on: ubuntu-latest + permissions: + security-events: write + strategy: + matrix: + language: [go, javascript-typescript] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: github/codeql-action/init@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18 + with: + languages: ${{ matrix.language }} + - uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18 + - uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18 diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 2562b4659..03196523a 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -1,22 +1,26 @@ name: Security Scan on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - gitleaks: - name: Secret Scanning (gitleaks) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 + gitleaks: + name: Secret Scanning (gitleaks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - - name: Run Gitleaks - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196e88a9c30 # v2.3.8 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run Gitleaks + uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196e88a9c30 # v2.3.8 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..288552c25 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,43 @@ +name: Semgrep SAST + +on: + pull_request: + branches: [main] + schedule: + - cron: "0 3 * * 1" # Weekly on Monday at 3am UTC + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + semgrep: + runs-on: ubuntu-latest + timeout-minutes: 15 + + container: + image: returntocorp/semgrep + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Run Semgrep + run: > + semgrep scan + --config p/auto + --config p/owasp-top-ten + --config p/r2c-security-audit + --error + --json + --output semgrep-results.json + . + continue-on-error: true + + - name: Upload Semgrep results + if: always() + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: semgrep-results + path: semgrep-results.json + retention-days: 30 diff --git a/.github/workflows/staging-validation.yml b/.github/workflows/staging-validation.yml index 2aca851dd..d2c4158cc 100644 --- a/.github/workflows/staging-validation.yml +++ b/.github/workflows/staging-validation.yml @@ -3,304 +3,314 @@ name: Staging Validation Pipeline # Comprehensive staging validation: deploy, perf, Lighthouse, stability, GDPR, bundle size on: - workflow_dispatch: - inputs: - skip_deploy: - description: 'Skip deployment (validate existing staging)' - required: false - default: 'false' - type: boolean - stability_duration: - description: 'Stability check duration (minutes)' - required: false - default: '10' - type: string + workflow_dispatch: + inputs: + skip_deploy: + description: "Skip deployment (validate existing staging)" + required: false + default: "false" + type: boolean + stability_duration: + description: "Stability check duration (minutes)" + required: false + default: "10" + type: string env: - STAGING_URL: ${{ vars.STAGING_URL || 'https://staging.veza.app' }} - STAGING_API_URL: ${{ vars.STAGING_API_URL || 'https://staging.veza.app/api/v1' }} + STAGING_URL: ${{ vars.STAGING_URL || 'https://staging.veza.app' }} + STAGING_API_URL: ${{ vars.STAGING_API_URL || 'https://staging.veza.app/api/v1' }} + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - # ───────────────────────────────────────────────────── - # TASK-STAG-001: Deploy staging (all services) - # ───────────────────────────────────────────────────── - deploy-staging: - name: Deploy to Staging - runs-on: ubuntu-latest - if: inputs.skip_deploy != 'true' - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # ───────────────────────────────────────────────────── + # TASK-STAG-001: Deploy staging (all services) + # ───────────────────────────────────────────────────── + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + if: inputs.skip_deploy != 'true' + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - name: Build all images - run: | - docker build -t veza-backend-api:staging -f veza-backend-api/Dockerfile.production veza-backend-api/ - docker build -t veza-frontend:staging -f apps/web/Dockerfile.production apps/web/ - docker build -t veza-stream-server:staging -f veza-stream-server/Dockerfile.production veza-stream-server/ + - name: Build all images + run: | + docker build -t veza-backend-api:staging -f veza-backend-api/Dockerfile.production veza-backend-api/ + docker build -t veza-frontend:staging -f apps/web/Dockerfile.production apps/web/ + docker build -t veza-stream-server:staging -f veza-stream-server/Dockerfile.production veza-stream-server/ - - name: Push to registry - if: vars.DOCKER_REGISTRY != '' - run: | - echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin - for svc in veza-backend-api veza-frontend veza-stream-server; do - docker tag "${svc}:staging" "${{ vars.DOCKER_REGISTRY }}/${svc}:staging" - docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:staging" - done + - name: Push to registry + if: vars.DOCKER_REGISTRY != '' + run: | + echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin + for svc in veza-backend-api veza-frontend veza-stream-server; do + docker tag "${svc}:staging" "${{ vars.DOCKER_REGISTRY }}/${svc}:staging" + docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:staging" + done - - name: Deploy via SSH (docker-compose) - if: vars.STAGING_SSH_HOST != '' - env: - SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} - run: | - mkdir -p ~/.ssh - echo "$SSH_KEY" > ~/.ssh/staging_key - chmod 600 ~/.ssh/staging_key - ssh -i ~/.ssh/staging_key -o StrictHostKeyChecking=no \ - ${{ vars.STAGING_SSH_USER }}@${{ vars.STAGING_SSH_HOST }} \ - "cd /opt/veza && docker compose -f docker-compose.staging.yml pull && docker compose -f docker-compose.staging.yml up -d" - rm -f ~/.ssh/staging_key + - name: Deploy via SSH (docker-compose) + if: vars.STAGING_SSH_HOST != '' + env: + SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} + run: | + mkdir -p ~/.ssh + echo "$SSH_KEY" > ~/.ssh/staging_key + chmod 600 ~/.ssh/staging_key + ssh -i ~/.ssh/staging_key -o StrictHostKeyChecking=no \ + ${{ vars.STAGING_SSH_USER }}@${{ vars.STAGING_SSH_HOST }} \ + "cd /opt/veza && docker compose -f docker-compose.staging.yml pull && docker compose -f docker-compose.staging.yml up -d" + rm -f ~/.ssh/staging_key - - name: Deploy via Kubernetes - if: vars.KUBE_CONFIG_SET == 'true' - run: | - KUBECONFIG="${{ runner.temp }}/kubeconfig" - echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG" - chmod 600 "$KUBECONFIG" - export KUBECONFIG - for svc in veza-backend-api veza-stream-server; do - kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:staging" \ - -n veza --record || echo "Skipping ${svc}" - done - kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true - rm -f "$KUBECONFIG" + - name: Deploy via Kubernetes + if: vars.KUBE_CONFIG_SET == 'true' + run: | + KUBECONFIG="${{ runner.temp }}/kubeconfig" + echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG" + chmod 600 "$KUBECONFIG" + export KUBECONFIG + for svc in veza-backend-api veza-stream-server; do + kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:staging" \ + -n veza --record || echo "Skipping ${svc}" + done + kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true + rm -f "$KUBECONFIG" - - name: Wait for staging to be healthy - run: | - echo "Waiting for staging services to be healthy..." - for i in $(seq 1 30); do - STATUS=$(curl -sf "${{ env.STAGING_API_URL }}/health" | jq -r '.status' 2>/dev/null || echo "unreachable") - if [ "$STATUS" = "ok" ] || [ "$STATUS" = "healthy" ]; then - echo "Staging is healthy!" - exit 0 - fi - echo "Attempt $i/30: status=$STATUS, waiting 10s..." - sleep 10 - done - echo "Staging did not become healthy in 300s" - exit 1 + - name: Wait for staging to be healthy + run: | + echo "Waiting for staging services to be healthy..." + for i in $(seq 1 30); do + STATUS=$(curl -sf "${{ env.STAGING_API_URL }}/health" | jq -r '.status' 2>/dev/null || echo "unreachable") + if [ "$STATUS" = "ok" ] || [ "$STATUS" = "healthy" ]; then + echo "Staging is healthy!" + exit 0 + fi + echo "Attempt $i/30: status=$STATUS, waiting 10s..." + sleep 10 + done + echo "Staging did not become healthy in 300s" + exit 1 - - name: Deep health check - run: | - echo "## Deep Health Check" >> $GITHUB_STEP_SUMMARY - HEALTH=$(curl -sf "${{ env.STAGING_API_URL }}/health/deep" || echo '{"error":"unreachable"}') - echo '```json' >> $GITHUB_STEP_SUMMARY - echo "$HEALTH" | jq . >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY + - name: Deep health check + run: | + echo "## Deep Health Check" >> $GITHUB_STEP_SUMMARY + HEALTH=$(curl -sf "${{ env.STAGING_API_URL }}/health/deep" || echo '{"error":"unreachable"}') + echo '```json' >> $GITHUB_STEP_SUMMARY + echo "$HEALTH" | jq . >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY - # ───────────────────────────────────────────────────── - # TASK-STAG-002: Performance validation (p95 < 100ms) - # ───────────────────────────────────────────────────── - performance-validation: - name: Performance Validation (k6) - runs-on: ubuntu-latest - needs: deploy-staging - if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # ───────────────────────────────────────────────────── + # TASK-STAG-002: Performance validation (p95 < 100ms) + # ───────────────────────────────────────────────────── + performance-validation: + name: Performance Validation (k6) + runs-on: ubuntu-latest + needs: deploy-staging + if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Install k6 - run: | - sudo gpg -k - sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 - echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] 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: Install k6 + run: | + sudo gpg -k + sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] 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: Run staging performance validation - run: | - k6 run --out json=perf-results.json \ - --env BASE_URL="${{ env.STAGING_API_URL }}" \ - --env SCENARIO=smoke \ - loadtests/staging/validation_v0140.js + - name: Run staging performance validation + run: | + k6 run --out json=perf-results.json \ + --env BASE_URL="${{ env.STAGING_API_URL }}" \ + --env SCENARIO=smoke \ + loadtests/staging/validation_v0140.js - - name: Upload performance results - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: performance-results - path: perf-results.json + - name: Upload performance results + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: performance-results + path: perf-results.json + if: always() + + - name: Performance summary + if: always() + run: | + echo "## Performance Validation" >> $GITHUB_STEP_SUMMARY + echo "Target: p95 < 100ms, stream start < 500ms" >> $GITHUB_STEP_SUMMARY + + # ───────────────────────────────────────────────────── + # TASK-STAG-003: Lighthouse validation + # ───────────────────────────────────────────────────── + lighthouse-validation: + name: Lighthouse Audit + runs-on: ubuntu-latest + needs: deploy-staging + if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + + - name: Install Lighthouse CI + run: npm install -g @lhci/cli@0.14.x + + - name: Run Lighthouse CI + run: lhci autorun --config=.lighthouserc.js + env: + LHCI_BUILD_CONTEXT__CURRENT_HASH: ${{ github.sha }} + + - name: Upload Lighthouse results + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: lighthouse-results + path: .lighthouseci/ + if: always() + + - name: Lighthouse summary + if: always() + run: | + echo "## Lighthouse Validation" >> $GITHUB_STEP_SUMMARY + echo "Targets: Performance >= 85, Accessibility >= 90, PWA >= 90" >> $GITHUB_STEP_SUMMARY + if [ -f .lighthouseci/assertion-results.json ]; then + PASSED=$(jq '[.[] | select(.level == "error")] | length' .lighthouseci/assertion-results.json 2>/dev/null || echo "?") + echo "Assertion errors: $PASSED" >> $GITHUB_STEP_SUMMARY + fi + + # ───────────────────────────────────────────────────── + # TASK-STAG-004: Stability validation (5xx < 0.1%) + # ───────────────────────────────────────────────────── + stability-validation: + name: Stability Check + runs-on: ubuntu-latest + needs: [deploy-staging, performance-validation] + if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Run stability check + run: | + chmod +x scripts/staging-stability-check.sh + DURATION_MINUTES=${{ inputs.stability_duration || '10' }} \ + STAGING_API_URL="${{ env.STAGING_API_URL }}" \ + MAX_5XX_RATE="0.001" \ + bash scripts/staging-stability-check.sh + + - name: Stability summary + if: always() + run: | + echo "## Stability Validation" >> $GITHUB_STEP_SUMMARY + echo "Duration: ${{ inputs.stability_duration || '10' }} minutes" >> $GITHUB_STEP_SUMMARY + echo "Target: 5xx rate < 0.1%" >> $GITHUB_STEP_SUMMARY + if [ -f stability-report.json ]; then + cat stability-report.json | jq . >> $GITHUB_STEP_SUMMARY + fi + + # ───────────────────────────────────────────────────── + # TASK-STAG-005: GDPR validation (export + deletion E2E) + # ───────────────────────────────────────────────────── + gdpr-validation: + name: GDPR Compliance Check + runs-on: ubuntu-latest + needs: deploy-staging + if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: "1.24" + cache: true + + - name: Run GDPR integration tests + working-directory: veza-backend-api + run: go test -v -tags=integration -run TestGDPR -timeout 120s ./tests/integration/... + + - name: GDPR summary + if: always() + run: | + echo "## GDPR Validation" >> $GITHUB_STEP_SUMMARY + echo "- Data export: tested" >> $GITHUB_STEP_SUMMARY + echo "- Account deletion: tested" >> $GITHUB_STEP_SUMMARY + + # ───────────────────────────────────────────────────── + # TASK-STAG-006: Bundle size validation (< 200KB gzip) + # ───────────────────────────────────────────────────── + bundle-size-validation: + name: Bundle Size Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build frontend + working-directory: apps/web + run: npx vite build --outDir dist_verification + env: + NODE_ENV: production + + - name: Check bundle size + working-directory: apps/web + run: node scripts/check-bundle-size.mjs + + - name: Bundle size summary + if: always() + run: | + echo "## Bundle Size Validation" >> $GITHUB_STEP_SUMMARY + echo "Target: JS initial < 200KB gzipped" >> $GITHUB_STEP_SUMMARY + + # ───────────────────────────────────────────────────── + # Final summary + # ───────────────────────────────────────────────────── + validation-summary: + name: Validation Summary + runs-on: ubuntu-latest + needs: + [ + deploy-staging, + performance-validation, + lighthouse-validation, + stability-validation, + gdpr-validation, + bundle-size-validation, + ] if: always() + steps: + - name: Generate final report + run: | + echo "# Staging Validation Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Deploy (STAG-001) | ${{ needs.deploy-staging.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Performance (STAG-002) | ${{ needs.performance-validation.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Lighthouse (STAG-003) | ${{ needs.lighthouse-validation.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Stability (STAG-004) | ${{ needs.stability-validation.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| GDPR (STAG-005) | ${{ needs.gdpr-validation.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Bundle Size (STAG-006) | ${{ needs.bundle-size-validation.result }} |" >> $GITHUB_STEP_SUMMARY - - name: Performance summary - if: always() - run: | - echo "## Performance Validation" >> $GITHUB_STEP_SUMMARY - echo "Target: p95 < 100ms, stream start < 500ms" >> $GITHUB_STEP_SUMMARY - - # ───────────────────────────────────────────────────── - # TASK-STAG-003: Lighthouse validation - # ───────────────────────────────────────────────────── - lighthouse-validation: - name: Lighthouse Audit - runs-on: ubuntu-latest - needs: deploy-staging - if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up Node - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: '20' - - - name: Install Lighthouse CI - run: npm install -g @lhci/cli@0.14.x - - - name: Run Lighthouse CI - run: lhci autorun --config=.lighthouserc.js - env: - LHCI_BUILD_CONTEXT__CURRENT_HASH: ${{ github.sha }} - - - name: Upload Lighthouse results - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: lighthouse-results - path: .lighthouseci/ - if: always() - - - name: Lighthouse summary - if: always() - run: | - echo "## Lighthouse Validation" >> $GITHUB_STEP_SUMMARY - echo "Targets: Performance >= 85, Accessibility >= 90, PWA >= 90" >> $GITHUB_STEP_SUMMARY - if [ -f .lighthouseci/assertion-results.json ]; then - PASSED=$(jq '[.[] | select(.level == "error")] | length' .lighthouseci/assertion-results.json 2>/dev/null || echo "?") - echo "Assertion errors: $PASSED" >> $GITHUB_STEP_SUMMARY - fi - - # ───────────────────────────────────────────────────── - # TASK-STAG-004: Stability validation (5xx < 0.1%) - # ───────────────────────────────────────────────────── - stability-validation: - name: Stability Check - runs-on: ubuntu-latest - needs: [deploy-staging, performance-validation] - if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Run stability check - run: | - chmod +x scripts/staging-stability-check.sh - DURATION_MINUTES=${{ inputs.stability_duration || '10' }} \ - STAGING_API_URL="${{ env.STAGING_API_URL }}" \ - MAX_5XX_RATE="0.001" \ - bash scripts/staging-stability-check.sh - - - name: Stability summary - if: always() - run: | - echo "## Stability Validation" >> $GITHUB_STEP_SUMMARY - echo "Duration: ${{ inputs.stability_duration || '10' }} minutes" >> $GITHUB_STEP_SUMMARY - echo "Target: 5xx rate < 0.1%" >> $GITHUB_STEP_SUMMARY - if [ -f stability-report.json ]; then - cat stability-report.json | jq . >> $GITHUB_STEP_SUMMARY - fi - - # ───────────────────────────────────────────────────── - # TASK-STAG-005: GDPR validation (export + deletion E2E) - # ───────────────────────────────────────────────────── - gdpr-validation: - name: GDPR Compliance Check - runs-on: ubuntu-latest - needs: deploy-staging - if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: "1.24" - cache: true - - - name: Run GDPR integration tests - working-directory: veza-backend-api - run: go test -v -tags=integration -run TestGDPR -timeout 120s ./tests/integration/... - - - name: GDPR summary - if: always() - run: | - echo "## GDPR Validation" >> $GITHUB_STEP_SUMMARY - echo "- Data export: tested" >> $GITHUB_STEP_SUMMARY - echo "- Account deletion: tested" >> $GITHUB_STEP_SUMMARY - - # ───────────────────────────────────────────────────── - # TASK-STAG-006: Bundle size validation (< 200KB gzip) - # ───────────────────────────────────────────────────── - bundle-size-validation: - name: Bundle Size Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Set up Node - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build frontend - working-directory: apps/web - run: npx vite build --outDir dist_verification - env: - NODE_ENV: production - - - name: Check bundle size - working-directory: apps/web - run: node scripts/check-bundle-size.mjs - - - name: Bundle size summary - if: always() - run: | - echo "## Bundle Size Validation" >> $GITHUB_STEP_SUMMARY - echo "Target: JS initial < 200KB gzipped" >> $GITHUB_STEP_SUMMARY - - # ───────────────────────────────────────────────────── - # Final summary - # ───────────────────────────────────────────────────── - validation-summary: - name: Validation Summary - runs-on: ubuntu-latest - needs: [deploy-staging, performance-validation, lighthouse-validation, stability-validation, gdpr-validation, bundle-size-validation] - if: always() - steps: - - name: Generate final report - run: | - echo "# Staging Validation Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Deploy (STAG-001) | ${{ needs.deploy-staging.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Performance (STAG-002) | ${{ needs.performance-validation.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Lighthouse (STAG-003) | ${{ needs.lighthouse-validation.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Stability (STAG-004) | ${{ needs.stability-validation.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| GDPR (STAG-005) | ${{ needs.gdpr-validation.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Bundle Size (STAG-006) | ${{ needs.bundle-size-validation.result }} |" >> $GITHUB_STEP_SUMMARY - - - name: Check all passed - run: | - FAILED=0 - for result in "${{ needs.performance-validation.result }}" "${{ needs.lighthouse-validation.result }}" "${{ needs.bundle-size-validation.result }}"; do - if [ "$result" = "failure" ]; then - FAILED=1 - fi - done - if [ "$FAILED" = "1" ]; then - echo "Some validations failed — see summary above." - exit 1 - fi - echo "All critical validations passed!" + - name: Check all passed + run: | + FAILED=0 + for result in "${{ needs.performance-validation.result }}" "${{ needs.lighthouse-validation.result }}" "${{ needs.bundle-size-validation.result }}"; do + if [ "$result" = "failure" ]; then + FAILED=1 + fi + done + if [ "$FAILED" = "1" ]; then + echo "Some validations failed — see summary above." + exit 1 + fi + echo "All critical validations passed!" diff --git a/.github/workflows/storybook-audit.yml b/.github/workflows/storybook-audit.yml index 6836de344..41bd2c463 100644 --- a/.github/workflows/storybook-audit.yml +++ b/.github/workflows/storybook-audit.yml @@ -4,44 +4,48 @@ name: Storybook Audit on: - push: - paths: - - "apps/web/**" - - ".github/workflows/storybook-audit.yml" - pull_request: - paths: - - "apps/web/**" - - ".github/workflows/storybook-audit.yml" - workflow_dispatch: + push: + paths: + - "apps/web/**" + - ".github/workflows/storybook-audit.yml" + pull_request: + paths: + - "apps/web/**" + - ".github/workflows/storybook-audit.yml" + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - audit: - name: Build & audit Storybook - runs-on: ubuntu-latest - defaults: - run: - working-directory: apps/web + audit: + name: Build & audit Storybook + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/web - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Node - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: "20" - cache: "npm" - cache-dependency-path: apps/web/package-lock.json + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: apps/web/package-lock.json - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Install Playwright Chromium - run: npx playwright install chromium --with-deps + - name: Install Playwright Chromium + run: npx playwright install chromium --with-deps - - name: Validate Storybook (build, serve 6007, audit) - run: npm run validate:storybook - env: - VITE_API_URL: /api/v1 - VITE_USE_MSW: "true" - VITE_STORYBOOK: "true" + - name: Validate Storybook (build, serve 6007, audit) + run: npm run validate:storybook + env: + VITE_API_URL: /api/v1 + VITE_USE_MSW: "true" + VITE_STORYBOOK: "true" diff --git a/.github/workflows/stream-ci.yml b/.github/workflows/stream-ci.yml index 32eb709d6..ca6345c2d 100644 --- a/.github/workflows/stream-ci.yml +++ b/.github/workflows/stream-ci.yml @@ -1,41 +1,44 @@ name: Stream Server CI on: - push: - paths: - - "veza-stream-server/**" - - "veza-common/**" - - ".github/workflows/stream-ci.yml" - pull_request: - paths: - - "veza-stream-server/**" - - "veza-common/**" - - ".github/workflows/stream-ci.yml" + push: + paths: + - "veza-stream-server/**" + - "veza-common/**" + - ".github/workflows/stream-ci.yml" + pull_request: + paths: + - "veza-stream-server/**" + - "veza-common/**" + - ".github/workflows/stream-ci.yml" + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" jobs: - test: - runs-on: ubuntu-latest + test: + runs-on: ubuntu-latest - defaults: - run: - working-directory: veza-stream-server + defaults: + run: + working-directory: veza-stream-server - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Set up Rust - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - with: - components: clippy + - name: Set up Rust + uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + components: clippy - - name: Lint with clippy - run: cargo clippy --all-targets -- -D warnings + - name: Lint with clippy + run: cargo clippy --all-targets -- -D warnings - - name: Audit dependencies - uses: actions-rust-lang/audit@v1 # TODO: pin to SHA — no known mapping provided - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Run tests - run: cargo test --all + - name: Audit dependencies + uses: actions-rust-lang/audit@v1 # TODO: pin to SHA — no known mapping provided + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Run tests + run: cargo test --all diff --git a/.github/workflows/trivy-fs.yml b/.github/workflows/trivy-fs.yml new file mode 100644 index 000000000..e380f73c8 --- /dev/null +++ b/.github/workflows/trivy-fs.yml @@ -0,0 +1,45 @@ +name: Trivy Filesystem Scan + +on: + pull_request: + branches: [main] + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + trivy-scan: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Run Trivy filesystem scan + uses: aquasecurity/trivy-action@master + with: + scan-type: "fs" + scan-ref: "." + severity: "CRITICAL,HIGH" + exit-code: "1" + format: "table" + + - name: Run Trivy (SARIF output) + if: always() + uses: aquasecurity/trivy-action@master + with: + scan-type: "fs" + scan-ref: "." + severity: "CRITICAL,HIGH" + exit-code: "0" + format: "sarif" + output: "trivy-results.sarif" + + - name: Upload Trivy SARIF + if: always() + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: trivy-results + path: trivy-results.sarif + retention-days: 30 diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml new file mode 100644 index 000000000..6d3d921c5 --- /dev/null +++ b/.github/workflows/visual-regression.yml @@ -0,0 +1,54 @@ +name: Visual Regression + +on: + pull_request: + branches: [main] + paths: + - "apps/web/**" + - ".github/workflows/visual-regression.yml" + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + visual-regression: + name: Lost Pixel visual regression + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Node + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + working-directory: apps/web + + - name: Build Storybook + run: npm run build-storybook + working-directory: apps/web + env: + VITE_API_URL: /api/v1 + VITE_USE_MSW: "true" + VITE_STORYBOOK: "true" + + - name: Run Lost Pixel + id: lostpixel + run: npx lost-pixel + working-directory: apps/web + + - name: Upload diff artifacts + if: failure() && steps.lostpixel.outcome == 'failure' + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: lost-pixel-diff + path: | + apps/web/.lostpixel/current/ + apps/web/.lostpixel/difference/ + retention-days: 7 diff --git a/.github/workflows/zap-dast.yml b/.github/workflows/zap-dast.yml new file mode 100644 index 000000000..5da86db13 --- /dev/null +++ b/.github/workflows/zap-dast.yml @@ -0,0 +1,34 @@ +name: OWASP ZAP DAST + +on: + schedule: + - cron: "0 3 * * *" # Nightly at 3am UTC + workflow_dispatch: + +env: + GIT_SSL_NO_VERIFY: "true" + NODE_TLS_REJECT_UNAUTHORIZED: "0" + +jobs: + zap-baseline: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: ZAP Baseline Scan + uses: zaproxy/action-baseline@v0.12.0 + with: + target: ${{ secrets.STAGING_URL || 'http://localhost:5174' }} + rules_file_name: .zap/rules.tsv + fail_action: false + artifact_name: zap-report + + - name: Upload ZAP report + if: always() + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: zap-report + path: report_html.html + retention-days: 30