diff --git a/.env.example b/.env.example index 796aa70..9325c3d 100644 --- a/.env.example +++ b/.env.example @@ -5,14 +5,14 @@ BETTER_AUTH_SECRET="supersecretkey" # Replace with a strong secret key BETTER_AUTH_URL="https://app.example.com" # Base URL of your app NEXT_PUBLIC_APP_URL="https://app.example.com" -GITHUB_CLIENT_ID="" -GITHUB_CLIENT_SECRET="" +GH_CLIENT_ID="" +GH_CLIENT_SECRET="" GROQ_API_KEY="" # Free key from https://console.groq.com/keys OPENAI_API_KEY="" # Optional β€” only needed when using the "openai" provider OPENAI_BASE_URL="" # Optional β€” override for OpenAI-compatible endpoints (e.g. OpenRouter) -GITHUB_WEBHOOK_SECRET="supersecretwebhooksecret" # Replace with a strong secret key +GH_WEBHOOK_SECRET="supersecretwebhooksecret" # Replace with a strong secret key INNGEST_EVENT_KEY="" INNGEST_SIGNING_KEY="" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d45488b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: CI / CD + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ci: + name: Lint, Type-check & Build + runs-on: ubuntu-latest + + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} + BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }} + NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL }} + GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} + GH_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }} + GH_WEBHOOK_SECRET: ${{ secrets.GH_WEBHOOK_SECRET }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} + INNGEST_EVENT_KEY: ${{ secrets.INNGEST_EVENT_KEY }} + INNGEST_SIGNING_KEY: ${{ secrets.INNGEST_SIGNING_KEY }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + run_install: false + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma Client + run: pnpm db:generate + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + deploy: + name: Deploy to Vercel (Production) + runs-on: ubuntu-latest + needs: ci + # Only deploy on direct pushes to main β€” not on pull requests + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + run_install: false + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Vercel CLI + run: npm i -g vercel@latest + + - name: Pull Vercel environment variables + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + + - name: Build project via Vercel + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy prebuilt project to Vercel + run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/deploy-vps.yml b/.github/workflows/deploy-vps.yml deleted file mode 100644 index ddeafc0..0000000 --- a/.github/workflows/deploy-vps.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Deploy VPS - -on: - push: - branches: - - main - workflow_dispatch: - -concurrency: - group: deploy-vps-production - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - env: - VPS_PORT: ${{ secrets.VPS_PORT != '' && secrets.VPS_PORT || '22' }} - DATABASE_URL: ${{ secrets.DATABASE_URL != '' && secrets.DATABASE_URL || 'postgresql://ci:ci@localhost:5432/codereviewai' }} - BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET != '' && secrets.BETTER_AUTH_SECRET || 'ci-build-secret-change-me-32-chars-min' }} - BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL != '' && secrets.BETTER_AUTH_URL || 'https://example.com' }} - NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL != '' && secrets.NEXT_PUBLIC_APP_URL || 'https://example.com' }} - GITHUB_CLIENT_ID: ${{ secrets.GITHUB_CLIENT_ID != '' && secrets.GITHUB_CLIENT_ID || 'ci-github-client-id' }} - GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET != '' && secrets.GITHUB_CLIENT_SECRET || 'ci-github-client-secret' }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY != '' && secrets.OPENAI_API_KEY || 'sk-ci-placeholder' }} - GITHUB_WEBHOOK_SECRET: ${{ secrets.GITHUB_WEBHOOK_SECRET != '' && secrets.GITHUB_WEBHOOK_SECRET || 'ci-webhook-secret' }} - INNGEST_EVENT_KEY: ${{ secrets.INNGEST_EVENT_KEY != '' && secrets.INNGEST_EVENT_KEY || 'ci-inngest-event-key' }} - INNGEST_SIGNING_KEY: ${{ secrets.INNGEST_SIGNING_KEY != '' && secrets.INNGEST_SIGNING_KEY || 'ci-inngest-signing-key' }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Generate Prisma client - run: pnpm db:generate - - - name: Lint - run: pnpm lint - - - name: Build - run: pnpm build - - - name: Configure SSH - run: | - set -euo pipefail - mkdir -p ~/.ssh - echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -p "$VPS_PORT" "${{ secrets.VPS_HOST }}" >> ~/.ssh/known_hosts - - - name: Ensure app directory exists - run: | - set -euo pipefail - ssh -p "$VPS_PORT" -i ~/.ssh/id_ed25519 "${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}" \ - "mkdir -p '${{ secrets.VPS_APP_DIR }}'" - - - name: Sync files to VPS - run: | - set -euo pipefail - rsync -az --delete \ - --exclude ".git" \ - --exclude "node_modules" \ - --exclude ".next" \ - --exclude ".env" \ - --exclude ".env.local" \ - --exclude ".env.production" \ - -e "ssh -p $VPS_PORT -i ~/.ssh/id_ed25519" \ - ./ "${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:${{ secrets.VPS_APP_DIR }}/" - - - name: Deploy on VPS - env: - DEPLOY_SHA: ${{ github.sha }} - run: | - set -euo pipefail - ssh -p "$VPS_PORT" -i ~/.ssh/id_ed25519 "${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}" \ - bash -s -- "$DEPLOY_SHA" "${{ secrets.VPS_APP_DIR }}" << 'EOF' - set -euo pipefail - DEPLOY_SHA="$1" - APP_DIR="$2" - - cd "$APP_DIR" - - echo "--- Writing deploy metadata ---" - echo "$DEPLOY_SHA" > .deploy_sha - date -u +"%Y-%m-%dT%H:%M:%SZ" > .deploy_time_utc - - echo "--- Building app image ---" - docker compose build --pull app - - echo "--- Pulling caddy image ---" - docker compose pull caddy || true - - echo "--- Running DB migrations ---" - docker compose run --rm app pnpm db:push - - echo "--- Swapping app container ---" - docker compose stop app || true - docker compose rm -f app || true - docker compose up -d --no-deps app - - echo "--- Ensuring caddy is up ---" - docker compose up -d caddy - - echo "--- Pruning old images ---" - docker image prune -f - - echo "Deployed commit: $DEPLOY_SHA" - echo "Deployed at (UTC):" - cat .deploy_time_utc - docker compose ps - EOF \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..746d1d9 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,19 @@ +echo "Running lint check..." + +pnpm run lint + +if [ $? -ne 0 ]; then + echo "Lint failed! Push aborted." + exit 1 +fi + +echo "Lint passed. Running build..." + +pnpm run build + +if [ $? -ne 0 ]; then + echo "Build failed! Push aborted." + exit 1 +fi + +echo "Lint and build succeeded. Proceeding with push." diff --git a/Dockerfile b/Dockerfile index 24c7d2b..201aace 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,10 +18,10 @@ ENV DATABASE_URL="postgresql://build:build@localhost:5432/codereviewai" \ BETTER_AUTH_SECRET="build-time-better-auth-secret-change-at-runtime" \ BETTER_AUTH_URL="https://example.com" \ NEXT_PUBLIC_APP_URL="https://example.com" \ - GITHUB_CLIENT_ID="build-github-client-id" \ - GITHUB_CLIENT_SECRET="build-github-client-secret" \ + GH_CLIENT_ID="build-github-client-id" \ + GH_CLIENT_SECRET="build-github-client-secret" \ OPENAI_API_KEY="sk-build-placeholder" \ - GITHUB_WEBHOOK_SECRET="build-github-webhook-secret" \ + GH_WEBHOOK_SECRET="build-github-webhook-secret" \ INNGEST_EVENT_KEY="build-inngest-event-key" \ INNGEST_SIGNING_KEY="build-inngest-signing-key" diff --git a/docs/vercel_deployment_epic.md b/docs/vercel_deployment_epic.md new file mode 100644 index 0000000..b465e09 --- /dev/null +++ b/docs/vercel_deployment_epic.md @@ -0,0 +1,715 @@ +# πŸš€ CodeReviewAI β€” Vercel Deployment Epic + +> **Goal:** Deploy the CodeReviewAI project to Vercel with full CI/CD via GitHub, ensuring all features β€” OAuth sign-in, webhook-triggered PR reviews, Inngest background jobs, tRPC API, and PostgreSQL persistence β€” work flawlessly in production. + +--- + +## πŸ“‹ Table of Contents + +1. [Architecture Overview](#1--architecture-overview) +2. [Prerequisites](#2--prerequisites) +3. [Phase 1 β€” Provision a Managed PostgreSQL Database](#phase-1--provision-a-managed-postgresql-database) +4. [Phase 2 β€” GitHub OAuth App Configuration](#phase-2--github-oauth-app-configuration) +5. [Phase 3 β€” Import Project into Vercel](#phase-3--import-project-into-vercel) +6. [Phase 4 β€” Environment Variables on Vercel](#phase-4--environment-variables-on-vercel) +7. [Phase 5 β€” Prisma on Vercel (Serverless)](#phase-5--prisma-on-vercel-serverless) +8. [Phase 6 β€” Inngest Setup for Vercel](#phase-6--inngest-setup-for-vercel) +9. [Phase 7 β€” GitHub Webhook for Automatic PR Reviews](#phase-7--github-webhook-for-automatic-pr-reviews) +10. [Phase 8 β€” CI/CD Pipeline with GitHub Actions](#phase-8--cicd-pipeline-with-github-actions) +11. [Phase 9 β€” Custom Domain (Optional)](#phase-9--custom-domain-optional) +12. [Phase 10 β€” Post-Deployment Verification Checklist](#phase-10--post-deployment-verification-checklist) +13. [Troubleshooting](#troubleshooting) +14. [Environment Variable Reference](#environment-variable-reference) + +--- + +## 1 β€” Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” PR opened/synced β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ GitHub β”‚ ──── webhook POST ──────────▢ β”‚ Vercel (Next.js) β”‚ +β”‚ Repos β”‚ β”‚ β”‚ +β”‚ β”‚ ◀── review comments (API) ─── β”‚ /api/webhooks/ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ github β”‚ + β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ /api/inngest ───▢│──▢ Inngest Cloud +β”‚ Browser β”‚ ──── HTTPS ──────────────────▢│ /api/trpc/[trpc] β”‚ (background jobs) +β”‚ (User) β”‚ β”‚ /api/auth/[...all] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ PostgreSQL (Neon / β”‚ + β”‚ Supabase / Railway) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ AI Provider β”‚ + β”‚ (Groq / OpenAI) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Key Differences from Docker Deployment + +| Concern | Docker (current) | Vercel (target) | +| ---------------- | ------------------------------------ | --------------------------------------------- | +| Runtime | Long-running Node.js server | Serverless functions (cold starts) | +| Database | Self-hosted PostgreSQL | Managed PostgreSQL (Neon / Supabase / Railway)| +| Background Jobs | Inngest Dev Server (local) | Inngest Cloud (managed) | +| TLS / Domain | Caddy reverse proxy | Vercel Edge Network (automatic HTTPS) | +| Build | `Dockerfile` + `docker-compose` | `vercel build` (auto-detected Next.js) | +| CI/CD | Manual `docker compose up` | Git push β†’ auto deploy via Vercel GitHub App | + +--- + +## 2 β€” Prerequisites + +Before you begin, ensure you have: + +- [ ] A [GitHub](https://github.com) account with admin access to the repo +- [ ] A [Vercel](https://vercel.com) account (free Hobby plan works) +- [ ] A managed PostgreSQL database (see Phase 1) +- [ ] An [Inngest Cloud](https://www.inngest.com) account (free tier available) +- [ ] An AI provider API key: + - **Groq** (default, free tier): [https://console.groq.com/keys](https://console.groq.com/keys) + - **OpenAI** (optional): [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys) +- [ ] `pnpm` installed locally (for testing): `npm i -g pnpm` + +--- + +## Phase 1 β€” Provision a Managed PostgreSQL Database + +Vercel serverless functions **cannot** run a local PostgreSQL container. You need an external managed database. Here are the recommended options: + +### Option A: Neon (Recommended for Vercel β€” free tier) + +1. Go to [https://neon.tech](https://neon.tech) and sign up. +2. Create a new project (e.g., `codereviewai`). +3. Neon gives you a connection string: + ``` + postgresql://:@.neon.tech/?sslmode=require + ``` +4. Copy this β€” you'll use it as `DATABASE_URL`. + +> **Tip:** Neon has a Vercel integration β€” install it from the Vercel Integrations Marketplace to auto-inject `DATABASE_URL`. + +### Option B: Supabase (free tier) + +1. Go to [https://supabase.com](https://supabase.com) and create a project. +2. Navigate to **Settings β†’ Database β†’ Connection string β†’ URI**. +3. Copy the connection string (use the **connection pooler** URI for serverless β€” port `6543`). + +### Option C: Railway + +1. Go to [https://railway.app](https://railway.app) and create a PostgreSQL service. +2. Copy the `DATABASE_URL` from the service variables. + +### Apply the Schema + +After obtaining your `DATABASE_URL`, push the Prisma schema to the remote database: + +```bash +# Set the DATABASE_URL locally (one-time) +$env:DATABASE_URL = "postgresql://user:password@host/dbname?sslmode=require" + +# Push the schema +pnpm db:push +``` + +This creates all tables (`user`, `session`, `account`, `verification`, `Repository`, `Review`). + +--- + +## Phase 2 β€” GitHub OAuth App Configuration + +The app uses **better-auth** with GitHub as a social provider (scopes: `read:user`, `user:email`, `repo`). You need a GitHub OAuth App. + +### Step-by-Step + +1. Go to **GitHub β†’ Settings β†’ Developer settings β†’ OAuth Apps β†’ New OAuth App**. +2. Fill in: + + | Field | Value | + | ---------------------------- | ------------------------------------------------------ | + | **Application name** | `CodeReviewAI` | + | **Homepage URL** | `https://your-app.vercel.app` | + | **Authorization callback URL** | `https://your-app.vercel.app/api/auth/callback/github` | + +3. Click **Register application**. +4. Copy the **Client ID** β†’ this becomes `GH_CLIENT_ID`. +5. Click **Generate a new client secret** β†’ copy it β†’ this becomes `GH_CLIENT_SECRET`. + +> **Important:** After your first deployment, you'll know your actual Vercel URL (e.g., `https://aicodereviewer.vercel.app`). Come back and update the callback URL if needed. + +--- + +## Phase 3 β€” Import Project into Vercel + +### 3.1 Push Code to GitHub + +If not already done: + +```bash +git init +git add . +git commit -m "initial commit" +git remote add origin https://github.com//aicodereviewer.git +git branch -M main +git push -u origin main +``` + +### 3.2 Import into Vercel + +1. Go to [https://vercel.com/new](https://vercel.com/new). +2. Click **Import Git Repository** and select your `aicodereviewer` repo. +3. Vercel auto-detects **Next.js** as the framework. +4. **Framework Preset:** `Next.js` (auto-detected). +5. **Build Command:** Leave as default β€” Vercel runs `pnpm build` which executes `prisma generate && next build`. +6. **Output Directory:** Leave as default (`.next`). +7. **Install Command:** Vercel auto-detects `pnpm` from `pnpm-lock.yaml`. +8. **Before adding environment variables** (see Phase 4), click **Deploy** β€” the first build may fail, **that's OK**. We'll fix it next. + +### 3.3 Vercel Project Settings to Verify + +After import, go to **Project Settings β†’ General** and confirm: + +| Setting | Value | +| -------------------- | ---------------------- | +| Framework Preset | Next.js | +| Node.js Version | 20.x | +| Package Manager | pnpm | +| Root Directory | `./` (default) | +| Build Command | `prisma generate && next build` (from `package.json`) | + +--- + +## Phase 4 β€” Environment Variables on Vercel + +Go to **Project Settings β†’ Environment Variables** and add each variable below. + +### Required Variables + +| Variable | Description | Example Value | Environments | +| ------------------------- | ---------------------------------------- | ------------------------------------------------------ | -------------------- | +| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host.neon.tech/db?sslmode=require` | Production, Preview, Development | +| `BETTER_AUTH_SECRET` | Random secret for session encryption | Generate with: `openssl rand -base64 32` | Production, Preview | +| `BETTER_AUTH_URL` | Canonical app URL | `https://your-app.vercel.app` | Production | +| `NEXT_PUBLIC_APP_URL` | Public-facing app URL (client-side) | `https://your-app.vercel.app` | Production, Preview | +| `GH_CLIENT_ID` | GitHub OAuth App Client ID | `Iv1.abc123...` | Production, Preview | +| `GH_CLIENT_SECRET` | GitHub OAuth App Client Secret | `gho_xxxx...` | Production, Preview | +| `GH_WEBHOOK_SECRET` | Secret for verifying GitHub webhook payloads | Generate with: `openssl rand -hex 20` | Production, Preview | +| `GROQ_API_KEY` | Groq API key (default AI provider) | `gsk_xxxx...` | Production, Preview | +| `INNGEST_EVENT_KEY` | Inngest event key | From Inngest Cloud dashboard | Production, Preview | +| `INNGEST_SIGNING_KEY` | Inngest signing key | From Inngest Cloud dashboard | Production, Preview | + +### Optional Variables + +| Variable | Description | Example Value | +| ------------------------- | ---------------------------------------- | -------------------------- | +| `OPENAI_API_KEY` | OpenAI API key (if using OpenAI provider)| `sk-xxxx...` | +| `GOOGLE_AI_API_KEY` | Google Gemini API key (if configured) | `AIza...` | + +### How to Generate Secrets + +```bash +# For BETTER_AUTH_SECRET (32 bytes, base64) +openssl rand -base64 32 + +# For GH_WEBHOOK_SECRET (20 bytes, hex) +openssl rand -hex 20 +``` + +> **Preview Deployments:** For preview/PR deployments, set `NEXT_PUBLIC_APP_URL` to use Vercel's automatic preview URL. You can use Vercel's `VERCEL_URL` system variable β€” the auth config already handles this: +> ```ts +> const appUrl = process.env.BETTER_AUTH_URL +> || process.env.NEXT_PUBLIC_APP_URL +> || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"); +> ``` + +--- + +## Phase 5 β€” Prisma on Vercel (Serverless) + +### 5.1 Why This Matters + +Vercel serverless functions have a **read-only filesystem** after build. Prisma Client must be generated at build time. The project already handles this: + +- **Build command** in `package.json`: `"build": "prisma generate && next build"` +- **`serverExternalPackages`** in `next.config.ts` already includes `@prisma/client` and `prisma` + +### 5.2 Connection Pooling (Important for Serverless) + +Serverless functions create many short-lived connections. Without pooling, you'll exhaust your database connection limit. + +**If using Neon:** +- Use the **pooled connection string** (already uses PgBouncer): + ``` + postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/db?sslmode=require + ``` +- Neon's serverless driver works out of the box. + +**If using Supabase:** +- Use the **connection pooler** string (port `6543`): + ``` + postgresql://user:pass@host.supabase.co:6543/postgres?pgbouncer=true + ``` + +### 5.3 Prisma Caching on Vercel + +The project uses a global singleton pattern for Prisma Client (see `src/server/db/index.ts`), which is the correct approach for serverless: + +```ts +const globalPrismaClient = globalThis as unknown as { + prisma: ReturnType | undefined; +}; +export const db = globalPrismaClient.prisma ?? createPrismaClient(); +``` + +No changes needed here. + +--- + +## Phase 6 β€” Inngest Setup for Vercel + +Inngest handles background processing (the `review/pr.requested` event that triggers AI code reviews). In production on Vercel, you must use **Inngest Cloud** (the local dev server won't work). + +### 6.1 Create Inngest Cloud Account + +1. Go to [https://www.inngest.com](https://www.inngest.com) and sign up. +2. Create a new **App** (e.g., `codereviewai`). +3. From the dashboard, copy: + - **Event Key** β†’ `INNGEST_EVENT_KEY` + - **Signing Key** β†’ `INNGEST_SIGNING_KEY` +4. Add both to Vercel environment variables (Phase 4). + +### 6.2 Sync Inngest with Your Vercel App + +After your first successful deployment: + +1. In the Inngest Cloud dashboard, go to **Apps β†’ Syncs**. +2. Click **Sync New App** and enter your serve endpoint: + ``` + https://your-app.vercel.app/api/inngest + ``` +3. Inngest will discover your registered functions (`review-pr`). +4. You should see the function listed in the **Functions** tab. + +> **Important:** The serve endpoint (`/api/inngest`) is already configured in `src/app/api/inngest/route.ts`: +> ```ts +> export const { GET, POST, PUT } = serve({ +> client: inngest, +> functions, +> }); +> ``` + +### 6.3 Vercel-Inngest Integration (Alternative) + +Instead of manual sync, install the **Inngest Vercel Integration**: + +1. Go to [Vercel Integrations β†’ Inngest](https://vercel.com/integrations/inngest). +2. Click **Add Integration** β†’ select your project. +3. This auto-syncs on every deployment and sets `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` automatically. + +### 6.4 Function Timeout Considerations + +Vercel Hobby plan has a **10-second** function timeout. Vercel Pro has **60 seconds**. The `review-pr` function calls AI APIs and may take longer. + +**Solutions:** +- Inngest handles long-running functions by breaking them into **steps** β€” each step is a separate invocation with its own timeout. The `review-pr` function already uses steps correctly (`step.run(...)` pattern). +- Each `step.run()` gets its own 10s/60s timeout window, so even a multi-step review with AI calls will work within limits. +- If AI calls are slow (>10s per step), upgrade to Vercel **Pro** plan. + +--- + +## Phase 7 β€” GitHub Webhook for Automatic PR Reviews + +The webhook endpoint at `/api/webhooks/github` receives `pull_request` events and triggers the Inngest review pipeline. + +### 7.1 Create the Webhook + +1. Go to **GitHub β†’ Your Repo β†’ Settings β†’ Webhooks β†’ Add webhook**. +2. Configure: + + | Field | Value | + | ---------------- | ------------------------------------------------------ | + | **Payload URL** | `https://your-app.vercel.app/api/webhooks/github` | + | **Content type** | `application/json` | + | **Secret** | Same value as `GH_WEBHOOK_SECRET` env var | + | **Events** | Select **"Let me select individual events"** β†’ check only **Pull requests** | + | **Active** | βœ… checked | + +3. Click **Add webhook**. + +### 7.2 Organization-Wide Webhook (Optional) + +If you want automatic reviews for **all repos** in a GitHub organization: + +1. Go to **Organization β†’ Settings β†’ Webhooks β†’ Add webhook**. +2. Use the same configuration as above. +3. All repos in the org will send PR events to your app. + +### 7.3 Webhook Flow + +``` +GitHub PR opened β†’ POST /api/webhooks/github + β†’ Verify HMAC signature (GH_WEBHOOK_SECRET) + β†’ Find repository in DB + β†’ Create Review record (status: PENDING) + β†’ Send Inngest event "review/pr.requested" + β†’ Inngest Cloud invokes review-pr function + β†’ Fetch PR files from GitHub API + β†’ Send to AI provider (Groq/OpenAI) + β†’ Save review result to DB (status: COMPLETED) +``` + +--- + +## Phase 8 β€” CI/CD Pipeline with GitHub Actions + +Vercel's GitHub integration provides automatic deployments on push/PR. For additional CI (linting, type-checking), add a GitHub Actions workflow. + +### 8.1 Create the Workflow File + +Create `.github/workflows/ci.yml`: + +```yaml +name: CI / CD + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + # Build-time placeholders (same idea as Dockerfile) + DATABASE_URL: "postgresql://build:build@localhost:5432/codereviewai" + BETTER_AUTH_SECRET: "ci-placeholder-secret" + BETTER_AUTH_URL: "https://example.com" + NEXT_PUBLIC_APP_URL: "https://example.com" + GH_CLIENT_ID: "ci-github-client-id" + GH_CLIENT_SECRET: "ci-github-client-secret" + OPENAI_API_KEY: "sk-ci-placeholder" + GH_WEBHOOK_SECRET: "ci-webhook-secret" + INNGEST_EVENT_KEY: "ci-inngest-event-key" + INNGEST_SIGNING_KEY: "ci-inngest-signing-key" + GROQ_API_KEY: "gsk-ci-placeholder" + +jobs: + ci: + name: Lint, Type-check & Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma Client + run: pnpm db:generate + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build +``` + +### 8.2 Vercel Auto-Deploy (Already Built-In) + +When you connect your GitHub repo to Vercel: + +- **Every push to `main`** β†’ triggers a **Production** deployment. +- **Every pull request** β†’ triggers a **Preview** deployment with a unique URL. +- Preview URLs are posted as a comment on the PR automatically. + +No additional configuration needed β€” this is handled by the Vercel GitHub App. + +### 8.3 Branch Protection Rules (Recommended) + +Go to **GitHub β†’ Repo β†’ Settings β†’ Branches β†’ Branch protection rules β†’ Add rule** for `main`: + +- [x] Require a pull request before merging +- [x] Require status checks to pass before merging + - Add: `ci` (from the GitHub Actions workflow) + - Add: `Vercel` (from Vercel's deployment check) +- [x] Require branches to be up to date before merging + +--- + +## Phase 9 β€” Custom Domain (Optional) + +### 9.1 Add Domain in Vercel + +1. Go to **Project Settings β†’ Domains**. +2. Click **Add Domain** and enter your domain (e.g., `codereview.yourdomain.com`). +3. Vercel provides DNS records to configure. + +### 9.2 Update DNS + +Add the DNS records at your registrar: + +| Type | Name | Value | +| ----- | ----------------------- | ------------------------------ | +| CNAME | `codereview` | `cname.vercel-dns.com` | + +Or for apex domain: +| Type | Name | Value | +| ----- | ---- | ------------------ | +| A | `@` | `76.76.21.21` | + +### 9.3 Update Environment Variables + +After adding a custom domain, update: +- `BETTER_AUTH_URL` β†’ `https://codereview.yourdomain.com` +- `NEXT_PUBLIC_APP_URL` β†’ `https://codereview.yourdomain.com` +- GitHub OAuth App callback URL β†’ `https://codereview.yourdomain.com/api/auth/callback/github` +- GitHub Webhook Payload URL β†’ `https://codereview.yourdomain.com/api/webhooks/github` + +Trigger a redeployment after updating env vars. + +--- + +## Phase 10 β€” Post-Deployment Verification Checklist + +After your first successful deployment, verify each feature: + +### βœ… Basic + +- [ ] App loads at `https://your-app.vercel.app` without errors +- [ ] Landing page renders correctly +- [ ] Light/dark theme toggle works + +### βœ… Authentication + +- [ ] Sign up with email/password works +- [ ] Sign in with GitHub OAuth works +- [ ] GitHub OAuth redirects back correctly (no callback URL mismatch) +- [ ] Session persists after page refresh +- [ ] Sign out works + +### βœ… Repository Management + +- [ ] Connect GitHub button appears after sign-in +- [ ] GitHub repos are listed after connecting +- [ ] Can add a repo to the dashboard +- [ ] Repo detail page loads at `/repos/[id]` + +### βœ… Pull Request Reviews + +- [ ] Open a PR on a connected repo +- [ ] Webhook delivery shows `200` in GitHub webhook settings β†’ Recent Deliveries +- [ ] Review record appears in the database (status: PENDING β†’ PROCESSING β†’ COMPLETED) +- [ ] Inngest Cloud dashboard shows the function execution +- [ ] Review result appears on the PR detail page +- [ ] AI-generated summary, risk score, and comments are populated + +### βœ… Analytics + +- [ ] Analytics page loads with chart data +- [ ] Stats cards show correct counts +- [ ] Activity heatmap renders + +### βœ… API + +- [ ] tRPC endpoints respond (`/api/trpc/*`) +- [ ] Auth endpoints respond (`/api/auth/*`) +- [ ] Inngest endpoint responds (`/api/inngest` β€” returns registration info on GET) + +--- + +## Troubleshooting + +### Build Fails with "Module evaluation error" + +**Cause:** Next.js evaluates server modules at build time, and environment variables are missing. + +**Fix:** The `next.config.ts` already marks these as `serverExternalPackages`: +```ts +serverExternalPackages: ["@prisma/client", "prisma", "better-auth", "inngest"], +``` +Ensure all required env vars are set in Vercel **before** building. Vercel injects env vars at build time. + +--- + +### `PrismaClientInitializationError: Can't reach database server` + +**Cause:** `DATABASE_URL` is wrong or the database is not accessible from Vercel's network. + +**Fix:** +1. Verify `DATABASE_URL` is correct in Vercel env vars. +2. Ensure SSL is enabled (`?sslmode=require` in the URL). +3. Check that your database provider allows connections from any IP (Vercel uses dynamic IPs). +4. Neon/Supabase/Railway allow this by default. + +--- + +### GitHub OAuth Callback Fails (redirect_uri_mismatch) + +**Cause:** The OAuth callback URL in GitHub doesn't match the actual Vercel deployment URL. + +**Fix:** +1. Go to GitHub β†’ Developer settings β†’ OAuth Apps β†’ your app. +2. Update **Authorization callback URL** to: `https://your-actual-vercel-url.vercel.app/api/auth/callback/github` +3. Ensure `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` match the same URL. + +--- + +### Inngest Functions Not Triggering + +**Cause:** Inngest Cloud not synced with your app. + +**Fix:** +1. Visit `https://your-app.vercel.app/api/inngest` in a browser β€” you should see a JSON response with registered functions. +2. In Inngest Cloud dashboard β†’ Apps β†’ click **Sync** and enter the URL above. +3. Verify `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` are set correctly. + +--- + +### Webhook Returns 401 (Invalid Signature) + +**Cause:** `GH_WEBHOOK_SECRET` in Vercel doesn't match the secret configured in GitHub webhook settings. + +**Fix:** Ensure the exact same secret string is used in both places. Regenerate if unsure: +```bash +openssl rand -hex 20 +``` +Update both GitHub webhook settings and Vercel env var, then redeploy. + +--- + +### Function Timeout on Vercel Hobby Plan + +**Cause:** AI API calls exceed 10-second limit. + +**Fix:** +- The `review-pr` function uses Inngest steps, which retry individually. Each step gets its own timeout window. +- If single AI calls exceed 10s, upgrade to Vercel Pro (60s timeout) or switch to a faster AI model (e.g., Groq `llama-3.1-8b-instant` instead of `llama-3.3-70b-versatile`). + +--- + +### Preview Deployments: Auth Doesn't Work + +**Cause:** Preview URLs are dynamic, and `BETTER_AUTH_URL` points to production. + +**Fix:** The auth config already handles this by falling back to `VERCEL_URL`: +```ts +const appUrl = process.env.BETTER_AUTH_URL + || process.env.NEXT_PUBLIC_APP_URL + || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"); +``` +Make sure `BETTER_AUTH_URL` is only set for the **Production** environment in Vercel (not Preview). + +--- + +## Environment Variable Reference + +### Complete `.env.example` + +```env +# ─── Database ───────────────────────────────────────────── +DATABASE_URL="postgresql://user:password@host.neon.tech/codereviewai?sslmode=require" + +# ─── Auth (better-auth) ────────────────────────────────── +BETTER_AUTH_SECRET="generate-with-openssl-rand-base64-32" +BETTER_AUTH_URL="https://your-app.vercel.app" +NEXT_PUBLIC_APP_URL="https://your-app.vercel.app" + +# ─── GitHub OAuth ───────────────────────────────────────── +GH_CLIENT_ID="your-github-oauth-client-id" +GH_CLIENT_SECRET="your-github-oauth-client-secret" + +# ─── GitHub Webhook ─────────────────────────────────────── +GH_WEBHOOK_SECRET="generate-with-openssl-rand-hex-20" + +# ─── AI Providers ───────────────────────────────────────── +GROQ_API_KEY="gsk_your_groq_api_key" +# OPENAI_API_KEY="sk-your-openai-key" # optional +# GOOGLE_AI_API_KEY="AIza..." # optional + +# ─── Inngest ────────────────────────────────────────────── +INNGEST_EVENT_KEY="your-inngest-event-key" +INNGEST_SIGNING_KEY="your-inngest-signing-key" +``` + +--- + +## Quick-Start Summary (TL;DR) + +```bash +# 1. Provision database (Neon recommended) +# Copy DATABASE_URL + +# 2. Push schema to database +pnpm db:push + +# 3. Create GitHub OAuth App +# Callback URL: https://your-app.vercel.app/api/auth/callback/github + +# 4. Import repo into Vercel (vercel.com/new) + +# 5. Set all env vars in Vercel dashboard + +# 6. Trigger redeploy +# Vercel β†’ Deployments β†’ Redeploy + +# 7. Create Inngest Cloud account +# Sync app: https://your-app.vercel.app/api/inngest + +# 8. Add GitHub webhook +# Payload URL: https://your-app.vercel.app/api/webhooks/github +# Events: Pull requests only + +# 9. Open a PR on a connected repo β†’ review happens automatically! πŸŽ‰ +``` + +--- + +## CI/CD Flow Diagram + +``` +Developer pushes code + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ GitHub Actions CI β”‚ +β”‚ - pnpm install β”‚ +β”‚ - prisma generate β”‚ +β”‚ - pnpm lint β”‚ +β”‚ - pnpm build β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ βœ… Pass + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Vercel Auto-Deploy β”‚ +β”‚ - Install deps β”‚ +β”‚ - prisma generate β”‚ +β”‚ - next build β”‚ +β”‚ - Deploy to Edge β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Production Live β”‚ +β”‚ - Inngest synced β”‚ +β”‚ - Webhooks active β”‚ +β”‚ - Auth working β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +*Last updated: March 2026* diff --git a/package.json b/package.json index ac15a75..6dc4495 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "start": "next start", "lint": "eslint", "db:push": "prisma db push", - "db:generate": "prisma generate" + "db:generate": "prisma generate", + "prepare": "husky" }, "dependencies": { "@google/genai": "^1.43.0", @@ -45,6 +46,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.1.6", + "husky": "^9.1.7", "prisma": "^6.19.2", "shadcn": "^3.8.4", "tailwindcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 863a160..d559f40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: eslint-config-next: specifier: 16.1.6 version: 16.1.6(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + husky: + specifier: ^9.1.7 + version: 9.1.7 prisma: specifier: ^6.19.2 version: 6.19.2(typescript@5.9.3) @@ -3483,6 +3486,11 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -8894,6 +8902,8 @@ snapshots: human-signals@8.0.1: {} + husky@9.1.7: {} + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx b/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx index 0ede970..24b3d47 100644 --- a/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx +++ b/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx @@ -1,11 +1,10 @@ "use client"; -import { use, useState, useMemo, useEffect } from "react"; +import { use, useState, useMemo } from "react"; import Link from "next/link"; import { trpc } from "@/lib/trpc/client"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { @@ -56,30 +55,32 @@ export default function ReviewComparePage({ params }: PageProps) { { enabled: !Number.isNaN(prNum) }, ); - const reviews = reviewsQuery.data ?? []; + const reviews = useMemo(() => reviewsQuery.data ?? [], [reviewsQuery.data]); const [baselineId, setBaselineId] = useState(""); const [currentId, setCurrentId] = useState(""); // Default to oldest (baseline) and newest (current) when data loads - useEffect(() => { - if (reviews.length === 0) return; + const resolvedBaselineId = useMemo(() => { + if (reviews.length === 0) return baselineId; const newest = reviews[0]; const oldest = reviews[reviews.length - 1]; - if (reviews.length === 1) { - setBaselineId(newest.id); - setCurrentId(newest.id); - } else { - setBaselineId((prev) => - !prev || !reviews.some((r) => r.id === prev) ? oldest.id : prev, - ); - setCurrentId((prev) => - !prev || !reviews.some((r) => r.id === prev) ? newest.id : prev, - ); - } - }, [reviews]); + if (reviews.length === 1) return newest.id; + return !baselineId || !reviews.some((r) => r.id === baselineId) + ? oldest.id + : baselineId; + }, [reviews, baselineId]); - const baseline = reviews.find((r) => r.id === baselineId); - const current = reviews.find((r) => r.id === currentId); + const resolvedCurrentId = useMemo(() => { + if (reviews.length === 0) return currentId; + const newest = reviews[0]; + if (reviews.length === 1) return newest.id; + return !currentId || !reviews.some((r) => r.id === currentId) + ? newest.id + : currentId; + }, [reviews, currentId]); + + const baseline = reviews.find((r) => r.id === resolvedBaselineId); + const current = reviews.find((r) => r.id === resolvedCurrentId); const comparison = useMemo(() => { if (!baseline || !current) return null; @@ -96,7 +97,7 @@ export default function ReviewComparePage({ params }: PageProps) { const isLoading = pr.isLoading || reviewsQuery.isLoading; const isInvalidPr = pr.error || !pr.data; const hasEnoughReviews = reviews.length >= 2; - const sameSelection = baselineId && currentId && baselineId === currentId; + const sameSelection = resolvedBaselineId && resolvedCurrentId && resolvedBaselineId === resolvedCurrentId; if (isLoading) { return ( @@ -231,7 +232,7 @@ export default function ReviewComparePage({ params }: PageProps) { setCurrentId(e.target.value)} className={cn( "flex h-11 w-full rounded-lg border border-input bg-background px-4 py-2.5 text-sm", @@ -379,7 +380,6 @@ export default function ReviewComparePage({ params }: PageProps) { key={`fixed-${i}`} comment={comment} variant="fixed" - index={i} /> ))} @@ -400,7 +400,6 @@ export default function ReviewComparePage({ params }: PageProps) { key={`new-${i}`} comment={comment} variant="new" - index={i} /> ))} @@ -421,7 +420,6 @@ export default function ReviewComparePage({ params }: PageProps) { key={`unchanged-${i}`} comment={comment} variant="unchanged" - index={i} /> ))} diff --git a/src/app/api/webhooks/github/route.ts b/src/app/api/webhooks/github/route.ts index 308088d..b5dfa34 100644 --- a/src/app/api/webhooks/github/route.ts +++ b/src/app/api/webhooks/github/route.ts @@ -21,9 +21,9 @@ interface PullRequestPayload { } function verifySignature(payload: string, signature: string | null): boolean { - const secret = process.env.GITHUB_WEBHOOK_SECRET; + const secret = process.env.GH_WEBHOOK_SECRET; if (!secret) { - console.warn("GITHUB_WEBHOOK_SECRET not set, skipping verification"); + console.warn("GH_WEBHOOK_SECRET not set, skipping verification"); return true; } diff --git a/src/components/review-comparison-card.tsx b/src/components/review-comparison-card.tsx index 48b8963..0932481 100644 --- a/src/components/review-comparison-card.tsx +++ b/src/components/review-comparison-card.tsx @@ -1,6 +1,5 @@ "use client"; -import React from "react"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { @@ -15,6 +14,7 @@ import { Paintbrush, Lightbulb, } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import type { ReviewCommentInput } from "@/lib/review-comparison"; @@ -47,27 +47,17 @@ function getSeverityStyle(severity: string) { return SEVERITY_STYLES[severity] ?? SEVERITY_STYLES.low; } -function getCategoryIcon(category?: string) { - switch (category) { - case "bug": - return Bug; - case "security": - return Shield; - case "performance": - return Zap; - case "style": - return Paintbrush; - case "suggestion": - return Lightbulb; - default: - return CircleDot; - } -} +const CATEGORY_ICONS: Record = { + bug: Bug, + security: Shield, + performance: Zap, + style: Paintbrush, + suggestion: Lightbulb, +}; export interface ComparisonCommentCardProps { comment: ReviewCommentInput; variant: "fixed" | "new" | "unchanged"; - index: number; } /** @@ -76,10 +66,9 @@ export interface ComparisonCommentCardProps { export function ComparisonCommentCard({ comment, variant, - index, }: ComparisonCommentCardProps) { const severityStyle = getSeverityStyle(comment.severity); - const CategoryIcon = getCategoryIcon(comment.category); + const CategoryIcon = comment.category ? CATEGORY_ICONS[comment.category] ?? CircleDot : CircleDot; const variantConfig = { fixed: { diff --git a/src/components/user-menu.tsx b/src/components/user-menu.tsx index 32c7f0e..854af4c 100644 --- a/src/components/user-menu.tsx +++ b/src/components/user-menu.tsx @@ -8,12 +8,11 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { LogOut, Settings, User, ChevronDown } from "lucide-react"; -import { use } from "react"; interface UserProps { id: string; diff --git a/src/server/auth/index.ts b/src/server/auth/index.ts index 57d7526..1922930 100644 --- a/src/server/auth/index.ts +++ b/src/server/auth/index.ts @@ -21,8 +21,8 @@ export const auth = betterAuth({ }, socialProviders: { github: { - clientId: process.env.GITHUB_CLIENT_ID ?? "", - clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "", + clientId: process.env.GH_CLIENT_ID ?? "", + clientSecret: process.env.GH_CLIENT_SECRET ?? "", scope: ["read:user", "user:email", "repo"], }, },