name: Veza CI/CD on: push: branches: [ "main", "remediation/*", "feature/mvp-complete" ] pull_request: branches: [ "main", "feature/mvp-complete" ] workflow_dispatch: # Allow manual trigger jobs: backend-go: name: 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: Set up Node uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: '20' cache: 'npm' - name: Set up Go uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: '1.24' cache: true - name: Install dependencies run: npm ci - name: Run govulncheck run: | cd veza-backend-api go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... - name: Vet run: | cd veza-backend-api go vet ./... - name: Install golangci-lint run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - name: Lint run: npx turbo run lint --filter=veza-backend-api - name: Test with coverage run: | cd veza-backend-api go test ./internal/handlers/... ./internal/services/... -short -coverprofile=coverage.out -covermode=atomic COVERAGE=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%') echo "Coverage: ${COVERAGE}%" echo "coverage=${COVERAGE}" >> $GITHUB_OUTPUT if awk -v c="$COVERAGE" -v t=60 'BEGIN {exit !(c+0>=t)}'; then echo "Coverage gate passed (>= 60%)" else echo "Coverage $COVERAGE% is below threshold 60%" exit 1 fi - name: Build run: npx turbo run build --filter=veza-backend-api rust-services: name: Rust Services (Stream) 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: Set up Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: components: rustfmt, clippy - name: Install dependencies run: npm ci - 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: Auditing Stream Server run: | cd veza-stream-server cargo audit - name: Lint run: npx turbo run lint --filter=veza-stream-server - name: Build run: npx turbo run build --filter=veza-stream-server - name: Test run: npx turbo run test --filter=veza-stream-server frontend: name: 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: Install Dependencies run: npm ci - name: Security audit (npm) run: npm audit --audit-level=critical - 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: Generate Types from OpenAPI run: | cd apps/web chmod +x scripts/generate-types.sh ./scripts/generate-types.sh continue-on-error: false # This step ensures types are generated before typecheck # Cache keyed on openapi.yaml hash, so types regenerate when spec changes - name: Check types sync with OpenAPI spec run: | if ! git diff --exit-code apps/web/src/types/generated/; then echo "::error::Types are out of sync with openapi.yaml. Run 'make openapi' then 'cd apps/web && ./scripts/generate-types.sh' and commit the updated types." exit 1 fi - name: Lint run: npx turbo run lint --filter=veza-frontend - name: Format Check run: | cd apps/web npm run format:check --if-present - name: Type Check run: | cd apps/web npm run typecheck - name: Test with coverage run: npx turbo run test --filter=veza-frontend -- --run --coverage - name: Contrast Tests run: | cd apps/web npm run test -- --run src/__tests__/contrast.test.ts - name: Build run: npx turbo run build --filter=veza-frontend 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: Install dependencies run: npm ci - 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 e2e: name: E2E (Playwright) runs-on: ubuntu-latest timeout-minutes: 45 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 working-directory: apps/web - 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 }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: playwright-report path: apps/web/playwright-report/ retention-days: 7 notify-failure: name: Notify on failure needs: [backend-go, rust-services, 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 }}"