feat(deployment): Add Docker support and deployment configuration
- Add Dockerfile for multi-stage build with Node.js and Nginx - Create docker-compose.yml for simplified container deployment - Add .dockerignore to optimize Docker build context - Configure nginx.conf with performance and security optimizations - Update README.md and README_EN.md with Docker deployment instructions - Enhance database configuration to support optional Supabase integration - Improve environment configuration handling for local and containerized environments Enables easier deployment and provides a standardized container-based setup for the application.
This commit is contained in:
parent
9b11e47b36
commit
93bac360f1
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 依赖
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# 构建产物
|
||||||
|
dist
|
||||||
|
.vite
|
||||||
|
|
||||||
|
# 环境变量(保留 .env 用于构建)
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# 编辑器
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# 操作系统
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# 文档
|
||||||
|
*.md
|
||||||
|
docs
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
.history
|
||||||
|
.cache
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 多阶段构建 - 构建阶段
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 禁用代理并安装 pnpm
|
||||||
|
ENV HTTP_PROXY=""
|
||||||
|
ENV HTTPS_PROXY=""
|
||||||
|
ENV http_proxy=""
|
||||||
|
ENV https_proxy=""
|
||||||
|
ENV NO_PROXY="*"
|
||||||
|
ENV no_proxy="*"
|
||||||
|
|
||||||
|
RUN npm config set registry https://registry.npmjs.org/ && \
|
||||||
|
npm config delete proxy 2>/dev/null || true && \
|
||||||
|
npm config delete https-proxy 2>/dev/null || true && \
|
||||||
|
npm config delete http-proxy 2>/dev/null || true && \
|
||||||
|
npm install -g pnpm
|
||||||
|
|
||||||
|
# 复制依赖文件
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
|
# 复制项目文件(包括 .env)
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建应用(环境变量会在构建时被读取)
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# 生产阶段 - 使用 nginx 提供静态文件服务
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# 复制自定义 nginx 配置
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 从构建阶段复制构建产物
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# 启动 nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
36
README.md
36
README.md
|
|
@ -49,14 +49,43 @@
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 环境要求
|
### 🐳 Docker 部署(推荐)
|
||||||
|
|
||||||
|
使用 Docker 可以快速部署应用,无需配置 Node.js 环境。
|
||||||
|
|
||||||
|
1. **克隆项目**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/lintsinghua/XCodeReviewer.git
|
||||||
|
cd XCodeReviewer
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **配置环境变量**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# 编辑 .env 文件,至少需要配置 VITE_GEMINI_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **构建并启动**
|
||||||
|
```bash
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **访问应用**
|
||||||
|
|
||||||
|
在浏览器中打开 `http://localhost:5174`
|
||||||
|
|
||||||
|
### 💻 本地开发部署
|
||||||
|
|
||||||
|
如果需要进行开发或自定义修改,可以使用本地部署方式。
|
||||||
|
|
||||||
|
#### 环境要求
|
||||||
|
|
||||||
- **Node.js**: `18+`
|
- **Node.js**: `18+`
|
||||||
- **pnpm**: `8+` (推荐) 或 `npm` / `yarn`
|
- **pnpm**: `8+` (推荐) 或 `npm` / `yarn`
|
||||||
- **Google Gemini API Key**: 用于 AI 代码分析
|
- **Google Gemini API Key**: 用于 AI 代码分析
|
||||||
- **Supabase 项目**: 用于数据存储(可选,支持离线模式)
|
|
||||||
|
|
||||||
### 安装与启动
|
#### 安装与启动
|
||||||
|
|
||||||
1. **克隆项目**
|
1. **克隆项目**
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -256,6 +285,7 @@ XCodeReviewer/
|
||||||
5. 查看详细的问题报告
|
5. 查看详细的问题报告
|
||||||
|
|
||||||
### 构建和部署
|
### 构建和部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 开发模式
|
# 开发模式
|
||||||
pnpm dev
|
pnpm dev
|
||||||
|
|
|
||||||
44
README_EN.md
44
README_EN.md
|
|
@ -49,14 +49,50 @@ In the fast-paced world of software development, ensuring code quality is crucia
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Requirements
|
### 🐳 Docker Deployment (Recommended)
|
||||||
|
|
||||||
|
Deploy quickly using Docker without Node.js environment setup.
|
||||||
|
|
||||||
|
1. **Clone the project**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/lintsinghua/XCodeReviewer.git
|
||||||
|
cd XCodeReviewer
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure environment variables**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env file and set at least VITE_GEMINI_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build and start**
|
||||||
|
```bash
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access the application**
|
||||||
|
|
||||||
|
Open `http://localhost:5174` in your browser
|
||||||
|
|
||||||
|
**Common commands:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f # View logs
|
||||||
|
docker-compose restart # Restart service
|
||||||
|
docker-compose down # Stop service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💻 Local Development Deployment
|
||||||
|
|
||||||
|
For development or custom modifications, use local deployment.
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
|
||||||
- **Node.js**: `18+`
|
- **Node.js**: `18+`
|
||||||
- **pnpm**: `8+` (recommended) or `npm` / `yarn`
|
- **pnpm**: `8+` (recommended) or `npm` / `yarn`
|
||||||
- **Google Gemini API Key**: For AI code analysis
|
- **Google Gemini API Key**: For AI code analysis
|
||||||
- **Supabase Project**: For data storage (optional, supports offline mode)
|
|
||||||
|
|
||||||
### Installation & Setup
|
#### Installation & Setup
|
||||||
|
|
||||||
1. **Clone the project**
|
1. **Clone the project**
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -111,7 +147,7 @@ In the fast-paced world of software development, ensuring code quality is crucia
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Access the application**
|
5. **Access the application**
|
||||||
Open `http://localhost:5173` in your browser
|
Open `http://localhost:5174` in your browser
|
||||||
|
|
||||||
### 🔑 Getting API Keys
|
### 🔑 Getting API Keys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
services:
|
||||||
|
# XCodeReviewer 前端应用
|
||||||
|
xcodereviewer:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: xcodereviewer-app
|
||||||
|
ports:
|
||||||
|
- "5174:80"
|
||||||
|
environment:
|
||||||
|
# Google Gemini AI 配置 (必需)
|
||||||
|
- VITE_GEMINI_API_KEY=${VITE_GEMINI_API_KEY}
|
||||||
|
- VITE_GEMINI_MODEL=${VITE_GEMINI_MODEL:-gemini-2.5-flash}
|
||||||
|
- VITE_GEMINI_TIMEOUT_MS=${VITE_GEMINI_TIMEOUT_MS:-25000}
|
||||||
|
|
||||||
|
# Supabase 配置 (可选)
|
||||||
|
- VITE_SUPABASE_URL=${VITE_SUPABASE_URL}
|
||||||
|
- VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY}
|
||||||
|
|
||||||
|
# GitHub 集成 (可选)
|
||||||
|
- VITE_GITHUB_TOKEN=${VITE_GITHUB_TOKEN}
|
||||||
|
|
||||||
|
# 应用配置
|
||||||
|
- VITE_APP_ID=${VITE_APP_ID:-xcodereviewer}
|
||||||
|
- VITE_MAX_ANALYZE_FILES=${VITE_MAX_ANALYZE_FILES:-40}
|
||||||
|
- VITE_LLM_CONCURRENCY=${VITE_LLM_CONCURRENCY:-2}
|
||||||
|
- VITE_LLM_GAP_MS=${VITE_LLM_GAP_MS:-500}
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- xcodereviewer-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
xcodereviewer-network:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip 压缩配置
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
||||||
|
|
||||||
|
# 安全头部
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# 处理 React Router 的客户端路由
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 静态资源缓存
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# API 代理(如果需要)
|
||||||
|
# location /api {
|
||||||
|
# proxy_pass http://backend:8000;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection 'upgrade';
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_cache_bypass $http_upgrade;
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
|
@ -19,19 +19,49 @@ const isValidUuid = (value?: string): boolean => {
|
||||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
// 检查是否配置了 Supabase
|
||||||
|
const hasSupabaseConfig = supabaseUrl && supabaseAnonKey;
|
||||||
|
|
||||||
|
// 如果没有配置 Supabase,使用虚拟配置避免错误
|
||||||
|
const finalSupabaseUrl = hasSupabaseConfig ? supabaseUrl : 'https://demo.supabase.co';
|
||||||
|
const finalSupabaseKey = hasSupabaseConfig ? supabaseAnonKey : 'demo-key';
|
||||||
|
|
||||||
|
export const supabase = hasSupabaseConfig ? createClient(finalSupabaseUrl, finalSupabaseKey, {
|
||||||
global: {
|
global: {
|
||||||
fetch: undefined
|
fetch: undefined
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
storageKey: (import.meta.env.VITE_APP_ID || "sb") + "-auth-token"
|
storageKey: (import.meta.env.VITE_APP_ID || "sb") + "-auth-token"
|
||||||
}
|
}
|
||||||
});
|
}) : null;
|
||||||
|
|
||||||
|
// 演示模式标识
|
||||||
|
export const isDemoMode = !hasSupabaseConfig;
|
||||||
|
|
||||||
|
// 演示数据
|
||||||
|
const demoProfile: Profile = {
|
||||||
|
id: 'demo-user',
|
||||||
|
phone: null,
|
||||||
|
email: 'demo@xcodereviewer.com',
|
||||||
|
full_name: 'Demo User',
|
||||||
|
avatar_url: null,
|
||||||
|
role: 'admin',
|
||||||
|
github_username: 'demo-user',
|
||||||
|
gitlab_username: null,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
// 用户相关API
|
// 用户相关API
|
||||||
export const api = {
|
export const api = {
|
||||||
// Profile相关
|
// Profile相关
|
||||||
async getProfilesById(id: string): Promise<Profile | null> {
|
async getProfilesById(id: string): Promise<Profile | null> {
|
||||||
|
if (isDemoMode) {
|
||||||
|
return demoProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supabase) return null;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
|
@ -43,6 +73,12 @@ export const api = {
|
||||||
},
|
},
|
||||||
|
|
||||||
async getProfilesCount(): Promise<number> {
|
async getProfilesCount(): Promise<number> {
|
||||||
|
if (isDemoMode) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supabase) return 0;
|
||||||
|
|
||||||
const { count, error } = await supabase
|
const { count, error } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('*', { count: 'exact', head: true });
|
.select('*', { count: 'exact', head: true });
|
||||||
|
|
@ -95,6 +131,25 @@ export const api = {
|
||||||
|
|
||||||
// Project相关
|
// Project相关
|
||||||
async getProjects(): Promise<Project[]> {
|
async getProjects(): Promise<Project[]> {
|
||||||
|
if (isDemoMode) {
|
||||||
|
return [{
|
||||||
|
id: 'demo-project-1',
|
||||||
|
name: 'Demo Project',
|
||||||
|
description: '这是一个演示项目,展示 XCodeReviewer 的功能',
|
||||||
|
repository_url: 'https://github.com/demo/project',
|
||||||
|
repository_type: 'github',
|
||||||
|
default_branch: 'main',
|
||||||
|
programming_languages: JSON.stringify(['TypeScript', 'JavaScript', 'React']),
|
||||||
|
owner_id: 'demo-user',
|
||||||
|
owner: demoProfile,
|
||||||
|
is_active: true,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supabase) return [];
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('projects')
|
.from('projects')
|
||||||
.select(`
|
.select(`
|
||||||
|
|
@ -382,6 +437,28 @@ export const api = {
|
||||||
|
|
||||||
// 统计相关
|
// 统计相关
|
||||||
async getProjectStats(): Promise<any> {
|
async getProjectStats(): Promise<any> {
|
||||||
|
if (isDemoMode) {
|
||||||
|
return {
|
||||||
|
total_projects: 1,
|
||||||
|
active_projects: 1,
|
||||||
|
total_tasks: 3,
|
||||||
|
completed_tasks: 2,
|
||||||
|
total_issues: 15,
|
||||||
|
resolved_issues: 12
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supabase) {
|
||||||
|
return {
|
||||||
|
total_projects: 0,
|
||||||
|
active_projects: 0,
|
||||||
|
total_tasks: 0,
|
||||||
|
completed_tasks: 0,
|
||||||
|
total_issues: 0,
|
||||||
|
resolved_issues: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const [projectsResult, tasksResult, issuesResult] = await Promise.all([
|
const [projectsResult, tasksResult, issuesResult] = await Promise.all([
|
||||||
supabase.from('projects').select('id, is_active', { count: 'exact' }),
|
supabase.from('projects').select('id, is_active', { count: 'exact' }),
|
||||||
supabase.from('audit_tasks').select('id, status', { count: 'exact' }),
|
supabase.from('audit_tasks').select('id, status', { count: 'exact' }),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue