From 20795030a5bfc348d29457c1e8ba3b72c88772f1 Mon Sep 17 00:00:00 2001 From: OM CHOKSI Date: Mon, 22 Jun 2026 13:43:17 +0530 Subject: [PATCH] deploy: add Render services configuration --- README.md | 32 ++++-- deploy/render/README.md | 17 ++++ deploy/render/api.Dockerfile | 19 ++++ deploy/render/api_start.sh | 3 + deploy/render/dashboard.Dockerfile | 16 +++ deploy/render/dashboard_start.sh | 3 + deploy/render/review-ui.Dockerfile | 16 +++ deploy/render/review_ui_start.sh | 3 + docs/render_deployment.md | 153 +++++++++++++++++++++++++++++ render.yaml | 44 +++++++++ scripts/check_render_deployment.py | 130 ++++++++++++++++++++++++ ui/app.py | 3 +- 12 files changed, 429 insertions(+), 10 deletions(-) create mode 100644 deploy/render/README.md create mode 100644 deploy/render/api.Dockerfile create mode 100755 deploy/render/api_start.sh create mode 100644 deploy/render/dashboard.Dockerfile create mode 100755 deploy/render/dashboard_start.sh create mode 100644 deploy/render/review-ui.Dockerfile create mode 100755 deploy/render/review_ui_start.sh create mode 100644 docs/render_deployment.md create mode 100644 render.yaml create mode 100755 scripts/check_render_deployment.py diff --git a/README.md b/README.md index a938a3d..a5fadf3 100644 --- a/README.md +++ b/README.md @@ -122,17 +122,31 @@ The demo runs the rules-only engine (no RAG, no auth, single-user). --- -## Deployment Architecture +## Deployment -| Component | Host | Purpose | -|-----------------|--------------------|---------------------------------| -| Demo (rules) | Hugging Face Space | Public lightweight playground | -| API + Dashboard | Render / Railway | Production backend | -| Database | MongoDB Atlas | Users, reviews, usage | -| Email | Resend | Notifications | -| GitHub App | GitHub Marketplace | Final PR integration | +| Component | Host | Status | +|---|---|---| +| RAG Service | Hugging Face Space | **Live** | +| API + Dashboard | Render | **Ready to deploy** | +| Database | MongoDB Atlas | Designed, not deployed | +| Email | Resend | Designed, not deployed | +| GitHub App | GitHub Marketplace | Planned | -See [docs/deployment_strategy.md](docs/deployment_strategy.md) for details. +### Render Deployment (3 services) + +```bash +# Verify deployed services +export RENDER_API_URL=https://codesec-api.onrender.com +export RENDER_DASHBOARD_URL=https://codesec-dashboard.onrender.com +export RENDER_REVIEW_UI_URL=https://codesec-review-ui.onrender.com +python scripts/check_render_deployment.py +``` + +Render blueprint: `render.yaml` + +Full instructions: [docs/render_deployment.md](docs/render_deployment.md) + +See also [docs/deployment_strategy.md](docs/deployment_strategy.md) for the full multi-environment plan. --- diff --git a/deploy/render/README.md b/deploy/render/README.md new file mode 100644 index 0000000..d106915 --- /dev/null +++ b/deploy/render/README.md @@ -0,0 +1,17 @@ +# Render Deployment + +These files configure CodeSecAudit AI for deployment on Render. + +## Services + +| Service | Dockerfile | Start Script | Purpose | +|---|---|---|---| +| API | `api.Dockerfile` | `api_start.sh` | FastAPI review + history endpoints | +| Dashboard | `dashboard.Dockerfile` | `dashboard_start.sh` | Streamlit analytics dashboard | +| Review UI | `review-ui.Dockerfile` | `review_ui_start.sh` | Streamlit review interface | + +## Blueprint + +The root `render.yaml` defines all three services as a blueprint group. + +See [docs/render_deployment.md](../../docs/render_deployment.md) for full instructions. diff --git a/deploy/render/api.Dockerfile b/deploy/render/api.Dockerfile new file mode 100644 index 0000000..96f9d67 --- /dev/null +++ b/deploy/render/api.Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY pyproject.toml . +COPY config/ config/ +COPY review_engine/ review_engine/ +COPY review_store/ review_store/ +COPY api/ api/ + +RUN pip install --no-cache-dir -e ".[api]" && \ + rm -rf /root/.cache + +COPY deploy/render/api_start.sh /api_start.sh +RUN chmod +x /api_start.sh + +EXPOSE 8003 + +CMD ["/api_start.sh"] diff --git a/deploy/render/api_start.sh b/deploy/render/api_start.sh new file mode 100755 index 0000000..9e07afc --- /dev/null +++ b/deploy/render/api_start.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +uvicorn api.main:app --host 0.0.0.0 --port "${PORT:-8003}" diff --git a/deploy/render/dashboard.Dockerfile b/deploy/render/dashboard.Dockerfile new file mode 100644 index 0000000..28b07c7 --- /dev/null +++ b/deploy/render/dashboard.Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY pyproject.toml . +COPY ui/dashboard.py ui/dashboard.py + +RUN pip install --no-cache-dir -e ".[ui]" && \ + rm -rf /root/.cache + +COPY deploy/render/dashboard_start.sh /dashboard_start.sh +RUN chmod +x /dashboard_start.sh + +EXPOSE 8502 + +CMD ["/dashboard_start.sh"] diff --git a/deploy/render/dashboard_start.sh b/deploy/render/dashboard_start.sh new file mode 100755 index 0000000..8bbd715 --- /dev/null +++ b/deploy/render/dashboard_start.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +streamlit run ui/dashboard.py --server.address 0.0.0.0 --server.port "${PORT:-8502}" --server.headless true diff --git a/deploy/render/review-ui.Dockerfile b/deploy/render/review-ui.Dockerfile new file mode 100644 index 0000000..93b5eb5 --- /dev/null +++ b/deploy/render/review-ui.Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY pyproject.toml . +COPY ui/app.py ui/app.py + +RUN pip install --no-cache-dir -e ".[ui]" && \ + rm -rf /root/.cache + +COPY deploy/render/review_ui_start.sh /review_ui_start.sh +RUN chmod +x /review_ui_start.sh + +EXPOSE 8501 + +CMD ["/review_ui_start.sh"] diff --git a/deploy/render/review_ui_start.sh b/deploy/render/review_ui_start.sh new file mode 100755 index 0000000..378e2cf --- /dev/null +++ b/deploy/render/review_ui_start.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -e +streamlit run ui/app.py --server.address 0.0.0.0 --server.port "${PORT:-8501}" --server.headless true diff --git a/docs/render_deployment.md b/docs/render_deployment.md new file mode 100644 index 0000000..33bcf72 --- /dev/null +++ b/docs/render_deployment.md @@ -0,0 +1,153 @@ +# Render Deployment + +Deploy CodeSecAudit AI on Render with three services: API, Dashboard, and Review UI. + +The RAG retrieval service remains on Hugging Face Space (already deployed at `https://OMCHOKSI108-codereview-agent.hf.space`). + +## Option A: Blueprint Deploy (Recommended) + +1. Push the repository to GitHub. +2. Log in to [Render Dashboard](https://dashboard.render.com). +3. Click **New → Blueprint**. +4. Select your repository (`OMCHOKSI108/codesec-audit-dataset`). +5. Render reads `render.yaml` and creates three services: + + | Service | Name | Purpose | + |---|---|---| + | API | `codesec-api` | FastAPI review + history endpoints | + | Dashboard | `codesec-dashboard` | Streamlit analytics dashboard | + | Review UI | `codesec-review-ui` | Streamlit review interface | + +6. **Add the secret** `CODESEC_RAG_API_KEY` in the Render dashboard for `codesec-api` (under Environment → Secret Files). This is the API key shared with the HF Space RAG service. +7. **Update** `CODESEC_API_URL` on `codesec-dashboard` and `codesec-review-ui` to the actual URL of your deployed API service (e.g., `https://codesec-api.onrender.com`). This is set automatically by the blueprint but may need a suffix if the URL differs. +8. Click **Apply** and wait for the build (~3-5 min per service). + +## Option B: Manual Service Creation + +### 1. Create the API Service + +| Setting | Value | +|---|---| +| **Type** | Web Service | +| **Name** | `codesec-api` | +| **Environment** | Docker | +| **Dockerfile Path** | `deploy/render/api.Dockerfile` | +| **Plan** | Free | +| **Health Check Path** | `/health` | + +**Required env vars**: + +```env +APP_ENV=production +CODESEC_ENABLE_RAG=true +CODESEC_RAG_MODE=remote +CODESEC_RAG_SERVICE_URL=https://OMCHOKSI108-codereview-agent.hf.space +CODESEC_RAG_API_KEY= +CODESEC_DEFAULT_TOP_K=3 +DATABASE_BACKEND=sqlite +CODESEC_DB_PATH=/tmp/reviews.db +OWNER_CONTACT_EMAIL=omchoksi108@gmail.com +``` + +`CODESEC_RAG_API_KEY` must be set as a **secret** (not plain text). It must match the `RAG_API_KEY` set on the Hugging Face Space. + +### 2. Create the Dashboard Service + +| Setting | Value | +|---|---| +| **Type** | Web Service | +| **Name** | `codesec-dashboard` | +| **Environment** | Docker | +| **Dockerfile Path** | `deploy/render/dashboard.Dockerfile` | +| **Plan** | Free | + +**Required env vars**: + +```env +CODESEC_API_URL=https://codesec-api.onrender.com +``` + +Replace with your actual API URL after the API service deploys. + +### 3. Create the Review UI Service + +| Setting | Value | +|---|---| +| **Type** | Web Service | +| **Name** | `codesec-review-ui` | +| **Environment** | Docker | +| **Dockerfile Path** | `deploy/render/review-ui.Dockerfile` | +| **Plan** | Free | + +**Required env vars**: + +```env +CODESEC_API_URL=https://codesec-api.onrender.com +``` + +## Verify Deployment + +After all services deploy, run the smoke test: + +```bash +export RENDER_API_URL=https://codesec-api.onrender.com +export RENDER_DASHBOARD_URL=https://codesec-dashboard.onrender.com +export RENDER_REVIEW_UI_URL=https://codesec-review-ui.onrender.com + +python scripts/check_render_deployment.py +``` + +Expected output: + +``` +[1/6] API /health .................... PASS +[2/6] API / .......................... PASS +[3/6] API /review/code ............... PASS +[4/6] API /stats ..................... PASS +[5/6] Dashboard URL .................. PASS +[6/6] Review UI URL .................. PASS +All checks passed. +``` + +## Architecture + +```text +┌─────────────────────────────┐ +│ Hugging Face Space │ +│ codesec-rag-service │ +│ /rag/search (RAG index) │ +│ 2,833 OWASP chunks │ +└──────────┬──────────────────┘ + │ HTTP (outbound from Render) + ▼ +┌─────────────────────────────┐ +│ Render — codesec-api │ +│ FastAPI review + history │ +│ SQLite at /tmp/reviews.db │ +│ Health: /health │ +└──────────┬──────────────────┘ + │ + ┌─────┴─────┐ + ▼ ▼ +┌─────────┐ ┌─────────┐ +│ Dashboard│ │Review UI│ +│streamlit │ │streamlit│ +└──────────┘ └─────────┘ +``` + +## Known Limitations on Render Free + +| Limitation | Impact | Mitigation | +|---|---|---| +| **Spin-down after inactivity** | First request after idle period takes 30-60s | Set up external uptime monitor (e.g., UptimeRobot, cron-job.org) | +| **Ephemeral filesystem** | SQLite DB (`/tmp/reviews.db`) resets on each deploy or restart | Migrate to MongoDB Atlas for production persistence | +| **512 MB RAM** | May limit concurrent request handling | Rule-based engine is lightweight (~100 MB idle) | +| **No cron jobs on free** | Cannot run periodic tasks | Use external cron services if needed | + +## Production Readiness Next Steps + +1. **MongoDB Atlas** — Replace SQLite with MongoDB for persistent review storage. +2. **Resend Email** — Set `RESEND_API_KEY` and enable email workflows. +3. **GitHub App** — Register a GitHub App and set `GITHUB_*` env vars. +4. **Custom Domain** — Add a custom domain in Render dashboard. +5. **Paid Plan** — Upgrade from free to starter/individual for no spin-down. diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..6865a63 --- /dev/null +++ b/render.yaml @@ -0,0 +1,44 @@ +services: + - type: web + name: codesec-api + env: docker + dockerfilePath: deploy/render/api.Dockerfile + plan: free + healthCheckPath: /health + envVars: + - key: APP_ENV + value: production + - key: CODESEC_ENABLE_RAG + value: "true" + - key: CODESEC_RAG_MODE + value: remote + - key: CODESEC_RAG_SERVICE_URL + value: https://OMCHOKSI108-codereview-agent.hf.space + - key: CODESEC_DEFAULT_TOP_K + value: "3" + - key: DATABASE_BACKEND + value: sqlite + - key: CODESEC_DB_PATH + value: /tmp/reviews.db + - key: OWNER_CONTACT_EMAIL + value: omchoksi108@gmail.com + - key: CODESEC_RAG_API_KEY + sync: false + + - type: web + name: codesec-dashboard + env: docker + dockerfilePath: deploy/render/dashboard.Dockerfile + plan: free + envVars: + - key: CODESEC_API_URL + value: https://codesec-api.onrender.com + + - type: web + name: codesec-review-ui + env: docker + dockerfilePath: deploy/render/review-ui.Dockerfile + plan: free + envVars: + - key: CODESEC_API_URL + value: https://codesec-api.onrender.com diff --git a/scripts/check_render_deployment.py b/scripts/check_render_deployment.py new file mode 100755 index 0000000..c262837 --- /dev/null +++ b/scripts/check_render_deployment.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +Smoke test for CodeSecAudit AI Render deployment. + +Tests: + - API /health + - API / + - API /review/code + - API /stats + - Dashboard URL + - Review UI URL + +Usage: + export RENDER_API_URL=https://codesec-api.onrender.com + export RENDER_DASHBOARD_URL=https://codesec-dashboard.onrender.com + export RENDER_REVIEW_UI_URL=https://codesec-review-ui.onrender.com + python scripts/check_render_deployment.py +""" + +import os +import sys +import json +import urllib.request +import urllib.error +import time + +PASS = "\033[92mPASS\033[0m" +FAIL = "\033[91mFAIL\033[0m" + + +def check_url(url: str, method: str = "GET", data: dict | None = None, + timeout: int = 30) -> tuple[bool, int, str | None]: + try: + body = json.dumps(data).encode() if data else None + req = urllib.request.Request(url, data=body, method=method) + if body: + req.add_header("Content-Type", "application/json") + start = time.time() + with urllib.request.urlopen(req, timeout=timeout) as resp: + elapsed = time.time() - start + content = resp.read().decode() + return True, resp.status, content[:200], elapsed + except urllib.error.HTTPError as e: + return False, e.code, str(e), 0 + except Exception as e: + return False, 0, str(e), 0 + + +def check(name: str, url: str, method: str = "GET", + data: dict | None = None, expect_status: int = 200) -> tuple[bool, str]: + ok, status, body, elapsed = check_url(url, method, data) + status_match = status == expect_status + passed = ok and status_match + + label = f"[{name}]" + dots = max(1, 60 - len(label)) + elapsed_str = f"{elapsed:.1f}s" if elapsed else "" + status_line = f"{label}{'.' * dots} {PASS if passed else FAIL}" + if elapsed_str: + status_line += f" ({elapsed_str})" + print(status_line) + + if not passed: + detail = body if isinstance(body, str) else str(body) + print(f" Expected {expect_status}, got {status}: {detail[:150]}") + + return passed + + +def main(): + api = os.getenv("RENDER_API_URL", "").rstrip("/") + dashboard = os.getenv("RENDER_DASHBOARD_URL", "").rstrip("/") + review_ui = os.getenv("RENDER_REVIEW_UI_URL", "").rstrip("/") + + if not api: + print("ERROR: RENDER_API_URL not set.") + print("Usage:") + print(" export RENDER_API_URL=https://codesec-api.onrender.com") + print(" export RENDER_DASHBOARD_URL=https://codesec-dashboard.onrender.com") + print(" export RENDER_REVIEW_UI_URL=https://codesec-review-ui.onrender.com") + print(" python scripts/check_render_deployment.py") + sys.exit(1) + + all_pass = True + + print(f"\nChecking Render deployment:\n") + print(f" API: {api}") + print(f" Dashboard: {dashboard or '(not set)'}") + print(f" Review UI: {review_ui or '(not set)'}\n") + + # 1. /health + all_pass &= check("1/6 API /health", f"{api}/health") + + # 2. / + all_pass &= check("2/6 API /", api) + + # 3. /review/code + all_pass &= check( + "3/6 API /review/code", + f"{api}/review/code", + method="POST", + data={"code": "eval(user_input)", "file_path": "test.py"}, + ) + + # 4. /stats + all_pass &= check("4/6 API /stats", f"{api}/stats") + + # 5. Dashboard URL + if dashboard: + all_pass &= check("5/6 Dashboard URL", dashboard) + else: + print(f"[5/6 Dashboard URL]...... SKIP (not set)") + + # 6. Review UI URL + if review_ui: + all_pass &= check("6/6 Review UI URL", review_ui) + else: + print(f"[6/6 Review UI URL]...... SKIP (not set)") + + print() + if all_pass: + print("All checks passed.") + sys.exit(0) + else: + print("Some checks failed.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/ui/app.py b/ui/app.py index e29d5ad..fe03209 100644 --- a/ui/app.py +++ b/ui/app.py @@ -1,8 +1,9 @@ import streamlit as st import requests import json +import os -API_URL = "http://localhost:8003" +API_URL = os.getenv("CODESEC_API_URL", "http://localhost:8003") TOP_K = 5 st.set_page_config(