Skip to content

feat: add AI code reviewer using Gemini API #68

feat: add AI code reviewer using Gemini API

feat: add AI code reviewer using Gemini API #68

Workflow file for this run

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"