feat: add AI code reviewer using Gemini API #68
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI Pipeline | |
| on: | |
| push: | |
| branches: [main, develop] | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ────────────────────────────────────────────────── | |
| # Stage 1: Lint + Type Check (fast fail) | |
| # ────────────────────────────────────────────────── | |
| lint: | |
| name: Lint & Type Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| - name: Run ESLint | |
| run: bun run lint | |
| - name: Run TypeScript check | |
| run: bun run typecheck | |
| docs-parity: | |
| name: Docs Parity | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| - name: Check docs parity | |
| run: bun run docs:parity | |
| # ────────────────────────────────────────────────── | |
| # Stage 2: Security Audit | |
| # ────────────────────────────────────────────────── | |
| security: | |
| name: Security Gates | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| - name: Dependency audit | |
| run: bun run security:audit | |
| # ────────────────────────────────────────────────── | |
| # Stage 3: Unit & Integration Tests + Coverage | |
| # ────────────────────────────────────────────────── | |
| test: | |
| name: Unit & Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 15 | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| POSTGRES_DB: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| - name: Run tests with coverage | |
| run: bun run test:coverage | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| - name: Coverage threshold check (>50%) | |
| run: | | |
| if [ -f coverage/coverage-summary.json ]; then | |
| LINE_PCT=$(bun -e " | |
| const data = JSON.parse(require('fs').readFileSync('coverage/coverage-summary.json','utf8')); | |
| console.log(data.total?.lines?.pct ?? 0); | |
| ") | |
| echo "Line coverage: ${LINE_PCT}%" | |
| if (( $(echo "$LINE_PCT < 50" | bc -l 2>/dev/null || echo 0) )); then | |
| echo "::error::Coverage ${LINE_PCT}% is below 50% threshold" | |
| exit 1 | |
| fi | |
| else | |
| echo "::warning::Coverage report not found, skipping threshold check" | |
| fi | |
| - name: Upload coverage | |
| if: always() | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| file: ./coverage/lcov.info | |
| fail_ci_if_error: false | |
| - name: Upload coverage artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: coverage/ | |
| retention-days: 14 | |
| # ────────────────────────────────────────────────── | |
| # Stage 4: E2E Tests (Playwright) | |
| # ────────────────────────────────────────────────── | |
| e2e: | |
| name: E2E Tests (Playwright) | |
| runs-on: ubuntu-latest | |
| needs: test | |
| timeout-minutes: 20 | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| POSTGRES_DB: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| - name: Install Playwright browsers | |
| run: bunx playwright install --with-deps chromium | |
| - name: Push Drizzle schema to test database | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| DATABASE_DRIVER: postgres | |
| run: bunx drizzle-kit push --force | |
| - name: Run E2E tests | |
| run: bunx playwright test | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| DATABASE_DRIVER: postgres | |
| JWT_SECRET: "ci-test-secret-key-for-e2e-tests" | |
| SITE_URL: "http://localhost:4321" | |
| INTERNAL_HOOK_SECRET: "ci-test-internal-hook-secret" | |
| CSRF_SKIP_DEV: "true" | |
| RATE_LIMIT_SKIP_DEV: "true" | |
| - name: Upload Playwright report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: playwright-report/ | |
| retention-days: 14 | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: test-results/ | |
| retention-days: 7 | |
| # ────────────────────────────────────────────────── | |
| # Stage 5: Container Security Scan (Trivy) | |
| # ────────────────────────────────────────────────── | |
| container-scan: | |
| name: Container Security (Trivy) | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build Docker image | |
| run: docker build -t opencodehub:ci -f Dockerfile . | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| image-ref: "opencodehub:ci" | |
| format: "sarif" | |
| output: "trivy-results.sarif" | |
| severity: "CRITICAL,HIGH" | |
| exit-code: "0" | |
| - name: Upload Trivy scan results | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: "trivy-results.sarif" | |
| continue-on-error: true | |
| # ────────────────────────────────────────────────── | |
| # Stage 6: SAST (Semgrep) | |
| # ────────────────────────────────────────────────── | |
| sast: | |
| name: SAST (Semgrep) | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 10 | |
| container: | |
| image: semgrep/semgrep | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run Semgrep | |
| run: semgrep scan --config auto --error --json --output semgrep-results.json src/ || true | |
| env: | |
| SEMGREP_RULES: >- | |
| p/typescript | |
| p/javascript | |
| p/security-audit | |
| - name: Upload Semgrep results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: semgrep-results | |
| path: semgrep-results.json | |
| retention-days: 14 | |
| # ────────────────────────────────────────────────── | |
| # Stage 7: Performance Gate | |
| # ────────────────────────────────────────────────── | |
| performance: | |
| name: Performance Gate | |
| runs-on: ubuntu-latest | |
| needs: [build] | |
| timeout-minutes: 10 | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| POSTGRES_DB: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| - name: Build application | |
| run: bun run build | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| DATABASE_DRIVER: postgres | |
| - name: Start server and run perf check | |
| run: | | |
| HOST=127.0.0.1 PORT=4321 node ./dist/server/entry.mjs & | |
| SERVER_PID=$! | |
| sleep 5 | |
| curl -s http://127.0.0.1:4321/api/health > /dev/null || true | |
| curl -s http://127.0.0.1:4321/api/metrics > /dev/null || true | |
| bun run perf:baseline | |
| kill $SERVER_PID 2>/dev/null || true | |
| env: | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| DATABASE_DRIVER: postgres | |
| JWT_SECRET: "ci-perf-secret" | |
| SITE_URL: http://127.0.0.1:4321 | |
| INTERNAL_HOOK_SECRET: "ci-perf-hook" | |
| CSRF_SKIP_DEV: "true" | |
| RATE_LIMIT_SKIP_DEV: "true" | |
| SKIP_REDIS_CHECK: "1" | |
| PERF_BASE_URL: http://127.0.0.1:4321 | |
| PERF_PATHS: /api/health,/api/metrics | |
| PERF_CONCURRENCY: 4 | |
| PERF_REQUESTS: 50 | |
| PERF_MAX_P95_MS: 500 | |
| PERF_OUTPUT: test-results/perf-report.json | |
| - name: Upload perf report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: perf-report | |
| path: test-results/perf-report.json | |
| retention-days: 14 | |
| # ────────────────────────────────────────────────── | |
| # Stage 8: Build Verification | |
| # ────────────────────────────────────────────────── | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| needs: [lint, docs-parity, security, test] | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| - name: Build application | |
| run: bun run build | |
| env: | |
| SKIP_REDIS_CHECK: "1" | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist | |
| path: dist/ | |
| retention-days: 7 | |
| # ────────────────────────────────────────────────── | |
| # Stage 8: Docker Build & Push | |
| # ────────────────────────────────────────────────── | |
| docker: | |
| name: Docker Build | |
| runs-on: ubuntu-latest | |
| needs: [build, e2e, container-scan, performance] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Container Registry (Docker Hub) | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: docker.io | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ secrets.DOCKER_USERNAME }}/opencodehub | |
| tags: | | |
| type=ref,event=branch | |
| type=sha | |
| - name: Build and push | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # ────────────────────────────────────────────────── | |
| # Stage 9: Publish CLI Package | |
| # ────────────────────────────────────────────────── | |
| cli-publish: | |
| name: Publish CLI | |
| runs-on: ubuntu-latest | |
| needs: [build, e2e] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Install dependencies | |
| run: | | |
| for i in 1 2 3; do | |
| bun install --frozen-lockfile && exit 0 | |
| echo "bun install attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::bun install failed after 3 attempts" | |
| exit 1 | |
| working-directory: cli | |
| - name: Build CLI | |
| run: bun run build | |
| working-directory: cli | |
| - name: Publish to npm | |
| run: npm publish --access public --provenance | |
| working-directory: cli | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # ────────────────────────────────────────────────── | |
| # Quality Gate Summary | |
| # ────────────────────────────────────────────────── | |
| quality-gate: | |
| name: Quality Gate | |
| runs-on: ubuntu-latest | |
| needs: [lint, docs-parity, security, test, e2e, container-scan, sast, performance, build] | |
| if: always() | |
| steps: | |
| - name: Check all gates | |
| run: | | |
| echo "=== Quality Gate Summary ===" | |
| echo "Lint & Typecheck: ${{ needs.lint.result }}" | |
| echo "Unit Tests: ${{ needs.test.result }}" | |
| echo "E2E Tests: ${{ needs.e2e.result }}" | |
| echo "Container Scan: ${{ needs.container-scan.result }}" | |
| echo "SAST: ${{ needs.sast.result }}" | |
| echo "Build: ${{ needs.build.result }}" | |
| echo "Docs Parity: ${{ needs.docs-parity.result }}" | |
| echo "Security Gates: ${{ needs.security.result }}" | |
| echo "Perf Gate: ${{ needs.performance.result }}" | |
| if [[ "${{ needs.lint.result }}" != "success" ]] || \ | |
| [[ "${{ needs.docs-parity.result }}" != "success" ]] || \ | |
| [[ "${{ needs.security.result }}" != "success" ]] || \ | |
| [[ "${{ needs.test.result }}" != "success" ]] || \ | |
| [[ "${{ needs.performance.result }}" != "success" ]] || \ | |
| [[ "${{ needs.build.result }}" != "success" ]]; then | |
| echo "::error::Quality gate FAILED — required checks did not pass" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.e2e.result }}" != "success" ]]; then | |
| echo "::warning::E2E tests failed — review Playwright report" | |
| fi | |
| if [[ "${{ needs.container-scan.result }}" != "success" ]] || \ | |
| [[ "${{ needs.sast.result }}" != "success" ]]; then | |
| echo "::warning::Security scans reported issues — review artifacts" | |
| fi | |
| echo "All quality gates PASSED" |