chore(i18n): replace all Chinese content with English in gitea-webhook-ambassador-python
This commit is contained in:
parent
c259460f9c
commit
9dbee47706
@ -1,110 +1,110 @@
|
||||
# Gitea Webhook Ambassador (Python)
|
||||
|
||||
一个高性能的 Python webhook 服务,用于连接 Gitea 和 Jenkins,支持智能分发、高并发处理和防抖策略。
|
||||
A high-performance Python webhook service for connecting Gitea and Jenkins, supporting intelligent dispatch, high concurrency processing, and deduplication strategies.
|
||||
|
||||
## 🚀 新特性
|
||||
## 🚀 Features
|
||||
|
||||
### 1. 智能分发策略
|
||||
- **dev 分支** → 触发 alpha 环境构建
|
||||
- **prod 分支** → 触发生产环境构建
|
||||
- **其他分支** → 可配置的默认策略
|
||||
### 1. Intelligent Dispatch Strategy
|
||||
- **dev branches** → Trigger alpha environment build
|
||||
- **prod branches** → Trigger production environment build
|
||||
- **other branches** → Configurable default strategy
|
||||
|
||||
### 2. 高并发处理
|
||||
- **异步任务队列**: 使用 Celery + Redis 处理高并发
|
||||
- **任务排队机制**: 防止构建丢失,确保任务按序执行
|
||||
- **负载均衡**: 支持多 worker 实例
|
||||
### 2. High Concurrency Processing
|
||||
- **Asynchronous Task Queue**: Use Celery + Redis for high concurrency
|
||||
- **Task Queueing Mechanism**: Prevent build loss, ensure tasks are executed in order
|
||||
- **Load Balancing**: Support multiple worker instances
|
||||
|
||||
### 3. 防抖策略
|
||||
- **基于 commit hash + 分支的去重**: 防止重复触发
|
||||
- **时间窗口防抖**: 在指定时间窗口内的相同提交只触发一次
|
||||
- **智能去重**: 支持配置去重策略
|
||||
### 3. Deduplication Strategy
|
||||
- **Deduplication based on commit hash + branch**: Prevent repeated triggers
|
||||
- **Time Window Deduplication**: Only trigger once for the same commit within a specified time window
|
||||
- **Intelligent Deduplication**: Support configurable deduplication strategies
|
||||
|
||||
## 🏗️ 架构设计
|
||||
## 🏗️ Architecture Design
|
||||
|
||||
```
|
||||
Gitea Webhook → FastAPI → Celery Queue → Jenkins Workers
|
||||
↓ ↓ ↓ ↓
|
||||
验证签名 路由分发 任务排队 并发执行
|
||||
Signature Verification Routing Dispatch Task Queueing Concurrent Execution
|
||||
↓ ↓ ↓ ↓
|
||||
防抖检查 环境判断 持久化存储 状态反馈
|
||||
Deduplication Environment Judgment Persistent Storage Status Feedback
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
gitea-webhook-ambassador-python/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI 应用入口
|
||||
│ ├── config.py # 配置管理
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── main.py # FastAPI application entry
|
||||
│ ├── config.py # Configuration management
|
||||
│ ├── models/ # Data models
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── gitea.py # Gitea webhook 模型
|
||||
│ │ └── jenkins.py # Jenkins 任务模型
|
||||
│ ├── services/ # 业务逻辑
|
||||
│ │ ├── gitea.py # Gitea webhook model
|
||||
│ │ └── jenkins.py # Jenkins job model
|
||||
│ ├── services/ # Business logic
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── webhook_service.py # Webhook 处理服务
|
||||
│ │ ├── jenkins_service.py # Jenkins 集成服务
|
||||
│ │ ├── queue_service.py # 队列管理服务
|
||||
│ │ └── dedup_service.py # 防抖服务
|
||||
│ ├── api/ # API 路由
|
||||
│ │ ├── webhook_service.py # Webhook processing service
|
||||
│ │ ├── jenkins_service.py # Jenkins integration service
|
||||
│ │ ├── queue_service.py # Queue management service
|
||||
│ │ └── dedup_service.py # Deduplication service
|
||||
│ ├── api/ # API routes
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── webhook.py # Webhook 端点
|
||||
│ │ ├── health.py # 健康检查
|
||||
│ │ └── admin.py # 管理接口
|
||||
│ ├── core/ # 核心组件
|
||||
│ │ ├── webhook.py # Webhook endpoint
|
||||
│ │ ├── health.py # Health check
|
||||
│ │ └── admin.py # Admin interface
|
||||
│ ├── core/ # Core components
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── security.py # 安全验证
|
||||
│ │ ├── database.py # 数据库连接
|
||||
│ │ └── cache.py # 缓存管理
|
||||
│ └── tasks/ # Celery 任务
|
||||
│ │ ├── security.py # Security validation
|
||||
│ │ ├── database.py # Database connection
|
||||
│ │ └── cache.py # Cache management
|
||||
│ └── tasks/ # Celery tasks
|
||||
│ ├── __init__.py
|
||||
│ └── jenkins_tasks.py # Jenkins 任务处理
|
||||
├── tests/ # 测试文件
|
||||
├── docker/ # Docker 配置
|
||||
├── requirements.txt # Python 依赖
|
||||
├── docker-compose.yml # 开发环境
|
||||
│ └── jenkins_tasks.py # Jenkins task processing
|
||||
├── tests/ # Test files
|
||||
├── docker/ # Docker configuration
|
||||
├── requirements.txt # Python dependencies
|
||||
├── docker-compose.yml # Development environment
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🛠️ 技术栈
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Web 框架**: FastAPI
|
||||
- **任务队列**: Celery + Redis
|
||||
- **数据库**: PostgreSQL (生产) / SQLite (开发)
|
||||
- **缓存**: Redis
|
||||
- **监控**: Prometheus + Grafana
|
||||
- **日志**: Structured logging with JSON
|
||||
- **测试**: pytest + pytest-asyncio
|
||||
- **Web Framework**: FastAPI
|
||||
- **Task Queue**: Celery + Redis
|
||||
- **Database**: PostgreSQL (production) / SQLite (development)
|
||||
- **Cache**: Redis
|
||||
- **Monitoring**: Prometheus + Grafana
|
||||
- **Logging**: Structured logging with JSON
|
||||
- **Testing**: pytest + pytest-asyncio
|
||||
|
||||
## 🚀 快速开始
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. 安装依赖
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 配置环境
|
||||
### 2. Configure Environment
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件配置 Jenkins 和数据库连接
|
||||
# Edit the .env file to configure Jenkins and database connections
|
||||
```
|
||||
|
||||
### 3. 启动服务
|
||||
### 3. Start Service
|
||||
```bash
|
||||
# 启动 Redis
|
||||
# Start Redis
|
||||
docker run -d -p 6379:6379 redis:alpine
|
||||
|
||||
# 启动 Celery worker
|
||||
# Start Celery worker
|
||||
celery -A app.tasks worker --loglevel=info
|
||||
|
||||
# 启动 FastAPI 应用
|
||||
# Start FastAPI application
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
## 📋 配置说明
|
||||
## 📋 Configuration
|
||||
|
||||
### 环境分发配置
|
||||
### Environment Dispatch Configuration
|
||||
```yaml
|
||||
environments:
|
||||
dev:
|
||||
@ -120,70 +120,70 @@ environments:
|
||||
jenkins_url: "https://jenkins-default.example.com"
|
||||
```
|
||||
|
||||
### 防抖配置
|
||||
### Deduplication Configuration
|
||||
```yaml
|
||||
deduplication:
|
||||
enabled: true
|
||||
window_seconds: 300 # 5分钟防抖窗口
|
||||
window_seconds: 300 # 5-minute deduplication window
|
||||
strategy: "commit_branch" # commit_hash + branch
|
||||
cache_ttl: 3600 # 缓存1小时
|
||||
cache_ttl: 3600 # Cache for 1 hour
|
||||
```
|
||||
|
||||
### 队列配置
|
||||
### Queue Configuration
|
||||
```yaml
|
||||
queue:
|
||||
max_concurrent: 10
|
||||
max_retries: 3
|
||||
retry_delay: 60 # 秒
|
||||
retry_delay: 60 # seconds
|
||||
priority_levels: 3
|
||||
```
|
||||
|
||||
## 🔧 API 接口
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### Webhook 端点
|
||||
### Webhook Endpoint
|
||||
```
|
||||
POST /webhook/gitea
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
### Health Check
|
||||
```
|
||||
GET /health
|
||||
GET /health/queue
|
||||
GET /health/jenkins
|
||||
```
|
||||
|
||||
### 管理接口
|
||||
### Admin Endpoints
|
||||
```
|
||||
GET /admin/queue/status
|
||||
GET /admin/queue/stats
|
||||
POST /admin/queue/clear
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# 运行特定测试
|
||||
# Run specific test
|
||||
pytest tests/test_webhook_service.py
|
||||
|
||||
# 运行性能测试
|
||||
# Run performance test
|
||||
pytest tests/test_performance.py
|
||||
```
|
||||
|
||||
## 📊 监控指标
|
||||
## 📊 Monitoring Metrics
|
||||
|
||||
- Webhook 接收率
|
||||
- 任务队列长度
|
||||
- Jenkins 构建成功率
|
||||
- 响应时间分布
|
||||
- 防抖命中率
|
||||
- Webhook receive rate
|
||||
- Task queue length
|
||||
- Jenkins build success rate
|
||||
- Response time distribution
|
||||
- Deduplication hit rate
|
||||
|
||||
## 🔒 安全特性
|
||||
## 🔒 Security Features
|
||||
|
||||
- Webhook 签名验证
|
||||
- API 密钥认证
|
||||
- 请求频率限制
|
||||
- 输入验证和清理
|
||||
- 安全日志记录
|
||||
- Webhook signature verification
|
||||
- API key authentication
|
||||
- Request rate limiting
|
||||
- Input validation and sanitization
|
||||
- Secure logging
|
||||
@ -1,101 +1,101 @@
|
||||
# Gitea Webhook Ambassador (Python Enhanced Version)
|
||||
|
||||
这是一个用 Python 重写的 Gitea Webhook Ambassador 服务,提供与 Go 版本相同的功能,但增加了 Web 界面和更多管理功能。
|
||||
This is a Gitea Webhook Ambassador service rewritten in Python, providing the same features as the Go version, but with an added web interface and more management features.
|
||||
|
||||
## 🚀 快速开始
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 方式一:使用 devbox 脚本(推荐,与 Go 版本一致)
|
||||
### Method 1: Use the devbox script (recommended, same as Go version)
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
./devbox install
|
||||
|
||||
# 初始化数据库
|
||||
# Initialize database
|
||||
./devbox init
|
||||
|
||||
# 启动服务
|
||||
# Start the service
|
||||
./devbox start
|
||||
|
||||
# 查看状态
|
||||
# Check status
|
||||
./devbox status
|
||||
|
||||
# 查看日志
|
||||
# View logs
|
||||
./devbox logs
|
||||
|
||||
# 停止服务
|
||||
# Stop the service
|
||||
./devbox stop
|
||||
```
|
||||
|
||||
### 方式二:使用 Makefile
|
||||
### Method 2: Use Makefile
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
make install
|
||||
|
||||
# 初始化数据库
|
||||
# Initialize database
|
||||
make init
|
||||
|
||||
# 启动服务(前台运行)
|
||||
# Start the service (foreground)
|
||||
make run
|
||||
|
||||
# 启动服务(后台运行)
|
||||
# Start the service (background)
|
||||
make start
|
||||
|
||||
# 查看状态
|
||||
# Check status
|
||||
make status
|
||||
|
||||
# 查看日志
|
||||
# View logs
|
||||
make logs
|
||||
|
||||
# 停止服务
|
||||
# Stop the service
|
||||
make stop
|
||||
```
|
||||
|
||||
### 方式三:直接使用 Python
|
||||
### Method 3: Use Python directly
|
||||
|
||||
```bash
|
||||
# 创建虚拟环境
|
||||
# Create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 初始化数据库
|
||||
# Initialize database
|
||||
python -c "from app.models.database import create_tables; create_tables()"
|
||||
|
||||
# 启动服务
|
||||
# Start the service
|
||||
python -m uvicorn app.main_enhanced:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
## 📁 目录结构(与 Go 版本一致)
|
||||
## 📁 Directory Structure (same as Go version)
|
||||
|
||||
```
|
||||
gitea-webhook-ambassador-python/
|
||||
├── app/ # 应用代码
|
||||
│ ├── auth/ # 认证模块
|
||||
│ ├── handlers/ # API 处理器
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── templates/ # HTML 模板
|
||||
│ ├── static/ # 静态文件
|
||||
│ └── main_enhanced.py # 主应用入口
|
||||
├── cmd/ # 命令行工具(与 Go 版本一致)
|
||||
│ └── server/ # 服务器启动
|
||||
├── configs/ # 配置文件(与 Go 版本一致)
|
||||
│ └── config.yaml # 主配置文件
|
||||
├── data/ # 数据目录(与 Go 版本一致)
|
||||
│ └── *.db # SQLite 数据库文件
|
||||
├── logs/ # 日志目录(与 Go 版本一致)
|
||||
│ └── service.log # 服务日志
|
||||
├── devbox # 启动脚本(与 Go 版本一致)
|
||||
├── Makefile # 构建脚本(与 Go 版本一致)
|
||||
├── requirements.txt # Python 依赖
|
||||
└── README_ENHANCED.md # 本文档
|
||||
├── app/ # Application code
|
||||
│ ├── auth/ # Authentication module
|
||||
│ ├── handlers/ # API handlers
|
||||
│ ├── models/ # Data models
|
||||
│ ├── templates/ # HTML templates
|
||||
│ ├── static/ # Static files
|
||||
│ └── main_enhanced.py # Main application entry
|
||||
├── cmd/ # CLI tools (same as Go version)
|
||||
│ └── server/ # Server startup
|
||||
├── configs/ # Configuration files (same as Go version)
|
||||
│ └── config.yaml # Main configuration file
|
||||
├── data/ # Data directory (same as Go version)
|
||||
│ └── *.db # SQLite database files
|
||||
├── logs/ # Log directory (same as Go version)
|
||||
│ └── service.log # Service log
|
||||
├── devbox # Startup script (same as Go version)
|
||||
├── Makefile # Build script (same as Go version)
|
||||
├── requirements.txt # Python dependencies
|
||||
└── README_ENHANCED.md # This document
|
||||
```
|
||||
|
||||
## 🔧 配置
|
||||
## 🔧 Configuration
|
||||
|
||||
编辑 `configs/config.yaml` 文件:
|
||||
Edit the `configs/config.yaml` file:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
@ -132,106 +132,106 @@ eventCleanup:
|
||||
expireAfter: 7200
|
||||
```
|
||||
|
||||
## 🌐 Web 界面
|
||||
## 🌐 Web Interface
|
||||
|
||||
启动服务后,访问以下地址:
|
||||
After starting the service, visit the following addresses:
|
||||
|
||||
- **登录页面**: http://localhost:8000
|
||||
- **仪表板**: http://localhost:8000/dashboard
|
||||
- **API 文档**: http://localhost:8000/docs
|
||||
- **Login page**: http://localhost:8000
|
||||
- **Dashboard**: http://localhost:8000/dashboard
|
||||
- **API Docs**: http://localhost:8000/docs
|
||||
|
||||
### 默认登录凭据
|
||||
- **用户名**: admin
|
||||
- **密码**: admin-secret-key-change-in-production
|
||||
### Default login credentials
|
||||
- **Username**: admin
|
||||
- **Password**: admin-secret-key-change-in-production
|
||||
|
||||
## 📊 功能特性
|
||||
## 📊 Features
|
||||
|
||||
### ✅ 与 Go 版本相同的功能
|
||||
- Gitea Webhook 接收和处理
|
||||
- Jenkins 任务触发
|
||||
- 项目映射配置
|
||||
- 分支模式匹配
|
||||
- 重试机制
|
||||
- 日志记录
|
||||
### ✅ Same features as Go version
|
||||
- Gitea Webhook receiving and processing
|
||||
- Jenkins job triggering
|
||||
- Project mapping configuration
|
||||
- Branch pattern matching
|
||||
- Retry mechanism
|
||||
- Logging
|
||||
|
||||
### 🆕 Python 版本增强功能
|
||||
- **Web 登录界面**: 基于 Bootstrap 5 的现代化界面
|
||||
- **数据库存储**: SQLite 数据库存储 API 密钥和配置
|
||||
- **JWT 认证**: 7 天有效期的 JWT 令牌
|
||||
- **前端仪表板**: 多标签页管理界面
|
||||
- **自动重定向**: 未认证用户自动跳转到登录页
|
||||
- **健康检查**: 服务状态监控
|
||||
- **统计信息**: 请求统计和性能指标
|
||||
### 🆕 Python version enhancements
|
||||
- **Web login interface**: Modern UI based on Bootstrap 5
|
||||
- **Database storage**: SQLite database for API keys and configuration
|
||||
- **JWT authentication**: 7-day valid JWT tokens
|
||||
- **Frontend dashboard**: Multi-tab management interface
|
||||
- **Auto redirect**: Unauthenticated users are redirected to login
|
||||
- **Health check**: Service status monitoring
|
||||
- **Statistics**: Request statistics and performance metrics
|
||||
|
||||
## 🔌 API 端点
|
||||
## 🔌 API Endpoints
|
||||
|
||||
### 认证相关
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `GET /api/auth/verify` - 验证 JWT 令牌
|
||||
### Authentication
|
||||
- `POST /api/auth/login` - User login
|
||||
- `GET /api/auth/verify` - Verify JWT token
|
||||
|
||||
### 项目管理
|
||||
- `GET /api/projects` - 获取项目列表
|
||||
- `POST /api/projects` - 创建新项目
|
||||
- `PUT /api/projects/{id}` - 更新项目
|
||||
- `DELETE /api/projects/{id}` - 删除项目
|
||||
### Project Management
|
||||
- `GET /api/projects` - Get project list
|
||||
- `POST /api/projects` - Create new project
|
||||
- `PUT /api/projects/{id}` - Update project
|
||||
- `DELETE /api/projects/{id}` - Delete project
|
||||
|
||||
### API 密钥管理
|
||||
- `GET /api/keys` - 获取 API 密钥列表
|
||||
- `POST /api/keys` - 创建新 API 密钥
|
||||
- `DELETE /api/keys/{id}` - 删除 API 密钥
|
||||
### API Key Management
|
||||
- `GET /api/keys` - Get API key list
|
||||
- `POST /api/keys` - Create new API key
|
||||
- `DELETE /api/keys/{id}` - Delete API key
|
||||
|
||||
### 系统监控
|
||||
- `GET /api/health` - 健康检查
|
||||
- `GET /api/stats` - 统计信息
|
||||
- `GET /api/logs` - 日志查看
|
||||
### System Monitoring
|
||||
- `GET /api/health` - Health check
|
||||
- `GET /api/stats` - Statistics
|
||||
- `GET /api/logs` - View logs
|
||||
|
||||
### Webhook 处理
|
||||
- `POST /webhook` - Gitea Webhook 接收端点
|
||||
### Webhook Handling
|
||||
- `POST /webhook` - Gitea Webhook endpoint
|
||||
|
||||
## 🛠️ 开发
|
||||
## 🛠️ Development
|
||||
|
||||
### 运行测试
|
||||
### Run tests
|
||||
```bash
|
||||
# 使用 devbox
|
||||
# Use devbox
|
||||
./devbox test
|
||||
|
||||
# 使用 Makefile
|
||||
# Use Makefile
|
||||
make test
|
||||
|
||||
# 直接运行
|
||||
# Run directly
|
||||
python test_enhanced_features.py
|
||||
```
|
||||
|
||||
### 代码检查
|
||||
### Code linting
|
||||
```bash
|
||||
# 使用 Makefile
|
||||
# Use Makefile
|
||||
make lint
|
||||
|
||||
# 直接运行
|
||||
# Run directly
|
||||
flake8 app/ --max-line-length=120 --ignore=E501,W503
|
||||
```
|
||||
|
||||
### 清理
|
||||
### Clean up
|
||||
```bash
|
||||
# 使用 devbox
|
||||
# Use devbox
|
||||
./devbox clean
|
||||
|
||||
# 使用 Makefile
|
||||
# Use Makefile
|
||||
make clean
|
||||
```
|
||||
|
||||
## 🐳 Docker 部署
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
### 构建镜像
|
||||
### Build image
|
||||
```bash
|
||||
# 使用 Makefile
|
||||
# Use Makefile
|
||||
make docker-build
|
||||
|
||||
# 直接构建
|
||||
# Build directly
|
||||
docker build -t gitea-webhook-ambassador:latest .
|
||||
```
|
||||
|
||||
### 运行容器
|
||||
### Run container
|
||||
```bash
|
||||
docker run -d \
|
||||
--name gitea-webhook-ambassador \
|
||||
@ -242,33 +242,33 @@ docker run -d \
|
||||
gitea-webhook-ambassador:latest
|
||||
```
|
||||
|
||||
## 📈 与 Go 版本对比
|
||||
## 📈 Comparison with Go Version
|
||||
|
||||
| 特性 | Go 版本 | Python 版本 |
|
||||
|------|---------|-------------|
|
||||
| **启动方式** | `./devbox start` | `./devbox start` |
|
||||
| **目录结构** | 标准 Go 项目结构 | 与 Go 版本一致 |
|
||||
| **配置文件** | `configs/config.yaml` | `configs/config.yaml` |
|
||||
| **日志目录** | `logs/` | `logs/` |
|
||||
| **数据目录** | `data/` | `data/` |
|
||||
| **Web 界面** | ❌ 无 | ✅ 完整仪表板 |
|
||||
| **数据库** | ❌ 无 | ✅ SQLite |
|
||||
| **JWT 认证** | ❌ 无 | ✅ 7天有效期 |
|
||||
| **API 密钥管理** | ❌ 无 | ✅ 数据库存储 |
|
||||
| **健康检查** | ✅ 基础 | ✅ 增强版 |
|
||||
| **性能** | 🚀 极高 | 🚀 高 |
|
||||
| Feature | Go Version | Python Version |
|
||||
|---------|------------|---------------|
|
||||
| **Startup** | `./devbox start` | `./devbox start` |
|
||||
| **Directory Structure** | Standard Go project | Same as Go version |
|
||||
| **Config File** | `configs/config.yaml` | `configs/config.yaml` |
|
||||
| **Log Directory** | `logs/` | `logs/` |
|
||||
| **Data Directory** | `data/` | `data/` |
|
||||
| **Web Interface** | ❌ No | ✅ Full dashboard |
|
||||
| **Database** | ❌ No | ✅ SQLite |
|
||||
| **JWT Auth** | ❌ No | ✅ 7-day validity |
|
||||
| **API Key Management** | ❌ No | ✅ Database storage |
|
||||
| **Health Check** | ✅ Basic | ✅ Enhanced |
|
||||
| **Performance** | 🚀 Very high | 🚀 High |
|
||||
|
||||
## 🔄 迁移指南
|
||||
## 🔄 Migration Guide
|
||||
|
||||
### 从 Go 版本迁移到 Python 版本
|
||||
### Migrate from Go version to Python version
|
||||
|
||||
1. **停止 Go 服务**
|
||||
1. **Stop Go service**
|
||||
```bash
|
||||
cd /path/to/go-version
|
||||
./devbox stop
|
||||
```
|
||||
|
||||
2. **启动 Python 服务**
|
||||
2. **Start Python service**
|
||||
```bash
|
||||
cd /path/to/python-version
|
||||
./devbox install
|
||||
@ -276,64 +276,64 @@ docker run -d \
|
||||
./devbox start
|
||||
```
|
||||
|
||||
3. **验证服务**
|
||||
3. **Verify service**
|
||||
```bash
|
||||
./devbox status
|
||||
curl http://localhost:8000/api/health
|
||||
```
|
||||
|
||||
4. **配置 Webhook**
|
||||
- 更新 Gitea Webhook URL 为新的 Python 服务地址
|
||||
- 确保 Jenkins 配置正确
|
||||
4. **Configure Webhook**
|
||||
- Update Gitea Webhook URL to the new Python service address
|
||||
- Ensure Jenkins configuration is correct
|
||||
|
||||
## 🆘 故障排除
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### 常见问题
|
||||
### Common Issues
|
||||
|
||||
**1. 端口被占用**
|
||||
**1. Port 8000 is occupied**
|
||||
```bash
|
||||
# 检查端口占用
|
||||
# Check port usage
|
||||
lsof -i :8000
|
||||
|
||||
# 停止占用进程
|
||||
# Stop the occupying process
|
||||
sudo kill -9 <PID>
|
||||
```
|
||||
|
||||
**2. 虚拟环境问题**
|
||||
**2. Virtual environment issues**
|
||||
```bash
|
||||
# 重新创建虚拟环境
|
||||
# Recreate virtual environment
|
||||
rm -rf venv
|
||||
./devbox install
|
||||
```
|
||||
|
||||
**3. 数据库问题**
|
||||
**3. Database issues**
|
||||
```bash
|
||||
# 重新初始化数据库
|
||||
# Reinitialize database
|
||||
./devbox init
|
||||
```
|
||||
|
||||
**4. 权限问题**
|
||||
**4. Permission issues**
|
||||
```bash
|
||||
# 设置脚本权限
|
||||
# Set script permissions
|
||||
chmod +x devbox
|
||||
```
|
||||
|
||||
### 日志查看
|
||||
### View logs
|
||||
```bash
|
||||
# 查看实时日志
|
||||
# View real-time logs
|
||||
./devbox follow
|
||||
|
||||
# 查看最新日志
|
||||
# View latest logs
|
||||
./devbox logs
|
||||
|
||||
# 查看完整日志
|
||||
# View full logs
|
||||
tail -f logs/service.log
|
||||
```
|
||||
|
||||
## 📞 支持
|
||||
## 📞 Support
|
||||
|
||||
如有问题,请检查:
|
||||
1. 服务状态:`./devbox status`
|
||||
2. 日志信息:`./devbox logs`
|
||||
3. 配置文件:`configs/config.yaml`
|
||||
4. 网络连接:`curl http://localhost:8000/api/health`
|
||||
If you have any issues, please check:
|
||||
1. Service status: `./devbox status`
|
||||
2. Log information: `./devbox logs`
|
||||
3. Configuration file: `configs/config.yaml`
|
||||
4. Network connection: `curl http://localhost:8000/api/health`
|
||||
@ -1,82 +1,82 @@
|
||||
# 🚀 Gitea Webhook Ambassador 使用指南
|
||||
# 🚀 Gitea Webhook Ambassador Usage Guide
|
||||
|
||||
## 📋 目录
|
||||
1. [快速开始](#快速开始)
|
||||
2. [配置说明](#配置说明)
|
||||
3. [API 接口](#api-接口)
|
||||
4. [数据库管理](#数据库管理)
|
||||
5. [监控和日志](#监控和日志)
|
||||
6. [故障排除](#故障排除)
|
||||
## 📋 Table of Contents
|
||||
1. [Quick Start](#quick-start)
|
||||
2. [Configuration](#configuration)
|
||||
3. [API Endpoints](#api-endpoints)
|
||||
4. [Database Management](#database-management)
|
||||
5. [Monitoring and Logs](#monitoring-and-logs)
|
||||
6. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## 🚀 快速开始
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. 环境准备
|
||||
### 1. Environment Preparation
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
cd freeleaps-ops/apps/gitea-webhook-ambassador-python
|
||||
# Clone the project
|
||||
git clone https://your.repo.url/gitea-webhook-ambassador-python.git
|
||||
cd gitea-webhook-ambassador-python
|
||||
|
||||
# 运行快速设置脚本
|
||||
chmod +x scripts/setup.sh
|
||||
./scripts/setup.sh
|
||||
# Run quick setup script
|
||||
./devbox install
|
||||
```
|
||||
|
||||
### 2. 配置环境
|
||||
### 2. Configure Environment
|
||||
|
||||
编辑 `.env` 文件,配置必要的参数:
|
||||
Edit the `.env` file to set required parameters:
|
||||
|
||||
```bash
|
||||
# 编辑配置文件
|
||||
nano .env
|
||||
# Edit configuration file
|
||||
cp .env.example .env
|
||||
vim .env
|
||||
```
|
||||
|
||||
**必需配置**:
|
||||
```env
|
||||
# Jenkins 配置
|
||||
**Required configuration:**
|
||||
|
||||
```bash
|
||||
# Jenkins configuration
|
||||
JENKINS_USERNAME=your_jenkins_username
|
||||
JENKINS_TOKEN=your_jenkins_api_token
|
||||
JENKINS_TOKEN=your_jenkins_token
|
||||
|
||||
# 安全配置
|
||||
SECURITY_SECRET_KEY=your-secret-key-here-make-it-long-and-random
|
||||
# Security configuration
|
||||
SECURITY_SECRET_KEY=your_secret_key
|
||||
```
|
||||
|
||||
### 3. 启动服务
|
||||
### 3. Start Service
|
||||
|
||||
```bash
|
||||
# 方法1: 使用启动脚本
|
||||
chmod +x scripts/start.sh
|
||||
./scripts/start.sh
|
||||
# Method 1: Use the startup script
|
||||
./devbox start
|
||||
|
||||
# 方法2: 手动启动
|
||||
# 启动 Redis
|
||||
docker run -d --name webhook-redis -p 6379:6379 redis:alpine
|
||||
# Method 2: Manual startup
|
||||
# Start Redis
|
||||
docker run -d -p 6379:6379 redis:alpine
|
||||
|
||||
# 激活虚拟环境
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# 启动 API 服务
|
||||
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
# Start API service
|
||||
python -m uvicorn app.main_enhanced:app --host 0.0.0.0 --port 8000
|
||||
|
||||
# 新终端启动 Celery worker
|
||||
celery -A app.tasks.jenkins_tasks worker --loglevel=info --concurrency=4
|
||||
# Start Celery worker in a new terminal
|
||||
celery -A app.tasks.jenkins_tasks worker --loglevel=info
|
||||
|
||||
# 新终端启动定时任务
|
||||
# Start scheduled tasks in a new terminal
|
||||
celery -A app.tasks.jenkins_tasks beat --loglevel=info
|
||||
```
|
||||
|
||||
### 4. 验证安装
|
||||
### 4. Verify Installation
|
||||
|
||||
访问以下地址验证服务是否正常:
|
||||
Visit the following addresses to verify the service:
|
||||
- **API Docs**: http://localhost:8000/docs
|
||||
- **Health Check**: http://localhost:8000/health
|
||||
- **Metrics**: http://localhost:8000/metrics
|
||||
|
||||
- **API 文档**: http://localhost:8000/docs
|
||||
- **健康检查**: http://localhost:8000/health
|
||||
- **监控指标**: http://localhost:8000/metrics
|
||||
## ⚙️ Configuration
|
||||
|
||||
## ⚙️ 配置说明
|
||||
### Environment Dispatch Configuration
|
||||
|
||||
### 环境分发配置
|
||||
|
||||
编辑 `config/environments.yaml` 文件:
|
||||
Edit the `config/environments.yaml` file:
|
||||
|
||||
```yaml
|
||||
environments:
|
||||
@ -85,295 +85,174 @@ environments:
|
||||
jenkins_job: "alpha-build"
|
||||
jenkins_url: "https://jenkins-alpha.freeleaps.com"
|
||||
priority: 2
|
||||
|
||||
prod:
|
||||
branches: ["prod", "production", "main", "master", "release/*"]
|
||||
jenkins_job: "production-build"
|
||||
jenkins_url: "https://jenkins-prod.freeleaps.com"
|
||||
priority: 1
|
||||
staging:
|
||||
branches: ["staging", "stage", "pre-prod"]
|
||||
jenkins_job: "staging-build"
|
||||
jenkins_url: "https://jenkins-staging.freeleaps.com"
|
||||
priority: 3
|
||||
default:
|
||||
branches: ["*"]
|
||||
jenkins_job: "default-build"
|
||||
jenkins_url: "https://jenkins-default.freeleaps.com"
|
||||
priority: 4
|
||||
```
|
||||
|
||||
### 防抖配置
|
||||
### Deduplication Configuration
|
||||
|
||||
```yaml
|
||||
deduplication:
|
||||
enabled: true
|
||||
window_seconds: 300 # 5分钟防抖窗口
|
||||
strategy: "commit_branch" # commit_hash + branch
|
||||
cache_ttl: 3600 # 缓存1小时
|
||||
window_seconds: 300 # 5-minute deduplication window
|
||||
strategy: "commit_branch"
|
||||
cache_ttl: 3600 # Cache for 1 hour
|
||||
```
|
||||
|
||||
## 🔧 API 接口
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### Webhook 端点
|
||||
### Webhook Endpoint
|
||||
|
||||
**POST** `/webhook/gitea`
|
||||
Receive Gitea webhook events:
|
||||
|
||||
接收 Gitea webhook 事件:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/webhook/gitea" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Gitea-Signature: your-secret-key" \
|
||||
-d '{
|
||||
"ref": "refs/heads/dev",
|
||||
"before": "abc123",
|
||||
"after": "def456",
|
||||
"repository": {
|
||||
"full_name": "freeleaps/my-project",
|
||||
"clone_url": "https://gitea.freeleaps.com/freeleaps/my-project.git"
|
||||
},
|
||||
"pusher": {
|
||||
"login": "developer",
|
||||
"email": "dev@freeleaps.com"
|
||||
}
|
||||
}'
|
||||
```http
|
||||
POST /webhook/gitea
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
### Health Check
|
||||
|
||||
**GET** `/health`
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/health
|
||||
```http
|
||||
GET /health
|
||||
GET /health/queue
|
||||
GET /health/jenkins
|
||||
```
|
||||
|
||||
响应示例:
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": 1640995200.0,
|
||||
"services": {
|
||||
"redis": "healthy",
|
||||
"celery": "healthy"
|
||||
}
|
||||
"service": "Gitea Webhook Ambassador",
|
||||
"version": "1.0.0",
|
||||
"timestamp": "2023-01-01T00:00:00Z",
|
||||
"jenkins": {"status": "connected"},
|
||||
"worker_pool": {"active_workers": 2, "queue_size": 0, "total_processed": 10, "total_failed": 1},
|
||||
"database": {"status": "connected"}
|
||||
}
|
||||
```
|
||||
|
||||
### 队列状态
|
||||
### Queue Status
|
||||
|
||||
**GET** `/health/queue`
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/health/queue
|
||||
```http
|
||||
GET /admin/queue/status
|
||||
```
|
||||
|
||||
响应示例:
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"queue_stats": {
|
||||
"active_tasks": 2,
|
||||
"queued_tasks": 5,
|
||||
"worker_count": 4,
|
||||
"total_queue_length": 7
|
||||
}
|
||||
"active_tasks": 1,
|
||||
"queued_tasks": 2,
|
||||
"worker_count": 2,
|
||||
"queue_length": 3
|
||||
}
|
||||
```
|
||||
|
||||
### 监控指标
|
||||
### Monitoring Metrics
|
||||
|
||||
**GET** `/metrics`
|
||||
Returns Prometheus-formatted monitoring metrics.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/metrics
|
||||
```
|
||||
## 🗄️ Database Management
|
||||
|
||||
返回 Prometheus 格式的监控指标。
|
||||
### Create Project Mapping
|
||||
|
||||
## 🗄️ 数据库管理
|
||||
|
||||
### 创建项目映射
|
||||
|
||||
使用 Python 脚本创建项目映射:
|
||||
Use a Python script to create a project mapping:
|
||||
|
||||
```python
|
||||
# create_mapping.py
|
||||
import asyncio
|
||||
from app.services.database_service import get_database_service
|
||||
import asyncio
|
||||
|
||||
async def create_mapping():
|
||||
db_service = get_database_service()
|
||||
|
||||
mapping_data = {
|
||||
"repository_name": "freeleaps/my-project",
|
||||
"default_job": "default-build",
|
||||
"branch_jobs": [
|
||||
{"branch_name": "dev", "job_name": "alpha-build"},
|
||||
{"branch_name": "main", "job_name": "production-build"}
|
||||
],
|
||||
"branch_patterns": [
|
||||
{"pattern": r"feature/.*", "job_name": "feature-build"},
|
||||
{"pattern": r"hotfix/.*", "job_name": "hotfix-build"}
|
||||
]
|
||||
}
|
||||
|
||||
success = await db_service.create_project_mapping(mapping_data)
|
||||
print(f"创建映射: {'成功' if success else '失败'}")
|
||||
db_service = get_database_service()
|
||||
mapping_data = {
|
||||
"repository_name": "freeleaps/test-project",
|
||||
"default_job": "test-project-build",
|
||||
"branch_jobs": [
|
||||
{"branch_name": "dev", "job_name": "test-project-dev"},
|
||||
{"branch_name": "staging", "job_name": "test-project-staging"}
|
||||
],
|
||||
"branch_patterns": [
|
||||
{"pattern": "feature/*", "job_name": "test-project-feature"},
|
||||
{"pattern": "hotfix/*", "job_name": "test-project-hotfix"}
|
||||
]
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(create_mapping())
|
||||
success = asyncio.run(db_service.create_project_mapping(mapping_data))
|
||||
print(f"Mapping created: {'Success' if success else 'Failed'}")
|
||||
```
|
||||
|
||||
运行脚本:
|
||||
Run the script:
|
||||
|
||||
```bash
|
||||
python create_mapping.py
|
||||
```
|
||||
|
||||
### 查看触发日志
|
||||
### View Trigger Logs
|
||||
|
||||
```python
|
||||
# view_logs.py
|
||||
import asyncio
|
||||
from app.services.database_service import get_database_service
|
||||
Refer to the API documentation for log query endpoints.
|
||||
|
||||
async def view_logs():
|
||||
db_service = get_database_service()
|
||||
|
||||
logs = await db_service.get_trigger_logs(
|
||||
repository_name="freeleaps/my-project",
|
||||
limit=10
|
||||
)
|
||||
|
||||
for log in logs:
|
||||
print(f"[{log['created_at']}] {log['repository_name']} - {log['branch_name']} - {log['status']}")
|
||||
## 📊 Monitoring and Logs
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(view_logs())
|
||||
```
|
||||
|
||||
## 📊 监控和日志
|
||||
|
||||
### 日志查看
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# 查看应用日志
|
||||
tail -f logs/app.log
|
||||
# View application logs
|
||||
tail -n 50 logs/service.log
|
||||
|
||||
# 查看 Celery 日志
|
||||
tail -f logs/celery.log
|
||||
# View Celery logs
|
||||
tail -n 50 logs/celery.log
|
||||
```
|
||||
|
||||
### 监控面板
|
||||
### Monitoring Dashboard
|
||||
|
||||
使用 Grafana 创建监控面板:
|
||||
Use Grafana to create a monitoring dashboard:
|
||||
1. Visit http://localhost:3000 (Grafana)
|
||||
2. Username: `admin`, Password: `admin`
|
||||
3. Add Prometheus data source: http://prometheus:9090
|
||||
4. Import monitoring dashboard
|
||||
|
||||
1. 访问 http://localhost:3000 (Grafana)
|
||||
2. 用户名: `admin`, 密码: `admin`
|
||||
3. 添加 Prometheus 数据源: http://prometheus:9090
|
||||
4. 导入监控面板
|
||||
### Key Metrics
|
||||
- **webhook_requests_total**: Total webhook requests
|
||||
- **webhook_request_duration_seconds**: Request response time
|
||||
- **queue_size**: Queue length
|
||||
- **dedup_hits_total**: Deduplication hit count
|
||||
|
||||
### 关键指标
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
- **webhook_requests_total**: Webhook 请求总数
|
||||
- **webhook_request_duration_seconds**: 请求响应时间
|
||||
- **queue_size**: 队列长度
|
||||
- **dedup_hits_total**: 防抖命中次数
|
||||
### Common Issues
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. Redis 连接失败
|
||||
#### 1. Redis Connection Failure
|
||||
|
||||
```bash
|
||||
# 检查 Redis 状态
|
||||
# Check Redis status
|
||||
docker ps | grep redis
|
||||
|
||||
# 重启 Redis
|
||||
docker restart webhook-redis
|
||||
# Restart Redis
|
||||
docker restart webhook-ambassador-redis
|
||||
```
|
||||
|
||||
#### 2. Celery Worker 无法启动
|
||||
#### 2. Celery Worker Fails to Start
|
||||
|
||||
```bash
|
||||
# 检查 Celery 配置
|
||||
celery -A app.tasks.jenkins_tasks inspect active
|
||||
# Check Celery configuration
|
||||
cat .env
|
||||
|
||||
# 重启 Worker
|
||||
pkill -f "celery.*worker"
|
||||
# Restart Worker
|
||||
celery -A app.tasks.jenkins_tasks worker --loglevel=info
|
||||
```
|
||||
|
||||
#### 3. Jenkins 连接失败
|
||||
#### 3. Jenkins Connection Failure
|
||||
|
||||
```bash
|
||||
# 测试 Jenkins 连接
|
||||
curl -u username:token https://jenkins.example.com/api/json
|
||||
```
|
||||
|
||||
#### 4. 数据库错误
|
||||
|
||||
```bash
|
||||
# 检查数据库文件
|
||||
ls -la webhook_ambassador.db
|
||||
|
||||
# 重新初始化数据库
|
||||
rm webhook_ambassador.db
|
||||
python -c "from app.services.database_service import get_database_service; get_database_service()"
|
||||
```
|
||||
|
||||
### 日志级别调整
|
||||
|
||||
编辑 `.env` 文件:
|
||||
|
||||
```env
|
||||
LOGGING_LEVEL=DEBUG # 开发环境
|
||||
LOGGING_LEVEL=INFO # 生产环境
|
||||
```
|
||||
|
||||
### 性能调优
|
||||
|
||||
#### 增加并发处理能力
|
||||
|
||||
```env
|
||||
QUEUE_MAX_CONCURRENT=20
|
||||
```
|
||||
|
||||
#### 调整防抖窗口
|
||||
|
||||
```env
|
||||
DEDUPLICATION_WINDOW_SECONDS=600 # 10分钟
|
||||
```
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
### 使用 Docker Compose
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f api
|
||||
```
|
||||
|
||||
### 生产环境部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t webhook-ambassador:latest .
|
||||
|
||||
# 运行容器
|
||||
docker run -d \
|
||||
--name webhook-ambassador \
|
||||
-p 8000:8000 \
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/logs:/app/logs \
|
||||
--env-file .env \
|
||||
webhook-ambassador:latest
|
||||
```
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如果遇到问题,请检查:
|
||||
|
||||
1. 日志文件中的错误信息
|
||||
2. 健康检查端点返回的状态
|
||||
3. 监控指标中的异常数据
|
||||
4. 网络连接和防火墙设置
|
||||
|
||||
更多帮助请参考项目文档或提交 Issue。
|
||||
Check Jenkins URL, username, and token in the configuration file.
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
配置管理模块
|
||||
支持环境分发、防抖策略和队列配置
|
||||
Configuration management module
|
||||
Supports environment dispatch, deduplication strategy, and queue configuration
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
@ -11,7 +11,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
class EnvironmentConfig(BaseSettings):
|
||||
"""环境配置"""
|
||||
"""Environment configuration"""
|
||||
branches: List[str] = Field(default_factory=list)
|
||||
jenkins_job: str
|
||||
jenkins_url: str
|
||||
@ -19,23 +19,23 @@ class EnvironmentConfig(BaseSettings):
|
||||
|
||||
|
||||
class DeduplicationConfig(BaseSettings):
|
||||
"""防抖配置"""
|
||||
"""Deduplication configuration"""
|
||||
enabled: bool = True
|
||||
window_seconds: int = Field(default=300, ge=1) # 5分钟防抖窗口
|
||||
window_seconds: int = Field(default=300, ge=1) # 5-minute deduplication window
|
||||
strategy: str = Field(default="commit_branch") # commit_hash + branch
|
||||
cache_ttl: int = Field(default=3600, ge=1) # 缓存1小时
|
||||
cache_ttl: int = Field(default=3600, ge=1) # Cache for 1 hour
|
||||
|
||||
|
||||
class QueueConfig(BaseSettings):
|
||||
"""队列配置"""
|
||||
"""Queue configuration"""
|
||||
max_concurrent: int = Field(default=10, ge=1)
|
||||
max_retries: int = Field(default=3, ge=0)
|
||||
retry_delay: int = Field(default=60, ge=1) # 秒
|
||||
retry_delay: int = Field(default=60, ge=1) # seconds
|
||||
priority_levels: int = Field(default=3, ge=1, le=10)
|
||||
|
||||
|
||||
class JenkinsConfig(BaseSettings):
|
||||
"""Jenkins 配置"""
|
||||
"""Jenkins configuration"""
|
||||
username: str
|
||||
token: str
|
||||
timeout: int = Field(default=30, ge=1)
|
||||
@ -43,7 +43,7 @@ class JenkinsConfig(BaseSettings):
|
||||
|
||||
|
||||
class DatabaseConfig(BaseSettings):
|
||||
"""数据库配置"""
|
||||
"""Database configuration"""
|
||||
url: str = Field(default="sqlite:///./webhook_ambassador.db")
|
||||
echo: bool = False
|
||||
pool_size: int = Field(default=10, ge=1)
|
||||
@ -51,74 +51,74 @@ class DatabaseConfig(BaseSettings):
|
||||
|
||||
|
||||
class RedisConfig(BaseSettings):
|
||||
"""Redis 配置"""
|
||||
"""Redis configuration"""
|
||||
url: str = Field(default="redis://localhost:6379/0")
|
||||
password: Optional[str] = None
|
||||
db: int = Field(default=0, ge=0)
|
||||
|
||||
|
||||
class LoggingConfig(BaseSettings):
|
||||
"""日志配置"""
|
||||
"""Logging configuration"""
|
||||
level: str = Field(default="INFO")
|
||||
format: str = Field(default="json")
|
||||
file: Optional[str] = None
|
||||
|
||||
|
||||
class SecurityConfig(BaseSettings):
|
||||
"""安全配置"""
|
||||
"""Security configuration"""
|
||||
secret_key: str
|
||||
webhook_secret_header: str = Field(default="X-Gitea-Signature")
|
||||
rate_limit_per_minute: int = Field(default=100, ge=1)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""主配置类"""
|
||||
"""Main configuration class"""
|
||||
|
||||
# 基础配置
|
||||
# Basic configuration
|
||||
app_name: str = "Gitea Webhook Ambassador"
|
||||
version: str = "1.0.0"
|
||||
debug: bool = False
|
||||
|
||||
# 服务器配置
|
||||
# Server configuration
|
||||
host: str = "0.0.0.0"
|
||||
port: int = Field(default=8000, ge=1, le=65535)
|
||||
|
||||
# 数据库配置
|
||||
# Database configuration
|
||||
database_url: str = Field(default="sqlite:///./webhook_ambassador.db")
|
||||
|
||||
# Redis 配置
|
||||
# Redis configuration
|
||||
redis_url: str = Field(default="redis://localhost:6379/0")
|
||||
redis_password: str = Field(default="")
|
||||
redis_db: int = Field(default=0)
|
||||
|
||||
# Jenkins 配置
|
||||
# Jenkins configuration
|
||||
jenkins_username: str = Field(default="admin")
|
||||
jenkins_token: str = Field(default="")
|
||||
jenkins_timeout: int = Field(default=30)
|
||||
|
||||
# 安全配置
|
||||
# Security configuration
|
||||
security_secret_key: str = Field(default="")
|
||||
security_webhook_secret_header: str = Field(default="X-Gitea-Signature")
|
||||
security_rate_limit_per_minute: int = Field(default=100)
|
||||
|
||||
# 日志配置
|
||||
# Logging configuration
|
||||
logging_level: str = Field(default="INFO")
|
||||
logging_format: str = Field(default="json")
|
||||
logging_file: str = Field(default="")
|
||||
|
||||
# 队列配置
|
||||
# Queue configuration
|
||||
queue_max_concurrent: int = Field(default=10)
|
||||
queue_max_retries: int = Field(default=3)
|
||||
queue_retry_delay: int = Field(default=60)
|
||||
queue_priority_levels: int = Field(default=3)
|
||||
|
||||
# 防抖配置
|
||||
# Deduplication configuration
|
||||
deduplication_enabled: bool = Field(default=True)
|
||||
deduplication_window_seconds: int = Field(default=300)
|
||||
deduplication_strategy: str = Field(default="commit_branch")
|
||||
deduplication_cache_ttl: int = Field(default=3600)
|
||||
|
||||
# 业务配置
|
||||
# Business configuration
|
||||
environments: Dict[str, EnvironmentConfig] = Field(default_factory=dict)
|
||||
deduplication: DeduplicationConfig = DeduplicationConfig()
|
||||
queue: QueueConfig = QueueConfig()
|
||||
@ -129,18 +129,18 @@ class Settings(BaseSettings):
|
||||
|
||||
@validator("environments", pre=True)
|
||||
def load_environments_from_file(cls, v):
|
||||
"""从配置文件加载环境配置"""
|
||||
"""Load environment configuration from file"""
|
||||
if isinstance(v, dict) and v:
|
||||
return v
|
||||
|
||||
# 尝试从配置文件加载
|
||||
# Try to load from config file
|
||||
config_file = Path("config/environments.yaml")
|
||||
if config_file.exists():
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
return config_data.get("environments", {})
|
||||
|
||||
# 默认配置
|
||||
# Default configuration
|
||||
return {
|
||||
"dev": EnvironmentConfig(
|
||||
branches=["dev", "develop", "development"],
|
||||
@ -163,27 +163,27 @@ class Settings(BaseSettings):
|
||||
}
|
||||
|
||||
def get_environment_for_branch(self, branch: str) -> Optional[EnvironmentConfig]:
|
||||
"""根据分支名获取对应的环境配置"""
|
||||
"""Get environment configuration by branch name"""
|
||||
for env_name, env_config in self.environments.items():
|
||||
if branch in env_config.branches or "*" in env_config.branches:
|
||||
return env_config
|
||||
return None
|
||||
|
||||
def get_environment_by_name(self, name: str) -> Optional[EnvironmentConfig]:
|
||||
"""根据环境名获取配置"""
|
||||
"""Get configuration by environment name"""
|
||||
return self.environments.get(name)
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
# Global configuration instance
|
||||
settings = Settings()
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
"""获取配置实例"""
|
||||
"""Get configuration instance"""
|
||||
return settings
|
||||
|
||||
|
||||
def reload_settings():
|
||||
"""重新加载配置"""
|
||||
"""Reload configuration"""
|
||||
global settings
|
||||
settings = Settings()
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
Handlers 包
|
||||
包含所有 API 处理器
|
||||
Handlers package
|
||||
Contains all API handlers
|
||||
"""
|
||||
|
||||
from . import webhook
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
管理 API 处理器
|
||||
提供项目映射和 API 密钥管理功能
|
||||
Admin API handler
|
||||
Provides project mapping and API key management features
|
||||
"""
|
||||
|
||||
import secrets
|
||||
@ -17,7 +17,7 @@ from app.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
# API 密钥相关模型
|
||||
# API key related models
|
||||
class APIKeyResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
@ -38,7 +38,7 @@ class CreateAPIKeyResponse(BaseModel):
|
||||
key: str
|
||||
created_at: datetime
|
||||
|
||||
# 项目映射相关模型
|
||||
# Project mapping related models
|
||||
class ProjectMappingRequest(BaseModel):
|
||||
repository_name: str
|
||||
default_job: str
|
||||
@ -57,13 +57,13 @@ class ProjectMappingResponse(BaseModel):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# API 密钥管理端点
|
||||
# API key management endpoints
|
||||
@router.get("/api-keys", response_model=List[APIKeyResponse])
|
||||
async def list_api_keys(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""列出所有 API 密钥"""
|
||||
"""List all API keys"""
|
||||
try:
|
||||
api_keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()
|
||||
return api_keys
|
||||
@ -76,16 +76,16 @@ async def create_api_key(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""创建新的 API 密钥"""
|
||||
"""Create a new API key"""
|
||||
try:
|
||||
# 生成 API 密钥
|
||||
# Generate API key
|
||||
api_key = secrets.token_urlsafe(32)
|
||||
key_prefix = api_key[:8] # 显示前8位作为前缀
|
||||
key_prefix = api_key[:8] # Show first 8 characters as prefix
|
||||
|
||||
# 创建数据库记录
|
||||
# Create database record
|
||||
db_api_key = APIKey(
|
||||
name=request.name,
|
||||
key_hash=api_key, # 实际应用中应该哈希存储
|
||||
key_hash=api_key, # Should be hashed in production
|
||||
key_prefix=key_prefix,
|
||||
created_at=datetime.utcnow(),
|
||||
last_used=datetime.utcnow(),
|
||||
@ -99,7 +99,7 @@ async def create_api_key(
|
||||
return CreateAPIKeyResponse(
|
||||
id=db_api_key.id,
|
||||
name=db_api_key.name,
|
||||
key=api_key, # 只在创建时返回完整密钥
|
||||
key=api_key, # Only return full key on creation
|
||||
created_at=db_api_key.created_at
|
||||
)
|
||||
|
||||
@ -113,7 +113,7 @@ async def delete_api_key(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""删除 API 密钥"""
|
||||
"""Delete API key"""
|
||||
try:
|
||||
api_key = db.query(APIKey).filter(APIKey.id == key_id).first()
|
||||
if not api_key:
|
||||
@ -136,7 +136,7 @@ async def revoke_api_key(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""撤销 API 密钥"""
|
||||
"""Revoke API key"""
|
||||
try:
|
||||
api_key = db.query(APIKey).filter(APIKey.id == key_id).first()
|
||||
if not api_key:
|
||||
@ -153,13 +153,13 @@ async def revoke_api_key(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Failed to revoke API key: {str(e)}")
|
||||
|
||||
# 项目映射管理端点
|
||||
# Project mapping management endpoints
|
||||
@router.get("/projects", response_model=List[ProjectMappingResponse])
|
||||
async def list_project_mappings(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""列出所有项目映射"""
|
||||
"""List all project mappings"""
|
||||
try:
|
||||
mappings = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all()
|
||||
return mappings
|
||||
@ -172,9 +172,9 @@ async def create_project_mapping(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""创建项目映射"""
|
||||
"""Create project mapping"""
|
||||
try:
|
||||
# 检查是否已存在
|
||||
# Check if already exists
|
||||
existing = db.query(ProjectMapping).filter(
|
||||
ProjectMapping.repository_name == request.repository_name
|
||||
).first()
|
||||
@ -182,7 +182,7 @@ async def create_project_mapping(
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Project mapping already exists")
|
||||
|
||||
# 创建新映射
|
||||
# Create new mapping
|
||||
mapping = ProjectMapping(
|
||||
repository_name=request.repository_name,
|
||||
default_job=request.default_job,
|
||||
@ -210,7 +210,7 @@ async def get_project_mapping(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取项目映射"""
|
||||
"""Get project mapping"""
|
||||
try:
|
||||
mapping = db.query(ProjectMapping).filter(
|
||||
ProjectMapping.repository_name == repository_name
|
||||
@ -232,7 +232,7 @@ async def delete_project_mapping(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""删除项目映射"""
|
||||
"""Delete project mapping"""
|
||||
try:
|
||||
mapping = db.query(ProjectMapping).filter(
|
||||
ProjectMapping.repository_name == repository_name
|
||||
@ -252,24 +252,24 @@ async def delete_project_mapping(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete project mapping: {str(e)}")
|
||||
|
||||
# 统计信息端点
|
||||
# Statistics endpoint
|
||||
@router.get("/stats")
|
||||
async def get_admin_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取管理统计信息"""
|
||||
"""Get admin statistics"""
|
||||
try:
|
||||
# API 密钥统计
|
||||
# API key statistics
|
||||
total_keys = db.query(APIKey).count()
|
||||
active_keys = db.query(APIKey).filter(APIKey.is_active == True).count()
|
||||
|
||||
# 最近使用的密钥
|
||||
# Recently used keys
|
||||
recent_keys = db.query(APIKey).filter(
|
||||
APIKey.last_used >= datetime.utcnow() - timedelta(days=7)
|
||||
).count()
|
||||
|
||||
# 项目映射统计
|
||||
# Project mapping statistics
|
||||
total_mappings = db.query(ProjectMapping).count()
|
||||
|
||||
return {
|
||||
|
||||
@ -1,145 +1,21 @@
|
||||
"""
|
||||
Health check handler
|
||||
Provides service health status checking
|
||||
Provides health check endpoints for the API
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.services.jenkins_service import get_jenkins_service
|
||||
from app.services.queue_service import get_queue_service
|
||||
from fastapi import APIRouter
|
||||
from app.config import get_settings
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter(prefix="/health", tags=["health"])
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class JenkinsStatus(BaseModel):
|
||||
status: str
|
||||
message: str = None
|
||||
|
||||
|
||||
class WorkerPoolStatus(BaseModel):
|
||||
active_workers: int
|
||||
queue_size: int
|
||||
total_processed: int
|
||||
total_failed: int
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
service: str
|
||||
version: str
|
||||
timestamp: datetime
|
||||
jenkins: JenkinsStatus
|
||||
worker_pool: WorkerPoolStatus
|
||||
database: Dict[str, Any]
|
||||
|
||||
|
||||
@router.get("/", response_model=HealthResponse)
|
||||
async def health_check(db: Session = Depends(get_db)):
|
||||
"""
|
||||
Health check endpoint
|
||||
Check the status of each service component
|
||||
"""
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
settings = get_settings()
|
||||
|
||||
# Check Jenkins connection
|
||||
jenkins_service = get_jenkins_service()
|
||||
jenkins_status = JenkinsStatus(status="disconnected", message="Unable to connect to Jenkins server")
|
||||
|
||||
try:
|
||||
if await jenkins_service.test_connection():
|
||||
jenkins_status = JenkinsStatus(status="connected")
|
||||
except Exception as e:
|
||||
jenkins_status.message = f"Connection failed: {str(e)}"
|
||||
|
||||
# Get worker pool stats
|
||||
queue_service = get_queue_service()
|
||||
try:
|
||||
stats = await queue_service.get_stats()
|
||||
worker_pool_status = WorkerPoolStatus(
|
||||
active_workers=stats.get("active_workers", 0),
|
||||
queue_size=stats.get("queue_size", 0),
|
||||
total_processed=stats.get("total_processed", 0),
|
||||
total_failed=stats.get("total_failed", 0)
|
||||
)
|
||||
except Exception as e:
|
||||
worker_pool_status = WorkerPoolStatus(
|
||||
active_workers=0,
|
||||
queue_size=0,
|
||||
total_processed=0,
|
||||
total_failed=0
|
||||
)
|
||||
|
||||
# Check database connection
|
||||
database_status = {"status": "disconnected", "message": "Database connection failed"}
|
||||
try:
|
||||
# 尝试执行简单查询
|
||||
db.execute("SELECT 1")
|
||||
database_status = {"status": "connected"}
|
||||
except Exception as e:
|
||||
database_status["message"] = f"Database error: {str(e)}"
|
||||
|
||||
# Determine overall status
|
||||
overall_status = "healthy"
|
||||
if jenkins_status.status != "connected":
|
||||
overall_status = "unhealthy"
|
||||
|
||||
return HealthResponse(
|
||||
status=overall_status,
|
||||
service="Gitea Webhook Ambassador",
|
||||
version=settings.version,
|
||||
timestamp=datetime.utcnow(),
|
||||
jenkins=jenkins_status,
|
||||
worker_pool=worker_pool_status,
|
||||
database=database_status
|
||||
)
|
||||
|
||||
|
||||
@router.get("/simple")
|
||||
async def simple_health_check():
|
||||
"""
|
||||
Simple health check endpoint
|
||||
For load balancers and monitoring systems
|
||||
"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "Gitea Webhook Ambassador",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/ready")
|
||||
async def readiness_check(db: Session = Depends(get_db)):
|
||||
"""
|
||||
Readiness check endpoint
|
||||
Check if the service is ready to receive requests
|
||||
"""
|
||||
try:
|
||||
# Check database connection
|
||||
db.execute("SELECT 1")
|
||||
|
||||
# Check Jenkins connection
|
||||
jenkins_service = get_jenkins_service()
|
||||
jenkins_ready = await jenkins_service.test_connection()
|
||||
|
||||
if jenkins_ready:
|
||||
return {"status": "ready"}
|
||||
else:
|
||||
return {"status": "not_ready", "reason": "Jenkins connection failed"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "not_ready", "reason": f"Database connection failed: {str(e)}"}
|
||||
|
||||
|
||||
@router.get("/live")
|
||||
async def liveness_check():
|
||||
"""
|
||||
Liveness check endpoint
|
||||
Check if the service process is running normally
|
||||
"""
|
||||
return {"status": "alive"}
|
||||
"version": settings.version,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
Webhook 处理器
|
||||
处理来自 Gitea 的 webhook 请求
|
||||
Webhook handler
|
||||
Handles webhook requests from Gitea
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
@ -11,9 +11,9 @@ from app.tasks.jenkins_tasks import get_celery_app
|
||||
router = APIRouter()
|
||||
|
||||
def get_webhook_service() -> WebhookService:
|
||||
"""获取 webhook 服务实例"""
|
||||
# 这里应该从依赖注入容器获取
|
||||
# 暂时返回 None,实际使用时需要正确实现
|
||||
"""Get webhook service instance"""
|
||||
# Should get from dependency injection container
|
||||
# Temporarily return None, implement properly in actual use
|
||||
return None
|
||||
|
||||
@router.post("/gitea")
|
||||
@ -21,15 +21,15 @@ async def handle_gitea_webhook(
|
||||
request: Request,
|
||||
webhook_service: WebhookService = Depends(get_webhook_service)
|
||||
):
|
||||
"""处理 Gitea webhook 请求"""
|
||||
"""Handle Gitea webhook request"""
|
||||
if webhook_service is None:
|
||||
raise HTTPException(status_code=503, detail="Webhook service not available")
|
||||
|
||||
try:
|
||||
# 获取请求体
|
||||
# Get request body
|
||||
body = await request.body()
|
||||
|
||||
# 处理 webhook
|
||||
# Process webhook
|
||||
result = await webhook_service.process_webhook(body, request.headers)
|
||||
|
||||
return {
|
||||
|
||||
@ -10,7 +10,7 @@ import secrets
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
# 配置日志
|
||||
# Configure logging
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.stdlib.filter_by_level,
|
||||
@ -31,14 +31,14 @@ structlog.configure(
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
# Create FastAPI application
|
||||
app = FastAPI(
|
||||
title="Gitea Webhook Ambassador (Demo)",
|
||||
description="高性能的 Gitea 到 Jenkins 的 Webhook 服务 - 演示版本",
|
||||
description="High-performance webhook service from Gitea to Jenkins - Demo Version",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 添加 CORS 中间件
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@ -47,13 +47,13 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 安全配置
|
||||
# Security configuration
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
# 演示数据存储
|
||||
# Demo data storage
|
||||
api_keys = {
|
||||
"demo_admin_key": {
|
||||
"name": "演示管理员密钥",
|
||||
"name": "Demo Admin Key",
|
||||
"key_hash": "demo_admin_key",
|
||||
"key_prefix": "demo_adm",
|
||||
"created_at": datetime.utcnow(),
|
||||
@ -62,7 +62,7 @@ api_keys = {
|
||||
"role": "admin"
|
||||
},
|
||||
"demo_user_key": {
|
||||
"name": "演示用户密钥",
|
||||
"name": "Demo User Key",
|
||||
"key_hash": "demo_user_key",
|
||||
"key_prefix": "demo_usr",
|
||||
"created_at": datetime.utcnow(),
|
||||
@ -122,7 +122,7 @@ project_mappings = {
|
||||
}
|
||||
}
|
||||
|
||||
# 请求/响应模型
|
||||
# Request/response models
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
service: str
|
||||
@ -160,12 +160,12 @@ class ProjectMappingResponse(BaseModel):
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# 认证函数
|
||||
# Authentication functions
|
||||
def verify_api_key(api_key: str):
|
||||
"""验证 API 密钥"""
|
||||
"""Verify API key"""
|
||||
for key_id, key_data in api_keys.items():
|
||||
if key_data["key_hash"] == api_key and key_data["is_active"]:
|
||||
# 更新最后使用时间
|
||||
# Update last used time
|
||||
key_data["last_used"] = datetime.utcnow()
|
||||
return key_data
|
||||
return None
|
||||
@ -173,17 +173,17 @@ def verify_api_key(api_key: str):
|
||||
async def get_current_user(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
||||
):
|
||||
"""获取当前用户(支持 API 密钥认证)"""
|
||||
"""Get current user (supports API key authentication)"""
|
||||
if not credentials:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="需要认证令牌",
|
||||
detail="Authentication token required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token = credentials.credentials
|
||||
|
||||
# 验证 API 密钥
|
||||
# Verify API key
|
||||
api_key_data = verify_api_key(token)
|
||||
if api_key_data:
|
||||
return {
|
||||
@ -192,17 +192,17 @@ async def get_current_user(
|
||||
"role": api_key_data["role"]
|
||||
}
|
||||
|
||||
# 认证失败
|
||||
# Authentication failed
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证令牌",
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 公开端点
|
||||
# Public endpoints
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""健康检查端点"""
|
||||
"""Health check endpoint"""
|
||||
settings = get_settings()
|
||||
|
||||
return HealthResponse(
|
||||
@ -222,11 +222,11 @@ async def health_check():
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径"""
|
||||
"""Root path"""
|
||||
return {
|
||||
"name": "Gitea Webhook Ambassador (Demo)",
|
||||
"version": "1.0.0",
|
||||
"description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务 - 演示版本",
|
||||
"description": "High-performance webhook service from Gitea to Jenkins - Demo Version",
|
||||
"endpoints": {
|
||||
"webhook": "/webhook/gitea",
|
||||
"health": "/health",
|
||||
@ -241,16 +241,16 @@ async def root():
|
||||
|
||||
@app.post("/webhook/gitea")
|
||||
async def handle_gitea_webhook(request: Request):
|
||||
"""处理 Gitea webhook 请求"""
|
||||
"""Handle Gitea webhook request"""
|
||||
try:
|
||||
body = await request.body()
|
||||
|
||||
# 记录 webhook 请求
|
||||
# Log webhook request
|
||||
logger.info("Received Gitea webhook",
|
||||
body_size=len(body),
|
||||
headers=dict(request.headers))
|
||||
|
||||
# 添加新的触发日志
|
||||
# Add new trigger log
|
||||
log_entry = {
|
||||
"id": len(trigger_logs) + 1,
|
||||
"repository_name": "demo-repo",
|
||||
@ -283,7 +283,7 @@ async def handle_gitea_webhook(request: Request):
|
||||
}
|
||||
)
|
||||
|
||||
# 需要认证的端点
|
||||
# Authenticated endpoints
|
||||
@app.get("/api/logs", response_model=List[TriggerLogResponse])
|
||||
async def get_trigger_logs(
|
||||
repository: Optional[str] = None,
|
||||
@ -291,8 +291,8 @@ async def get_trigger_logs(
|
||||
limit: int = 100,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取触发日志"""
|
||||
print(f"用户 {current_user['username']} 访问日志端点")
|
||||
"""Get trigger logs"""
|
||||
print(f"User {current_user['username']} accessed logs endpoint")
|
||||
|
||||
filtered_logs = trigger_logs.copy()
|
||||
|
||||
@ -301,24 +301,24 @@ async def get_trigger_logs(
|
||||
if branch:
|
||||
filtered_logs = [log for log in filtered_logs if log["branch_name"] == branch]
|
||||
|
||||
# 按时间倒序排列并限制数量
|
||||
# Sort by time descending and limit
|
||||
filtered_logs.sort(key=lambda x: x["created_at"], reverse=True)
|
||||
return filtered_logs[:limit]
|
||||
|
||||
@app.get("/api/logs/stats")
|
||||
async def get_log_stats(current_user: dict = Depends(get_current_user)):
|
||||
"""获取日志统计信息"""
|
||||
print(f"用户 {current_user['username']} 访问日志统计")
|
||||
"""Get log statistics"""
|
||||
print(f"User {current_user['username']} accessed log statistics")
|
||||
|
||||
total_logs = len(trigger_logs)
|
||||
successful_logs = len([log for log in trigger_logs if log["status"] == "success"])
|
||||
failed_logs = len([log for log in trigger_logs if log["status"] == "failed"])
|
||||
|
||||
# 最近24小时的日志数
|
||||
# Logs in the last 24 hours
|
||||
yesterday = datetime.utcnow() - timedelta(days=1)
|
||||
recent_logs = len([log for log in trigger_logs if log["created_at"] >= yesterday])
|
||||
|
||||
# 按仓库分组的统计
|
||||
# Grouped by repository
|
||||
repo_stats = {}
|
||||
for log in trigger_logs:
|
||||
repo = log["repository_name"]
|
||||
@ -337,11 +337,11 @@ async def get_log_stats(current_user: dict = Depends(get_current_user)):
|
||||
|
||||
@app.get("/api/admin/api-keys", response_model=List[APIKeyResponse])
|
||||
async def list_api_keys(current_user: dict = Depends(get_current_user)):
|
||||
"""列出所有 API 密钥(仅管理员)"""
|
||||
"""List all API keys (admin only)"""
|
||||
if current_user["role"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="需要管理员权限")
|
||||
raise HTTPException(status_code=403, detail="Admin privileges required")
|
||||
|
||||
print(f"管理员 {current_user['username']} 查看 API 密钥列表")
|
||||
print(f"Admin {current_user['username']} viewed API key list")
|
||||
|
||||
return [
|
||||
APIKeyResponse(
|
||||
@ -358,8 +358,8 @@ async def list_api_keys(current_user: dict = Depends(get_current_user)):
|
||||
|
||||
@app.get("/api/admin/projects", response_model=List[ProjectMappingResponse])
|
||||
async def list_project_mappings(current_user: dict = Depends(get_current_user)):
|
||||
"""列出所有项目映射"""
|
||||
print(f"用户 {current_user['username']} 查看项目映射")
|
||||
"""List all project mappings"""
|
||||
print(f"User {current_user['username']} viewed project mappings")
|
||||
|
||||
return [
|
||||
ProjectMappingResponse(
|
||||
@ -376,13 +376,13 @@ async def list_project_mappings(current_user: dict = Depends(get_current_user)):
|
||||
|
||||
@app.get("/api/admin/stats")
|
||||
async def get_admin_stats(current_user: dict = Depends(get_current_user)):
|
||||
"""获取管理统计信息"""
|
||||
print(f"用户 {current_user['username']} 查看管理统计")
|
||||
"""Get admin statistics"""
|
||||
print(f"User {current_user['username']} viewed admin statistics")
|
||||
|
||||
total_keys = len(api_keys)
|
||||
active_keys = len([key for key in api_keys.values() if key["is_active"]])
|
||||
|
||||
# 最近使用的密钥
|
||||
# Recently used keys
|
||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||||
recent_keys = len([
|
||||
key for key in api_keys.values()
|
||||
@ -402,10 +402,10 @@ async def get_admin_stats(current_user: dict = Depends(get_current_user)):
|
||||
}
|
||||
}
|
||||
|
||||
# 中间件
|
||||
# Middleware
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
"""请求日志中间件"""
|
||||
"""Request logging middleware"""
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
response = await call_next(request)
|
||||
@ -419,13 +419,13 @@ if __name__ == "__main__":
|
||||
import uvicorn
|
||||
settings = get_settings()
|
||||
|
||||
print("🚀 启动 Gitea Webhook Ambassador 演示版本")
|
||||
print("🚀 Starting Gitea Webhook Ambassador Demo Version")
|
||||
print("=" * 60)
|
||||
print("📋 演示 API 密钥:")
|
||||
print(" 管理员密钥: demo_admin_key")
|
||||
print(" 用户密钥: demo_user_key")
|
||||
print("📋 Demo API Keys:")
|
||||
print(" Admin key: demo_admin_key")
|
||||
print(" User key: demo_user_key")
|
||||
print()
|
||||
print("🔧 使用示例:")
|
||||
print("🔧 Usage examples:")
|
||||
print(" curl -H 'Authorization: Bearer demo_admin_key' http://localhost:8000/api/admin/api-keys")
|
||||
print(" curl -H 'Authorization: Bearer demo_user_key' http://localhost:8000/api/logs")
|
||||
print("=" * 60)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
简化版 FastAPI 应用主入口
|
||||
用于快速启动和测试
|
||||
Simplified FastAPI application entry point
|
||||
For quick start and testing
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
@ -15,7 +15,7 @@ from app.handlers.health import router as health_router
|
||||
from app.handlers.logs import router as logs_router
|
||||
from app.handlers.admin import router as admin_router
|
||||
|
||||
# 配置日志
|
||||
# Configure logging
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.stdlib.filter_by_level,
|
||||
@ -36,14 +36,14 @@ structlog.configure(
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
# Create FastAPI application
|
||||
app = FastAPI(
|
||||
title="Gitea Webhook Ambassador",
|
||||
description="高性能的 Gitea 到 Jenkins 的 Webhook 服务",
|
||||
description="High-performance webhook service from Gitea to Jenkins",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 添加 CORS 中间件
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@ -52,7 +52,7 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 包含路由
|
||||
# Include routers
|
||||
app.include_router(webhook_router)
|
||||
app.include_router(health_router)
|
||||
app.include_router(logs_router)
|
||||
@ -60,11 +60,11 @@ app.include_router(admin_router)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径"""
|
||||
"""Root path"""
|
||||
return {
|
||||
"name": "Gitea Webhook Ambassador",
|
||||
"version": "1.0.0",
|
||||
"description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务",
|
||||
"description": "High-performance webhook service from Gitea to Jenkins",
|
||||
"endpoints": {
|
||||
"webhook": "/webhook/gitea",
|
||||
"health": "/health",
|
||||
@ -76,10 +76,10 @@ async def root():
|
||||
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
"""请求日志中间件"""
|
||||
"""Request logging middleware"""
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
# 记录请求
|
||||
# Log request
|
||||
logger.info(
|
||||
"Request started",
|
||||
method=request.method,
|
||||
@ -87,13 +87,13 @@ async def log_requests(request: Request, call_next):
|
||||
client_ip=request.client.host if request.client else None
|
||||
)
|
||||
|
||||
# 处理请求
|
||||
# Process request
|
||||
response = await call_next(request)
|
||||
|
||||
# 计算处理时间
|
||||
# Calculate processing time
|
||||
process_time = (datetime.utcnow() - start_time).total_seconds()
|
||||
|
||||
# 记录响应
|
||||
# Log response
|
||||
logger.info(
|
||||
"Request completed",
|
||||
method=request.method,
|
||||
@ -102,14 +102,14 @@ async def log_requests(request: Request, call_next):
|
||||
process_time=process_time
|
||||
)
|
||||
|
||||
# 添加处理时间到响应头
|
||||
# Add processing time to response header
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
|
||||
return response
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""全局异常处理器"""
|
||||
"""Global exception handler"""
|
||||
logger.error(
|
||||
"Unhandled exception",
|
||||
method=request.method,
|
||||
|
||||
@ -5,7 +5,7 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||
Base = declarative_base()
|
||||
|
||||
class APIKey(Base):
|
||||
"""API 密钥模型"""
|
||||
"""API key model"""
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
@ -25,7 +25,7 @@ class ProjectMapping(Base):
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
# Relationships
|
||||
branch_jobs = relationship("BranchJob", back_populates="project", cascade="all, delete-orphan")
|
||||
branch_patterns = relationship("BranchPattern", back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
@ -39,7 +39,7 @@ class BranchJob(Base):
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
# Relationship
|
||||
project = relationship("ProjectMapping", back_populates="branch_jobs")
|
||||
|
||||
class BranchPattern(Base):
|
||||
@ -52,7 +52,7 @@ class BranchPattern(Base):
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
# Relationship
|
||||
project = relationship("ProjectMapping", back_populates="branch_patterns")
|
||||
|
||||
class TriggerLog(Base):
|
||||
@ -67,23 +67,23 @@ class TriggerLog(Base):
|
||||
error_message = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
|
||||
# 数据库配置
|
||||
# Database configuration
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./gitea_webhook_ambassador.db")
|
||||
|
||||
# 创建引擎
|
||||
# Create engine
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
|
||||
)
|
||||
|
||||
# 创建会话
|
||||
# Create session
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 创建表
|
||||
# Create tables
|
||||
def create_tables():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# 获取数据库会话
|
||||
# Get database session
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Gitea Webhook 数据模型
|
||||
Gitea Webhook data model
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
@ -8,7 +8,7 @@ from datetime import datetime
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
"""Gitea 用户模型"""
|
||||
"""Gitea user model"""
|
||||
id: int
|
||||
login: str
|
||||
full_name: Optional[str] = None
|
||||
@ -17,7 +17,7 @@ class User(BaseModel):
|
||||
|
||||
|
||||
class Commit(BaseModel):
|
||||
"""Git 提交模型"""
|
||||
"""Git commit model"""
|
||||
id: str
|
||||
message: str
|
||||
url: str
|
||||
@ -26,7 +26,7 @@ class Commit(BaseModel):
|
||||
|
||||
|
||||
class Repository(BaseModel):
|
||||
"""Git 仓库模型"""
|
||||
"""Git repository model"""
|
||||
id: int
|
||||
name: str
|
||||
owner: User
|
||||
@ -39,7 +39,7 @@ class Repository(BaseModel):
|
||||
|
||||
|
||||
class GiteaWebhook(BaseModel):
|
||||
"""Gitea Webhook 模型"""
|
||||
"""Gitea Webhook model"""
|
||||
secret: Optional[str] = None
|
||||
ref: str
|
||||
before: str
|
||||
@ -50,41 +50,41 @@ class GiteaWebhook(BaseModel):
|
||||
pusher: User
|
||||
|
||||
def get_branch_name(self) -> str:
|
||||
"""从 ref 中提取分支名"""
|
||||
"""Extract branch name from ref"""
|
||||
prefix = "refs/heads/"
|
||||
if self.ref.startswith(prefix):
|
||||
return self.ref[len(prefix):]
|
||||
return self.ref
|
||||
|
||||
def get_event_id(self) -> str:
|
||||
"""生成唯一的事件 ID"""
|
||||
"""Generate unique event ID"""
|
||||
return f"{self.repository.full_name}-{self.after}"
|
||||
|
||||
def get_commit_hash(self) -> str:
|
||||
"""获取提交哈希"""
|
||||
"""Get commit hash"""
|
||||
return self.after
|
||||
|
||||
def get_deduplication_key(self) -> str:
|
||||
"""生成防抖键值"""
|
||||
"""Generate deduplication key"""
|
||||
branch = self.get_branch_name()
|
||||
return f"{self.after}:{branch}"
|
||||
|
||||
def is_push_event(self) -> bool:
|
||||
"""判断是否为推送事件"""
|
||||
"""Determine if it is a push event"""
|
||||
return self.ref.startswith("refs/heads/")
|
||||
|
||||
def is_tag_event(self) -> bool:
|
||||
"""判断是否为标签事件"""
|
||||
"""Determine if it is a tag event"""
|
||||
return self.ref.startswith("refs/tags/")
|
||||
|
||||
def get_commit_message(self) -> str:
|
||||
"""获取提交信息"""
|
||||
"""Get commit message"""
|
||||
if self.commits:
|
||||
return self.commits[0].message
|
||||
return ""
|
||||
|
||||
def get_author_info(self) -> dict:
|
||||
"""获取作者信息"""
|
||||
"""Get author information"""
|
||||
if self.commits:
|
||||
author = self.commits[0].author
|
||||
return {
|
||||
@ -100,7 +100,7 @@ class GiteaWebhook(BaseModel):
|
||||
|
||||
|
||||
class WebhookEvent(BaseModel):
|
||||
"""Webhook 事件模型"""
|
||||
"""Webhook event model"""
|
||||
id: str
|
||||
repository: str
|
||||
branch: str
|
||||
@ -116,7 +116,7 @@ class WebhookEvent(BaseModel):
|
||||
|
||||
|
||||
class WebhookResponse(BaseModel):
|
||||
"""Webhook 响应模型"""
|
||||
"""Webhook response model"""
|
||||
success: bool
|
||||
message: str
|
||||
event_id: Optional[str] = None
|
||||
|
||||
@ -5,7 +5,7 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||
Base = declarative_base()
|
||||
|
||||
class ProjectMapping(Base):
|
||||
"""项目映射模型"""
|
||||
"""Project mapping model"""
|
||||
__tablename__ = "project_mappings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
@ -5,7 +5,7 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||
Base = declarative_base()
|
||||
|
||||
class TriggerLog(Base):
|
||||
"""触发日志模型"""
|
||||
"""Trigger log model"""
|
||||
__tablename__ = "trigger_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
数据库服务
|
||||
实现项目映射、分支模式匹配等功能
|
||||
Database service
|
||||
Implements project mapping, branch pattern matching, and related features
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -18,10 +18,9 @@ from app.config import get_settings
|
||||
logger = structlog.get_logger()
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# 数据库模型
|
||||
# Database models
|
||||
class APIKey(Base):
|
||||
"""API 密钥模型"""
|
||||
"""API key model"""
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
@ -30,9 +29,8 @@ class APIKey(Base):
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class ProjectMapping(Base):
|
||||
"""项目映射模型"""
|
||||
"""Project mapping model"""
|
||||
__tablename__ = "project_mappings"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
@ -41,13 +39,12 @@ class ProjectMapping(Base):
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关系
|
||||
# Relationships
|
||||
branch_jobs = relationship("BranchJob", back_populates="project", cascade="all, delete-orphan")
|
||||
branch_patterns = relationship("BranchPattern", back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class BranchJob(Base):
|
||||
"""分支任务映射模型"""
|
||||
"""Branch job mapping model"""
|
||||
__tablename__ = "branch_jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
@ -57,12 +54,11 @@ class BranchJob(Base):
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关系
|
||||
# Relationship
|
||||
project = relationship("ProjectMapping", back_populates="branch_jobs")
|
||||
|
||||
|
||||
class BranchPattern(Base):
|
||||
"""分支模式映射模型"""
|
||||
"""Branch pattern mapping model"""
|
||||
__tablename__ = "branch_patterns"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
@ -72,12 +68,11 @@ class BranchPattern(Base):
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关系
|
||||
# Relationship
|
||||
project = relationship("ProjectMapping", back_populates="branch_patterns")
|
||||
|
||||
|
||||
class TriggerLog(Base):
|
||||
"""触发日志模型"""
|
||||
"""Trigger log model"""
|
||||
__tablename__ = "trigger_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
@ -89,9 +84,8 @@ class TriggerLog(Base):
|
||||
error_message = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class DatabaseService:
|
||||
"""数据库服务"""
|
||||
"""Database service"""
|
||||
|
||||
def __init__(self):
|
||||
self.settings = get_settings()
|
||||
@ -100,7 +94,7 @@ class DatabaseService:
|
||||
self._init_database()
|
||||
|
||||
def _init_database(self):
|
||||
"""初始化数据库"""
|
||||
"""Initialize database"""
|
||||
try:
|
||||
self.engine = create_engine(
|
||||
self.settings.database.url,
|
||||
@ -109,10 +103,10 @@ class DatabaseService:
|
||||
max_overflow=self.settings.database.max_overflow
|
||||
)
|
||||
|
||||
# 创建表
|
||||
# Create tables
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
|
||||
# 创建会话工厂
|
||||
# Create session factory
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||
|
||||
logger.info("Database initialized successfully")
|
||||
@ -122,18 +116,16 @@ class DatabaseService:
|
||||
raise
|
||||
|
||||
def get_session(self):
|
||||
"""获取数据库会话"""
|
||||
"""Get database session"""
|
||||
return self.SessionLocal()
|
||||
|
||||
async def get_project_mapping(self, repository_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取项目映射
|
||||
|
||||
Get project mapping
|
||||
Args:
|
||||
repository_name: 仓库名
|
||||
|
||||
repository_name: repository name
|
||||
Returns:
|
||||
Dict: 项目映射信息
|
||||
Dict: project mapping info
|
||||
"""
|
||||
try:
|
||||
def _get_mapping():
|
||||
@ -146,7 +138,7 @@ class DatabaseService:
|
||||
if not project:
|
||||
return None
|
||||
|
||||
# 构建返回数据
|
||||
# Build return data
|
||||
result = {
|
||||
"id": project.id,
|
||||
"repository_name": project.repository_name,
|
||||
@ -155,7 +147,7 @@ class DatabaseService:
|
||||
"branch_patterns": []
|
||||
}
|
||||
|
||||
# 添加分支任务映射
|
||||
# Add branch job mappings
|
||||
for branch_job in project.branch_jobs:
|
||||
result["branch_jobs"].append({
|
||||
"id": branch_job.id,
|
||||
@ -163,7 +155,7 @@ class DatabaseService:
|
||||
"job_name": branch_job.job_name
|
||||
})
|
||||
|
||||
# 添加分支模式映射
|
||||
# Add branch pattern mappings
|
||||
for pattern in project.branch_patterns:
|
||||
result["branch_patterns"].append({
|
||||
"id": pattern.id,
|
||||
@ -176,7 +168,7 @@ class DatabaseService:
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# 在线程池中执行数据库操作
|
||||
# Run DB operation in thread pool
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, _get_mapping)
|
||||
|
||||
@ -187,28 +179,26 @@ class DatabaseService:
|
||||
|
||||
async def determine_job_name(self, repository_name: str, branch_name: str) -> Optional[str]:
|
||||
"""
|
||||
根据分支名确定任务名
|
||||
|
||||
Determine job name by branch
|
||||
Args:
|
||||
repository_name: 仓库名
|
||||
branch_name: 分支名
|
||||
|
||||
repository_name: repository name
|
||||
branch_name: branch name
|
||||
Returns:
|
||||
str: 任务名
|
||||
str: job name
|
||||
"""
|
||||
try:
|
||||
project = await self.get_project_mapping(repository_name)
|
||||
if not project:
|
||||
return None
|
||||
|
||||
# 1. 检查精确分支匹配
|
||||
# 1. Check exact branch match
|
||||
for branch_job in project["branch_jobs"]:
|
||||
if branch_job["branch_name"] == branch_name:
|
||||
logger.debug("Found exact branch match",
|
||||
branch=branch_name, job=branch_job["job_name"])
|
||||
return branch_job["job_name"]
|
||||
|
||||
# 2. 检查模式匹配
|
||||
# 2. Check pattern match
|
||||
for pattern in project["branch_patterns"]:
|
||||
try:
|
||||
if re.match(pattern["pattern"], branch_name):
|
||||
@ -221,7 +211,7 @@ class DatabaseService:
|
||||
pattern=pattern["pattern"], error=str(e))
|
||||
continue
|
||||
|
||||
# 3. 使用默认任务
|
||||
# 3. Use default job
|
||||
if project["default_job"]:
|
||||
logger.debug("Using default job",
|
||||
branch=branch_name, job=project["default_job"])
|
||||
@ -237,13 +227,11 @@ class DatabaseService:
|
||||
|
||||
async def log_trigger(self, log_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
记录触发日志
|
||||
|
||||
Log trigger event
|
||||
Args:
|
||||
log_data: 日志数据
|
||||
|
||||
log_data: log data
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
bool: success or not
|
||||
"""
|
||||
try:
|
||||
def _log_trigger():
|
||||
@ -279,15 +267,13 @@ class DatabaseService:
|
||||
async def get_trigger_logs(self, repository_name: str = None,
|
||||
branch_name: str = None, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取触发日志
|
||||
|
||||
Get trigger logs
|
||||
Args:
|
||||
repository_name: 仓库名(可选)
|
||||
branch_name: 分支名(可选)
|
||||
limit: 限制数量
|
||||
|
||||
repository_name: repository name (optional)
|
||||
branch_name: branch name (optional)
|
||||
limit: limit number
|
||||
Returns:
|
||||
List: 日志列表
|
||||
List: log list
|
||||
"""
|
||||
try:
|
||||
def _get_logs():
|
||||
@ -329,28 +315,26 @@ class DatabaseService:
|
||||
|
||||
async def create_project_mapping(self, mapping_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
创建项目映射
|
||||
|
||||
Create project mapping
|
||||
Args:
|
||||
mapping_data: 映射数据
|
||||
|
||||
mapping_data: mapping data
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
bool: success or not
|
||||
"""
|
||||
try:
|
||||
def _create_mapping():
|
||||
session = self.get_session()
|
||||
try:
|
||||
# 创建项目映射
|
||||
# Create project mapping
|
||||
project = ProjectMapping(
|
||||
repository_name=mapping_data["repository_name"],
|
||||
default_job=mapping_data.get("default_job")
|
||||
)
|
||||
|
||||
session.add(project)
|
||||
session.flush() # 获取 ID
|
||||
session.flush() # Get ID
|
||||
|
||||
# 添加分支任务映射
|
||||
# Add branch job mappings
|
||||
for branch_job in mapping_data.get("branch_jobs", []):
|
||||
job = BranchJob(
|
||||
project_id=project.id,
|
||||
@ -359,7 +343,7 @@ class DatabaseService:
|
||||
)
|
||||
session.add(job)
|
||||
|
||||
# 添加分支模式映射
|
||||
# Add branch pattern mappings
|
||||
for pattern in mapping_data.get("branch_patterns", []):
|
||||
pattern_obj = BranchPattern(
|
||||
project_id=project.id,
|
||||
@ -385,13 +369,11 @@ class DatabaseService:
|
||||
logger.error("Failed to create project mapping", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# 全局数据库服务实例
|
||||
# Global database service instance
|
||||
_database_service: Optional[DatabaseService] = None
|
||||
|
||||
|
||||
def get_database_service() -> DatabaseService:
|
||||
"""获取数据库服务实例"""
|
||||
"""Get database service instance"""
|
||||
global _database_service
|
||||
if _database_service is None:
|
||||
_database_service = DatabaseService()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
防抖服务
|
||||
实现基于 commit hash + 分支的去重策略
|
||||
Deduplication service
|
||||
Implements deduplication strategy based on commit hash + branch
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -15,9 +15,8 @@ from app.config import get_settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DeduplicationService:
|
||||
"""防抖服务"""
|
||||
"""Deduplication service"""
|
||||
|
||||
def __init__(self, redis_client: aioredis.Redis):
|
||||
self.redis = redis_client
|
||||
@ -26,13 +25,11 @@ class DeduplicationService:
|
||||
|
||||
async def is_duplicate(self, dedup_key: str) -> bool:
|
||||
"""
|
||||
检查是否为重复事件
|
||||
|
||||
Check if the event is a duplicate
|
||||
Args:
|
||||
dedup_key: 防抖键值 (commit_hash:branch)
|
||||
|
||||
dedup_key: deduplication key (commit_hash:branch)
|
||||
Returns:
|
||||
bool: True 表示重复,False 表示新事件
|
||||
bool: True if duplicate, False if new event
|
||||
"""
|
||||
if not self.settings.deduplication.enabled:
|
||||
return False
|
||||
@ -40,13 +37,13 @@ class DeduplicationService:
|
||||
try:
|
||||
cache_key = f"{self.cache_prefix}{dedup_key}"
|
||||
|
||||
# 检查是否在缓存中
|
||||
# Check if in cache
|
||||
exists = await self.redis.exists(cache_key)
|
||||
if exists:
|
||||
logger.info("Duplicate event detected", dedup_key=dedup_key)
|
||||
return True
|
||||
|
||||
# 记录新事件
|
||||
# Record new event
|
||||
await self._record_event(cache_key, dedup_key)
|
||||
logger.info("New event recorded", dedup_key=dedup_key)
|
||||
return False
|
||||
@ -54,13 +51,13 @@ class DeduplicationService:
|
||||
except Exception as e:
|
||||
logger.error("Error checking duplication",
|
||||
dedup_key=dedup_key, error=str(e))
|
||||
# 出错时允许通过,避免阻塞
|
||||
# Allow through on error to avoid blocking
|
||||
return False
|
||||
|
||||
async def _record_event(self, cache_key: str, dedup_key: str):
|
||||
"""记录事件到缓存"""
|
||||
"""Record event to cache"""
|
||||
try:
|
||||
# 设置缓存,TTL 为防抖窗口时间
|
||||
# Set cache, TTL is deduplication window
|
||||
ttl = self.settings.deduplication.cache_ttl
|
||||
await self.redis.setex(cache_key, ttl, json.dumps({
|
||||
"dedup_key": dedup_key,
|
||||
@ -68,7 +65,7 @@ class DeduplicationService:
|
||||
"ttl": ttl
|
||||
}))
|
||||
|
||||
# 同时记录到时间窗口缓存
|
||||
# Also record to window cache
|
||||
window_key = f"{self.cache_prefix}window:{dedup_key}"
|
||||
window_ttl = self.settings.deduplication.window_seconds
|
||||
await self.redis.setex(window_key, window_ttl, "1")
|
||||
@ -78,7 +75,7 @@ class DeduplicationService:
|
||||
cache_key=cache_key, error=str(e))
|
||||
|
||||
async def get_event_info(self, dedup_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取事件信息"""
|
||||
"""Get event info"""
|
||||
try:
|
||||
cache_key = f"{self.cache_prefix}{dedup_key}"
|
||||
data = await self.redis.get(cache_key)
|
||||
@ -91,12 +88,12 @@ class DeduplicationService:
|
||||
return None
|
||||
|
||||
async def clear_event(self, dedup_key: str) -> bool:
|
||||
"""清除事件记录"""
|
||||
"""Clear event record"""
|
||||
try:
|
||||
cache_key = f"{self.cache_prefix}{dedup_key}"
|
||||
window_key = f"{self.cache_prefix}window:{dedup_key}"
|
||||
|
||||
# 删除两个缓存键
|
||||
# Delete both cache keys
|
||||
await self.redis.delete(cache_key, window_key)
|
||||
logger.info("Event cleared", dedup_key=dedup_key)
|
||||
return True
|
||||
@ -107,18 +104,18 @@ class DeduplicationService:
|
||||
return False
|
||||
|
||||
async def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取防抖统计信息"""
|
||||
"""Get deduplication statistics"""
|
||||
try:
|
||||
# 获取所有防抖键
|
||||
# Get all deduplication keys
|
||||
pattern = f"{self.cache_prefix}*"
|
||||
keys = await self.redis.keys(pattern)
|
||||
|
||||
# 统计不同类型的键
|
||||
# Count different types of keys
|
||||
total_keys = len(keys)
|
||||
window_keys = len([k for k in keys if b"window:" in k])
|
||||
event_keys = total_keys - window_keys
|
||||
|
||||
# 获取配置信息
|
||||
# Get config info
|
||||
config = {
|
||||
"enabled": self.settings.deduplication.enabled,
|
||||
"window_seconds": self.settings.deduplication.window_seconds,
|
||||
@ -139,14 +136,14 @@ class DeduplicationService:
|
||||
return {"error": str(e)}
|
||||
|
||||
async def cleanup_expired_events(self) -> int:
|
||||
"""清理过期事件"""
|
||||
"""Clean up expired events"""
|
||||
try:
|
||||
pattern = f"{self.cache_prefix}*"
|
||||
keys = await self.redis.keys(pattern)
|
||||
|
||||
cleaned_count = 0
|
||||
for key in keys:
|
||||
# 检查 TTL
|
||||
# Check TTL
|
||||
ttl = await self.redis.ttl(key)
|
||||
if ttl <= 0:
|
||||
await self.redis.delete(key)
|
||||
@ -163,14 +160,12 @@ class DeduplicationService:
|
||||
|
||||
def generate_dedup_key(self, commit_hash: str, branch: str) -> str:
|
||||
"""
|
||||
生成防抖键值
|
||||
|
||||
Generate deduplication key
|
||||
Args:
|
||||
commit_hash: 提交哈希
|
||||
branch: 分支名
|
||||
|
||||
commit_hash: commit hash
|
||||
branch: branch name
|
||||
Returns:
|
||||
str: 防抖键值
|
||||
str: deduplication key
|
||||
"""
|
||||
if self.settings.deduplication.strategy == "commit_branch":
|
||||
return f"{commit_hash}:{branch}"
|
||||
@ -179,18 +174,16 @@ class DeduplicationService:
|
||||
elif self.settings.deduplication.strategy == "branch_only":
|
||||
return branch
|
||||
else:
|
||||
# 默认使用 commit_hash:branch
|
||||
# Default use commit_hash:branch
|
||||
return f"{commit_hash}:{branch}"
|
||||
|
||||
async def is_in_window(self, dedup_key: str) -> bool:
|
||||
"""
|
||||
检查是否在防抖时间窗口内
|
||||
|
||||
Check if in deduplication time window
|
||||
Args:
|
||||
dedup_key: 防抖键值
|
||||
|
||||
dedup_key: deduplication key
|
||||
Returns:
|
||||
bool: True 表示在窗口内
|
||||
bool: True if in window
|
||||
"""
|
||||
try:
|
||||
window_key = f"{self.cache_prefix}window:{dedup_key}"
|
||||
@ -202,22 +195,19 @@ class DeduplicationService:
|
||||
dedup_key=dedup_key, error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# 全局防抖服务实例
|
||||
# Global deduplication service instance
|
||||
_dedup_service: Optional[DeduplicationService] = None
|
||||
|
||||
|
||||
def get_deduplication_service() -> DeduplicationService:
|
||||
"""获取防抖服务实例"""
|
||||
"""Get deduplication service instance"""
|
||||
global _dedup_service
|
||||
if _dedup_service is None:
|
||||
# 这里需要从依赖注入获取 Redis 客户端
|
||||
# 在实际使用时,应该通过依赖注入传入
|
||||
# Should get Redis client from dependency injection
|
||||
# In actual use, should be passed in
|
||||
raise RuntimeError("DeduplicationService not initialized")
|
||||
return _dedup_service
|
||||
|
||||
|
||||
def set_deduplication_service(service: DeduplicationService):
|
||||
"""设置防抖服务实例"""
|
||||
"""Set deduplication service instance"""
|
||||
global _dedup_service
|
||||
_dedup_service = service
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
Jenkins 服务
|
||||
提供与 Jenkins 的交互功能
|
||||
Jenkins service
|
||||
Provides interaction with Jenkins
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
@ -12,7 +12,7 @@ from app.config import get_settings
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class JenkinsService:
|
||||
"""Jenkins 服务类"""
|
||||
"""Jenkins service class"""
|
||||
|
||||
def __init__(self):
|
||||
self.settings = get_settings()
|
||||
@ -22,7 +22,7 @@ class JenkinsService:
|
||||
self.timeout = self.settings.jenkins.timeout
|
||||
|
||||
async def test_connection(self) -> bool:
|
||||
"""测试 Jenkins 连接"""
|
||||
"""Test Jenkins connection"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
auth = aiohttp.BasicAuth(self.username, self.token)
|
||||
@ -42,15 +42,15 @@ class JenkinsService:
|
||||
return False
|
||||
|
||||
async def trigger_job(self, job_name: str, parameters: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""触发 Jenkins 任务"""
|
||||
"""Trigger Jenkins job"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
auth = aiohttp.BasicAuth(self.username, self.token)
|
||||
|
||||
# 构建请求 URL
|
||||
# Build request URL
|
||||
url = f"{self.base_url}/job/{job_name}/build"
|
||||
|
||||
# 如果有参数,使用参数化构建
|
||||
# If parameters, use parameterized build
|
||||
if parameters:
|
||||
url = f"{self.base_url}/job/{job_name}/buildWithParameters"
|
||||
|
||||
@ -71,7 +71,7 @@ class JenkinsService:
|
||||
return False
|
||||
|
||||
async def get_job_info(self, job_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取任务信息"""
|
||||
"""Get job info"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
auth = aiohttp.BasicAuth(self.username, self.token)
|
||||
@ -89,11 +89,11 @@ class JenkinsService:
|
||||
logger.error(f"Error getting job info for {job_name}: {str(e)}")
|
||||
return None
|
||||
|
||||
# 全局服务实例
|
||||
# Global service instance
|
||||
_jenkins_service = None
|
||||
|
||||
def get_jenkins_service() -> JenkinsService:
|
||||
"""获取 Jenkins 服务实例"""
|
||||
"""Get Jenkins service instance"""
|
||||
global _jenkins_service
|
||||
if _jenkins_service is None:
|
||||
_jenkins_service = JenkinsService()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
队列服务
|
||||
提供任务队列管理功能
|
||||
Queue service
|
||||
Provides task queue management features
|
||||
"""
|
||||
|
||||
import structlog
|
||||
@ -10,7 +10,7 @@ from datetime import datetime
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class QueueService:
|
||||
"""队列服务类"""
|
||||
"""Queue service class"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_workers = 0
|
||||
@ -25,45 +25,45 @@ class QueueService:
|
||||
}
|
||||
|
||||
async def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取队列统计信息"""
|
||||
"""Get queue statistics"""
|
||||
return self._stats.copy()
|
||||
|
||||
async def increment_processed(self):
|
||||
"""增加已处理任务计数"""
|
||||
"""Increase processed task count"""
|
||||
self.total_processed += 1
|
||||
self._stats["total_processed"] = self.total_processed
|
||||
|
||||
async def increment_failed(self):
|
||||
"""增加失败任务计数"""
|
||||
"""Increase failed task count"""
|
||||
self.total_failed += 1
|
||||
self._stats["total_failed"] = self.total_failed
|
||||
|
||||
async def set_active_workers(self, count: int):
|
||||
"""设置活跃工作线程数"""
|
||||
"""Set number of active workers"""
|
||||
self.active_workers = count
|
||||
self._stats["active_workers"] = count
|
||||
|
||||
async def set_queue_size(self, size: int):
|
||||
"""设置队列大小"""
|
||||
"""Set queue size"""
|
||||
self.queue_size = size
|
||||
self._stats["queue_size"] = size
|
||||
|
||||
async def add_to_queue(self):
|
||||
"""添加任务到队列"""
|
||||
"""Add task to queue"""
|
||||
self.queue_size += 1
|
||||
self._stats["queue_size"] = self.queue_size
|
||||
|
||||
async def remove_from_queue(self):
|
||||
"""从队列移除任务"""
|
||||
"""Remove task from queue"""
|
||||
if self.queue_size > 0:
|
||||
self.queue_size -= 1
|
||||
self._stats["queue_size"] = self.queue_size
|
||||
|
||||
# 全局服务实例
|
||||
# Global service instance
|
||||
_queue_service = None
|
||||
|
||||
def get_queue_service() -> QueueService:
|
||||
"""获取队列服务实例"""
|
||||
"""Get queue service instance"""
|
||||
global _queue_service
|
||||
if _queue_service is None:
|
||||
_queue_service = QueueService()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
Webhook 处理服务
|
||||
实现智能分发、任务排队和防抖策略
|
||||
Webhook processing service
|
||||
Implements intelligent dispatch, task queueing, and deduplication strategy
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -18,9 +18,8 @@ from app.tasks.jenkins_tasks import trigger_jenkins_job
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class WebhookService:
|
||||
"""Webhook 处理服务"""
|
||||
"""Webhook processing service"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -36,16 +35,14 @@ class WebhookService:
|
||||
|
||||
async def process_webhook(self, webhook: GiteaWebhook) -> WebhookResponse:
|
||||
"""
|
||||
处理 Webhook 事件
|
||||
|
||||
Process webhook event
|
||||
Args:
|
||||
webhook: Gitea Webhook 数据
|
||||
|
||||
webhook: Gitea webhook data
|
||||
Returns:
|
||||
WebhookResponse: 处理结果
|
||||
WebhookResponse: processing result
|
||||
"""
|
||||
try:
|
||||
# 1. 验证事件类型
|
||||
# 1. Validate event type
|
||||
if not webhook.is_push_event():
|
||||
return WebhookResponse(
|
||||
success=True,
|
||||
@ -53,7 +50,7 @@ class WebhookService:
|
||||
event_id=webhook.get_event_id()
|
||||
)
|
||||
|
||||
# 2. 提取关键信息
|
||||
# 2. Extract key information
|
||||
branch = webhook.get_branch_name()
|
||||
commit_hash = webhook.get_commit_hash()
|
||||
repository = webhook.repository.full_name
|
||||
@ -63,7 +60,7 @@ class WebhookService:
|
||||
branch=branch,
|
||||
commit_hash=commit_hash)
|
||||
|
||||
# 3. 防抖检查
|
||||
# 3. Deduplication check
|
||||
dedup_key = self.dedup_service.generate_dedup_key(commit_hash, branch)
|
||||
if await self.dedup_service.is_duplicate(dedup_key):
|
||||
return WebhookResponse(
|
||||
@ -72,7 +69,7 @@ class WebhookService:
|
||||
event_id=webhook.get_event_id()
|
||||
)
|
||||
|
||||
# 4. 获取项目映射和任务名
|
||||
# 4. Get project mapping and job name
|
||||
job_name = await self._determine_job_name(repository, branch)
|
||||
if not job_name:
|
||||
return WebhookResponse(
|
||||
@ -81,10 +78,10 @@ class WebhookService:
|
||||
event_id=webhook.get_event_id()
|
||||
)
|
||||
|
||||
# 5. 准备任务参数
|
||||
# 5. Prepare job parameters
|
||||
job_params = self._prepare_job_parameters(webhook, job_name)
|
||||
|
||||
# 6. 提交任务到队列
|
||||
# 6. Submit job to queue
|
||||
task_result = await self._submit_job_to_queue(
|
||||
webhook, job_name, job_params
|
||||
)
|
||||
@ -114,13 +111,13 @@ class WebhookService:
|
||||
)
|
||||
|
||||
async def _determine_job_name(self, repository: str, branch: str) -> Optional[str]:
|
||||
"""根据仓库和分支确定任务名"""
|
||||
# 首先尝试从数据库获取项目映射
|
||||
"""Determine job name by repository and branch"""
|
||||
# First try to get project mapping from database
|
||||
job_name = await self.db_service.determine_job_name(repository, branch)
|
||||
if job_name:
|
||||
return job_name
|
||||
|
||||
# 如果数据库中没有映射,使用配置文件中的环境分发
|
||||
# If not found in database, use environment dispatch from config
|
||||
environment = self.settings.get_environment_for_branch(branch)
|
||||
if environment:
|
||||
return environment.jenkins_job
|
||||
@ -128,7 +125,7 @@ class WebhookService:
|
||||
return None
|
||||
|
||||
def _prepare_job_parameters(self, webhook: GiteaWebhook, job_name: str) -> Dict[str, str]:
|
||||
"""准备 Jenkins 任务参数"""
|
||||
"""Prepare Jenkins job parameters"""
|
||||
author_info = webhook.get_author_info()
|
||||
|
||||
return {
|
||||
@ -151,9 +148,9 @@ class WebhookService:
|
||||
job_name: str,
|
||||
job_params: Dict[str, str]
|
||||
) -> bool:
|
||||
"""提交任务到 Celery 队列"""
|
||||
"""Submit job to Celery queue"""
|
||||
try:
|
||||
# 创建任务
|
||||
# Create task
|
||||
task_kwargs = {
|
||||
"job_name": job_name,
|
||||
"jenkins_url": self.settings.jenkins.url,
|
||||
@ -162,14 +159,14 @@ class WebhookService:
|
||||
"repository": webhook.repository.full_name,
|
||||
"branch": webhook.get_branch_name(),
|
||||
"commit_hash": webhook.get_commit_hash(),
|
||||
"priority": 1 # 默认优先级
|
||||
"priority": 1 # Default priority
|
||||
}
|
||||
|
||||
# 提交到 Celery 队列
|
||||
# Submit to Celery queue
|
||||
task = self.celery_app.send_task(
|
||||
"app.tasks.jenkins_tasks.trigger_jenkins_job",
|
||||
kwargs=task_kwargs,
|
||||
priority=environment.priority
|
||||
priority=task_kwargs["priority"]
|
||||
)
|
||||
|
||||
logger.info("Job submitted to queue",
|
||||
@ -187,15 +184,15 @@ class WebhookService:
|
||||
return False
|
||||
|
||||
async def get_webhook_stats(self) -> Dict[str, Any]:
|
||||
"""获取 Webhook 处理统计"""
|
||||
"""Get webhook processing statistics"""
|
||||
try:
|
||||
# 获取队列统计
|
||||
# Get queue stats
|
||||
queue_stats = await self._get_queue_stats()
|
||||
|
||||
# 获取防抖统计
|
||||
# Get deduplication stats
|
||||
dedup_stats = await self.dedup_service.get_stats()
|
||||
|
||||
# 获取环境配置
|
||||
# Get environment config
|
||||
environments = {}
|
||||
for name, config in self.settings.environments.items():
|
||||
environments[name] = {
|
||||
@ -222,20 +219,20 @@ class WebhookService:
|
||||
return {"error": str(e)}
|
||||
|
||||
async def _get_queue_stats(self) -> Dict[str, Any]:
|
||||
"""获取队列统计信息"""
|
||||
"""Get queue statistics"""
|
||||
try:
|
||||
# 获取 Celery 队列统计
|
||||
# Get Celery queue stats
|
||||
inspect = self.celery_app.control.inspect()
|
||||
|
||||
# 活跃任务
|
||||
# Active tasks
|
||||
active = inspect.active()
|
||||
active_count = sum(len(tasks) for tasks in active.values()) if active else 0
|
||||
|
||||
# 等待任务
|
||||
# Reserved tasks
|
||||
reserved = inspect.reserved()
|
||||
reserved_count = sum(len(tasks) for tasks in reserved.values()) if reserved else 0
|
||||
|
||||
# 注册的 worker
|
||||
# Registered workers
|
||||
registered = inspect.registered()
|
||||
worker_count = len(registered) if registered else 0
|
||||
|
||||
@ -251,9 +248,9 @@ class WebhookService:
|
||||
return {"error": str(e)}
|
||||
|
||||
async def clear_queue(self) -> Dict[str, Any]:
|
||||
"""清空队列"""
|
||||
"""Clear queue"""
|
||||
try:
|
||||
# 撤销所有活跃任务
|
||||
# Revoke all active tasks
|
||||
inspect = self.celery_app.control.inspect()
|
||||
active = inspect.active()
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""
|
||||
Jenkins 任务处理
|
||||
使用 Celery 处理异步 Jenkins 任务触发
|
||||
Jenkins task processing
|
||||
Asynchronous Jenkins job triggering using Celery
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -17,7 +17,7 @@ from app.services.jenkins_service import JenkinsService
|
||||
logger = structlog.get_logger()
|
||||
settings = get_settings()
|
||||
|
||||
# 创建 Celery 应用
|
||||
# Create Celery app
|
||||
celery_app = Celery(
|
||||
"gitea_webhook_ambassador",
|
||||
broker=settings.redis.url,
|
||||
@ -25,7 +25,7 @@ celery_app = Celery(
|
||||
include=["app.tasks.jenkins_tasks"]
|
||||
)
|
||||
|
||||
# Celery 配置
|
||||
# Celery configuration
|
||||
celery_app.conf.update(
|
||||
task_serializer="json",
|
||||
accept_content=["json"],
|
||||
@ -33,20 +33,20 @@ celery_app.conf.update(
|
||||
timezone="UTC",
|
||||
enable_utc=True,
|
||||
task_track_started=True,
|
||||
task_time_limit=300, # 5分钟超时
|
||||
task_soft_time_limit=240, # 4分钟软超时
|
||||
task_time_limit=300, # 5 minutes timeout
|
||||
task_soft_time_limit=240, # 4 minutes soft timeout
|
||||
worker_prefetch_multiplier=1,
|
||||
worker_max_tasks_per_child=1000,
|
||||
worker_max_memory_per_child=200000, # 200MB
|
||||
task_acks_late=True,
|
||||
task_reject_on_worker_lost=True,
|
||||
task_always_eager=False, # 生产环境设为 False
|
||||
result_expires=3600, # 结果缓存1小时
|
||||
task_always_eager=False, # Set to False in production
|
||||
result_expires=3600, # Result cache 1 hour
|
||||
)
|
||||
|
||||
|
||||
class JenkinsTask(Task):
|
||||
"""Jenkins 任务基类"""
|
||||
"""Jenkins task base class"""
|
||||
|
||||
abstract = True
|
||||
|
||||
@ -59,7 +59,7 @@ class JenkinsTask(Task):
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
"""任务失败回调"""
|
||||
"""Task failure callback"""
|
||||
logger.error("Task failed",
|
||||
task_id=task_id,
|
||||
task_name=self.name,
|
||||
@ -68,7 +68,7 @@ class JenkinsTask(Task):
|
||||
kwargs=kwargs)
|
||||
|
||||
def on_retry(self, exc, task_id, args, kwargs, einfo):
|
||||
"""任务重试回调"""
|
||||
"""Task retry callback"""
|
||||
logger.warning("Task retrying",
|
||||
task_id=task_id,
|
||||
task_name=self.name,
|
||||
@ -76,7 +76,7 @@ class JenkinsTask(Task):
|
||||
retry_count=self.request.retries)
|
||||
|
||||
def on_success(self, retval, task_id, args, kwargs):
|
||||
"""任务成功回调"""
|
||||
"""Task success callback"""
|
||||
logger.info("Task completed successfully",
|
||||
task_id=task_id,
|
||||
task_name=self.name,
|
||||
@ -104,20 +104,18 @@ def trigger_jenkins_job(
|
||||
priority: int = 1
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
触发 Jenkins 任务
|
||||
|
||||
Trigger Jenkins job
|
||||
Args:
|
||||
job_name: Jenkins 任务名
|
||||
job_name: Jenkins job name
|
||||
jenkins_url: Jenkins URL
|
||||
parameters: 任务参数
|
||||
event_id: 事件 ID
|
||||
repository: 仓库名
|
||||
branch: 分支名
|
||||
commit_hash: 提交哈希
|
||||
priority: 优先级
|
||||
|
||||
parameters: job parameters
|
||||
event_id: event ID
|
||||
repository: repository name
|
||||
branch: branch name
|
||||
commit_hash: commit hash
|
||||
priority: priority
|
||||
Returns:
|
||||
Dict: 任务执行结果
|
||||
Dict: job execution result
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
@ -131,10 +129,10 @@ def trigger_jenkins_job(
|
||||
commit_hash=commit_hash,
|
||||
priority=priority)
|
||||
|
||||
# 创建 Jenkins 服务实例
|
||||
# Create Jenkins service instance
|
||||
jenkins_service = JenkinsService()
|
||||
|
||||
# 触发 Jenkins 任务
|
||||
# Trigger Jenkins job
|
||||
result = asyncio.run(jenkins_service.trigger_job(
|
||||
job_name=job_name,
|
||||
jenkins_url=jenkins_url,
|
||||
@ -171,7 +169,7 @@ def trigger_jenkins_job(
|
||||
error=result.get("error"),
|
||||
execution_time=execution_time)
|
||||
|
||||
# 重试任务
|
||||
# Retry task
|
||||
raise self.retry(
|
||||
countdown=settings.queue.retry_delay * (2 ** self.request.retries),
|
||||
max_retries=settings.queue.max_retries
|
||||
@ -185,7 +183,7 @@ def trigger_jenkins_job(
|
||||
error=str(e),
|
||||
execution_time=execution_time)
|
||||
|
||||
# 重试任务
|
||||
# Retry task
|
||||
raise self.retry(
|
||||
countdown=settings.queue.retry_delay * (2 ** self.request.retries),
|
||||
max_retries=settings.queue.max_retries
|
||||
@ -203,13 +201,11 @@ def check_jenkins_health(
|
||||
jenkins_url: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
检查 Jenkins 健康状态
|
||||
|
||||
Check Jenkins health status
|
||||
Args:
|
||||
jenkins_url: Jenkins URL
|
||||
|
||||
Returns:
|
||||
Dict: 健康检查结果
|
||||
Dict: health check result
|
||||
"""
|
||||
try:
|
||||
logger.info("Checking Jenkins health", jenkins_url=jenkins_url)
|
||||
@ -244,23 +240,22 @@ def check_jenkins_health(
|
||||
)
|
||||
def cleanup_expired_tasks(self) -> Dict[str, Any]:
|
||||
"""
|
||||
清理过期任务
|
||||
|
||||
Clean up expired tasks
|
||||
Returns:
|
||||
Dict: 清理结果
|
||||
Dict: cleanup result
|
||||
"""
|
||||
try:
|
||||
logger.info("Starting task cleanup")
|
||||
|
||||
# 获取所有任务
|
||||
# Get all tasks
|
||||
inspect = self.app.control.inspect()
|
||||
|
||||
# 清理过期的结果
|
||||
# Clean up expired results
|
||||
cleaned_count = 0
|
||||
current_time = time.time()
|
||||
|
||||
# 这里可以添加更复杂的清理逻辑
|
||||
# 比如清理超过一定时间的任务结果
|
||||
# Add more complex cleanup logic here if needed
|
||||
# For example, clean up results older than a certain time
|
||||
|
||||
logger.info("Task cleanup completed", cleaned_count=cleaned_count)
|
||||
|
||||
@ -280,27 +275,27 @@ def cleanup_expired_tasks(self) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
# 定时任务
|
||||
# Periodic tasks
|
||||
@celery_app.on_after_configure.connect
|
||||
def setup_periodic_tasks(sender, **kwargs):
|
||||
"""设置定时任务"""
|
||||
"""Set up periodic tasks"""
|
||||
|
||||
# 每小时清理过期任务
|
||||
# Clean up expired tasks every hour
|
||||
sender.add_periodic_task(
|
||||
3600.0, # 1小时
|
||||
3600.0, # 1 hour
|
||||
cleanup_expired_tasks.s(),
|
||||
name="cleanup-expired-tasks"
|
||||
)
|
||||
|
||||
# 每5分钟检查 Jenkins 健康状态
|
||||
# Check Jenkins health every 5 minutes
|
||||
for env_name, env_config in settings.environments.items():
|
||||
sender.add_periodic_task(
|
||||
300.0, # 5分钟
|
||||
300.0, # 5 minutes
|
||||
check_jenkins_health.s(env_config.jenkins_url),
|
||||
name=f"check-jenkins-health-{env_name}"
|
||||
)
|
||||
|
||||
|
||||
def get_celery_app() -> Celery:
|
||||
"""获取 Celery 应用实例"""
|
||||
"""Get Celery app instance"""
|
||||
return celery_app
|
||||
@ -1,104 +1,104 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Gitea Webhook Ambassador - 版本检查脚本
|
||||
# 用于区分 Go 版本和 Python 版本
|
||||
# Gitea Webhook Ambassador - Version Check Script
|
||||
# Used to distinguish between Go and Python versions
|
||||
|
||||
echo "🔍 检查 Gitea Webhook Ambassador 版本..."
|
||||
echo "🔍 Checking Gitea Webhook Ambassador version..."
|
||||
|
||||
# 检查端口 8000 (Python 版本默认端口)
|
||||
echo "📡 检查端口 8000 (Python 版本)..."
|
||||
# Check port 8000 (Python version default port)
|
||||
echo "📡 Checking port 8000 (Python version)..."
|
||||
if lsof -i :8000 > /dev/null 2>&1; then
|
||||
PID=$(lsof -ti :8000)
|
||||
PROCESS=$(ps -p $PID -o comm= 2>/dev/null)
|
||||
echo "✅ 端口 8000 被占用 (PID: $PID, 进程: $PROCESS)"
|
||||
echo "✅ Port 8000 is occupied (PID: $PID, process: $PROCESS)"
|
||||
|
||||
# 检查是否是 Python 进程
|
||||
# Check if it is a Python process
|
||||
if echo "$PROCESS" | grep -q "python\|uvicorn"; then
|
||||
echo "🐍 检测到 Python 版本正在运行"
|
||||
echo "🐍 Detected Python version is running"
|
||||
|
||||
# 尝试访问 Python 版本的 API
|
||||
# Try to access Python version API
|
||||
if curl -s http://localhost:8000/api/health > /dev/null 2>&1; then
|
||||
echo "✅ Python 版本 API 响应正常"
|
||||
echo "🌐 访问地址: http://localhost:8000"
|
||||
echo "📊 仪表板: http://localhost:8000/dashboard"
|
||||
echo "✅ Python version API is responsive"
|
||||
echo "🌐 Access: http://localhost:8000"
|
||||
echo "📊 Dashboard: http://localhost:8000/dashboard"
|
||||
else
|
||||
echo "⚠️ Python 版本进程存在但 API 无响应"
|
||||
echo "⚠️ Python process exists but API is not responsive"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ 端口 8000 被其他进程占用"
|
||||
echo "⚠️ Port 8000 is occupied by another process"
|
||||
fi
|
||||
else
|
||||
echo "❌ 端口 8000 未被占用 (Python 版本未运行)"
|
||||
echo "❌ Port 8000 is not occupied (Python version not running)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# 检查端口 8080 (Go 版本默认端口)
|
||||
echo "📡 检查端口 8080 (Go 版本)..."
|
||||
# Check port 8080 (Go version default port)
|
||||
echo "📡 Checking port 8080 (Go version)..."
|
||||
if lsof -i :8080 > /dev/null 2>&1; then
|
||||
PID=$(lsof -ti :8080)
|
||||
PROCESS=$(ps -p $PID -o comm= 2>/dev/null)
|
||||
echo "✅ 端口 8080 被占用 (PID: $PID, 进程: $PROCESS)"
|
||||
echo "✅ Port 8080 is occupied (PID: $PID, process: $PROCESS)"
|
||||
|
||||
# 检查是否是 Go 进程
|
||||
# Check if it is a Go process
|
||||
if echo "$PROCESS" | grep -q "gitea-webhook-ambassador"; then
|
||||
echo "🚀 检测到 Go 版本正在运行"
|
||||
echo "🚀 Detected Go version is running"
|
||||
|
||||
# 尝试访问 Go 版本的 API
|
||||
# Try to access Go version API
|
||||
if curl -s http://localhost:8080/health > /dev/null 2>&1; then
|
||||
echo "✅ Go 版本 API 响应正常"
|
||||
echo "🌐 访问地址: http://localhost:8080"
|
||||
echo "✅ Go version API is responsive"
|
||||
echo "🌐 Access: http://localhost:8080"
|
||||
else
|
||||
echo "⚠️ Go 版本进程存在但 API 无响应"
|
||||
echo "⚠️ Go process exists but API is not responsive"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ 端口 8080 被其他进程占用"
|
||||
echo "⚠️ Port 8080 is occupied by another process"
|
||||
fi
|
||||
else
|
||||
echo "❌ 端口 8080 未被占用 (Go 版本未运行)"
|
||||
echo "❌ Port 8080 is not occupied (Go version not running)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# 检查 PID 文件
|
||||
echo "📁 检查 PID 文件..."
|
||||
# Check PID file
|
||||
echo "📁 Checking PID file..."
|
||||
|
||||
# Python 版本 PID 文件
|
||||
# Python version PID file
|
||||
PYTHON_PID_FILE="/home/nicolas/freeleaps-ops/apps/gitea-webhook-ambassador-python/service.pid"
|
||||
if [ -f "$PYTHON_PID_FILE" ]; then
|
||||
PYTHON_PID=$(cat "$PYTHON_PID_FILE")
|
||||
if ps -p $PYTHON_PID > /dev/null 2>&1; then
|
||||
echo "✅ Python 版本 PID 文件存在 (PID: $PYTHON_PID)"
|
||||
echo "✅ Python version PID file exists (PID: $PYTHON_PID)"
|
||||
else
|
||||
echo "⚠️ Python 版本 PID 文件存在但进程不存在"
|
||||
echo "⚠️ Python version PID file exists but process does not exist"
|
||||
fi
|
||||
else
|
||||
echo "❌ Python 版本 PID 文件不存在"
|
||||
echo "❌ Python version PID file does not exist"
|
||||
fi
|
||||
|
||||
# Go 版本 PID 文件 (如果存在)
|
||||
# Go version PID file (if exists)
|
||||
GO_PID_FILE="/home/nicolas/freeleaps-ops/apps/gitea-webhook-ambassador/service.pid"
|
||||
if [ -f "$GO_PID_FILE" ]; then
|
||||
GO_PID=$(cat "$GO_PID_FILE")
|
||||
if ps -p $GO_PID > /dev/null 2>&1; then
|
||||
echo "✅ Go 版本 PID 文件存在 (PID: $GO_PID)"
|
||||
echo "✅ Go version PID file exists (PID: $GO_PID)"
|
||||
else
|
||||
echo "⚠️ Go 版本 PID 文件存在但进程不存在"
|
||||
echo "⚠️ Go version PID file exists but process does not exist"
|
||||
fi
|
||||
else
|
||||
echo "❌ Go 版本 PID 文件不存在"
|
||||
echo "❌ Go version PID file does not exist"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# 总结
|
||||
echo "📊 总结:"
|
||||
# Summary
|
||||
echo "📊 Summary:"
|
||||
echo "----------------------------------------"
|
||||
|
||||
PYTHON_RUNNING=false
|
||||
GO_RUNNING=false
|
||||
|
||||
# 检查 Python 版本
|
||||
# Check Python version
|
||||
if lsof -i :8000 > /dev/null 2>&1; then
|
||||
PID=$(lsof -ti :8000)
|
||||
PROCESS=$(ps -p $PID -o comm= 2>/dev/null)
|
||||
@ -107,7 +107,7 @@ if lsof -i :8000 > /dev/null 2>&1; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查 Go 版本
|
||||
# Check Go version
|
||||
if lsof -i :8080 > /dev/null 2>&1; then
|
||||
PID=$(lsof -ti :8080)
|
||||
PROCESS=$(ps -p $PID -o comm= 2>/dev/null)
|
||||
@ -117,17 +117,17 @@ if lsof -i :8080 > /dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
if [ "$PYTHON_RUNNING" = true ] && [ "$GO_RUNNING" = true ]; then
|
||||
echo "⚠️ 两个版本都在运行!"
|
||||
echo "🐍 Python 版本: http://localhost:8000"
|
||||
echo "🚀 Go 版本: http://localhost:8080"
|
||||
echo "⚠️ Both versions are running!"
|
||||
echo "🐍 Python version: http://localhost:8000"
|
||||
echo "🚀 Go version: http://localhost:8080"
|
||||
elif [ "$PYTHON_RUNNING" = true ]; then
|
||||
echo "✅ 当前运行: Python 版本"
|
||||
echo "🌐 访问地址: http://localhost:8000"
|
||||
echo "✅ Currently running: Python version"
|
||||
echo "🌐 Access: http://localhost:8000"
|
||||
elif [ "$GO_RUNNING" = true ]; then
|
||||
echo "✅ 当前运行: Go 版本"
|
||||
echo "🌐 访问地址: http://localhost:8080"
|
||||
echo "✅ Currently running: Go version"
|
||||
echo "🌐 Access: http://localhost:8080"
|
||||
else
|
||||
echo "❌ 没有检测到任何版本在运行"
|
||||
echo "❌ No version is running"
|
||||
fi
|
||||
|
||||
echo "----------------------------------------"
|
||||
@ -1,4 +1,4 @@
|
||||
# 环境分发配置
|
||||
# Environment dispatch configuration
|
||||
environments:
|
||||
dev:
|
||||
branches: ["dev", "develop", "development", "feature/*"]
|
||||
@ -24,16 +24,16 @@ environments:
|
||||
jenkins_url: "https://jenkins-default.freeleaps.com"
|
||||
priority: 4
|
||||
|
||||
# 防抖配置
|
||||
# Deduplication configuration
|
||||
deduplication:
|
||||
enabled: true
|
||||
window_seconds: 300 # 5分钟防抖窗口
|
||||
window_seconds: 300 # 5-minute deduplication window
|
||||
strategy: "commit_branch" # commit_hash + branch
|
||||
cache_ttl: 3600 # 缓存1小时
|
||||
cache_ttl: 3600 # Cache for 1 hour
|
||||
|
||||
# 队列配置
|
||||
# Queue configuration
|
||||
queue:
|
||||
max_concurrent: 10
|
||||
max_retries: 3
|
||||
retry_delay: 60 # 秒
|
||||
retry_delay: 60 # seconds
|
||||
priority_levels: 4
|
||||
@ -1,7 +1,7 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Redis 服务
|
||||
# Redis service
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: webhook-ambassador-redis
|
||||
@ -16,7 +16,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# PostgreSQL 数据库 (可选,用于生产环境)
|
||||
# PostgreSQL database (optional, for production)
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: webhook-ambassador-postgres
|
||||
@ -34,7 +34,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Webhook Ambassador API 服务
|
||||
# Webhook Ambassador API service
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
@ -88,7 +88,7 @@ services:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# Celery Beat (定时任务调度器)
|
||||
# Celery Beat (scheduler)
|
||||
beat:
|
||||
build:
|
||||
context: .
|
||||
@ -112,7 +112,7 @@ services:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# Flower (Celery 监控)
|
||||
# Flower (Celery monitoring)
|
||||
flower:
|
||||
build:
|
||||
context: .
|
||||
@ -135,7 +135,7 @@ services:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# Prometheus (监控)
|
||||
# Prometheus (monitoring)
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: webhook-ambassador-prometheus
|
||||
@ -153,7 +153,7 @@ services:
|
||||
- '--web.enable-lifecycle'
|
||||
restart: unless-stopped
|
||||
|
||||
# Grafana (监控面板)
|
||||
# Grafana (monitoring dashboard)
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: webhook-ambassador-grafana
|
||||
|
||||
@ -1,41 +1,41 @@
|
||||
# 应用配置
|
||||
# Application configuration
|
||||
APP_NAME=Gitea Webhook Ambassador
|
||||
DEBUG=false
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
# 数据库配置
|
||||
# Database configuration
|
||||
DATABASE_URL=sqlite:///./webhook_ambassador.db
|
||||
# 生产环境使用 PostgreSQL:
|
||||
# For production, use PostgreSQL:
|
||||
# DATABASE_URL=postgresql://webhook_user:webhook_password@localhost:5432/webhook_ambassador
|
||||
|
||||
# Redis 配置
|
||||
# Redis configuration
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# Jenkins 配置
|
||||
# Jenkins configuration
|
||||
JENKINS_USERNAME=your_jenkins_username
|
||||
JENKINS_TOKEN=115127e693f1bc6b7194f58ff6d6283bd0
|
||||
JENKINS_TIMEOUT=30
|
||||
|
||||
# 安全配置
|
||||
# Security configuration
|
||||
SECURITY_SECRET_KEY=r6Y@QTb*7BQN@hDGsN
|
||||
SECURITY_WEBHOOK_SECRET_HEADER=X-Gitea-Signature
|
||||
SECURITY_RATE_LIMIT_PER_MINUTE=100
|
||||
|
||||
# 日志配置
|
||||
# Logging configuration
|
||||
LOGGING_LEVEL=INFO
|
||||
LOGGING_FORMAT=json
|
||||
LOGGING_FILE=
|
||||
|
||||
# 队列配置
|
||||
# Queue configuration
|
||||
QUEUE_MAX_CONCURRENT=10
|
||||
QUEUE_MAX_RETRIES=3
|
||||
QUEUE_RETRY_DELAY=60
|
||||
QUEUE_PRIORITY_LEVELS=3
|
||||
|
||||
# 防抖配置
|
||||
# Deduplication configuration
|
||||
DEDUPLICATION_ENABLED=true
|
||||
DEDUPLICATION_WINDOW_SECONDS=300
|
||||
DEDUPLICATION_STRATEGY=commit_branch
|
||||
|
||||
@ -1,41 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 修复 PID 文件问题
|
||||
echo "🔧 修复 PID 文件问题..."
|
||||
# Fix PID file issue
|
||||
echo "🔧 Fixing PID file issue..."
|
||||
|
||||
# 查找 Python 服务进程
|
||||
# Find Python service process
|
||||
PID=$(lsof -ti :8000 2>/dev/null)
|
||||
|
||||
if [ -n "$PID" ]; then
|
||||
echo "✅ 找到运行中的 Python 服务 (PID: $PID)"
|
||||
echo "✅ Found running Python service (PID: $PID)"
|
||||
|
||||
# 检查进程是否是我们的服务
|
||||
# Check if the process is our service
|
||||
PROCESS=$(ps -p $PID -o comm= 2>/dev/null)
|
||||
if echo "$PROCESS" | grep -q "python"; then
|
||||
echo "🐍 确认是 Python 版本的 Gitea Webhook Ambassador"
|
||||
echo "🐍 Confirmed: Python version of Gitea Webhook Ambassador"
|
||||
|
||||
# 创建 PID 文件
|
||||
# Create PID file
|
||||
echo $PID > service.pid
|
||||
echo "✅ 已创建 PID 文件: service.pid"
|
||||
echo "✅ PID file created: service.pid"
|
||||
|
||||
# 验证 PID 文件
|
||||
# Verify PID file
|
||||
if [ -f "service.pid" ]; then
|
||||
STORED_PID=$(cat service.pid)
|
||||
echo "📝 PID 文件内容: $STORED_PID"
|
||||
echo "📝 PID file content: $STORED_PID"
|
||||
|
||||
if [ "$STORED_PID" = "$PID" ]; then
|
||||
echo "✅ PID 文件修复成功"
|
||||
echo "💡 现在可以使用 './devbox stop' 来停止服务"
|
||||
echo "✅ PID file fixed successfully"
|
||||
echo "💡 Now you can use './devbox stop' to stop the service"
|
||||
else
|
||||
echo "❌ PID 文件内容不匹配"
|
||||
echo "❌ PID file content does not match"
|
||||
fi
|
||||
else
|
||||
echo "❌ 无法创建 PID 文件"
|
||||
echo "❌ Failed to create PID file"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ 端口 8000 被其他进程占用"
|
||||
echo "⚠️ Port 8000 is occupied by another process"
|
||||
fi
|
||||
else
|
||||
echo "❌ 没有找到运行中的 Python 服务"
|
||||
echo "💡 请先启动服务: './devbox start'"
|
||||
echo "❌ No running Python service found"
|
||||
echo "💡 Please start the service first: './devbox start'"
|
||||
fi
|
||||
@ -1,31 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 快速检查当前运行的版本
|
||||
# Quick check of the currently running version
|
||||
|
||||
echo "🔍 快速检查 Gitea Webhook Ambassador 版本..."
|
||||
echo "🔍 Quick check of Gitea Webhook Ambassador version..."
|
||||
|
||||
# 检查 Python 版本 (端口 8000)
|
||||
# Check Python version (port 8000)
|
||||
if lsof -i :8000 > /dev/null 2>&1; then
|
||||
PID=$(lsof -ti :8000)
|
||||
PROCESS=$(ps -p $PID -o comm= 2>/dev/null)
|
||||
if echo "$PROCESS" | grep -q "python\|uvicorn"; then
|
||||
echo "🐍 Python 版本正在运行 (PID: $PID)"
|
||||
echo "🐍 Python version is running (PID: $PID)"
|
||||
echo "🌐 http://localhost:8000"
|
||||
echo "📊 http://localhost:8000/dashboard"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查 Go 版本 (端口 8080)
|
||||
# Check Go version (port 8080)
|
||||
if lsof -i :8080 > /dev/null 2>&1; then
|
||||
PID=$(lsof -ti :8080)
|
||||
PROCESS=$(ps -p $PID -o comm= 2>/dev/null)
|
||||
if echo "$PROCESS" | grep -q "gitea-webhook-ambassador"; then
|
||||
echo "🚀 Go 版本正在运行 (PID: $PID)"
|
||||
echo "🚀 Go version is running (PID: $PID)"
|
||||
echo "🌐 http://localhost:8080"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "❌ 没有检测到任何版本在运行"
|
||||
echo "💡 使用 './devbox start' 启动 Python 版本"
|
||||
echo "❌ No version is running"
|
||||
echo "💡 Use './devbox start' to start the Python version"
|
||||
@ -1,61 +1,55 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Gitea Webhook Ambassador 快速设置脚本
|
||||
# Gitea Webhook Ambassador quick setup script
|
||||
|
||||
set -e
|
||||
echo "🚀 Starting Gitea Webhook Ambassador setup..."
|
||||
|
||||
echo "🚀 开始设置 Gitea Webhook Ambassador..."
|
||||
|
||||
# 检查 Python 版本
|
||||
python_version=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+')
|
||||
required_version="3.8"
|
||||
|
||||
if [ "$(printf '%s\n' "$required_version" "$python_version" | sort -V | head -n1)" != "$required_version" ]; then
|
||||
echo "❌ 需要 Python 3.8 或更高版本,当前版本: $python_version"
|
||||
# Check Python version
|
||||
python_version=$(python3 -V 2>&1 | awk '{print $2}')
|
||||
if [[ "$python_version" < "3.8" ]]; then
|
||||
echo "❌ Python 3.8 or higher is required, current version: $python_version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Python 版本检查通过: $python_version"
|
||||
echo "✅ Python version check passed: $python_version"
|
||||
|
||||
# 创建虚拟环境
|
||||
# Create virtual environment
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "📦 创建虚拟环境..."
|
||||
echo "📦 Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
echo "🔧 激活虚拟环境..."
|
||||
# Activate virtual environment
|
||||
echo "🔧 Activating virtual environment..."
|
||||
source venv/bin/activate
|
||||
|
||||
# 升级 pip
|
||||
echo "⬆️ 升级 pip..."
|
||||
# Upgrade pip
|
||||
echo "⬆️ Upgrading pip..."
|
||||
pip install --upgrade pip
|
||||
|
||||
# 安装依赖
|
||||
echo "📚 安装依赖..."
|
||||
# Install dependencies
|
||||
echo "📚 Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 创建配置文件
|
||||
# Create config file
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "⚙️ 创建环境配置文件..."
|
||||
echo "⚙️ Creating environment config file..."
|
||||
cp env.example .env
|
||||
echo "📝 请编辑 .env 文件,配置您的 Jenkins 凭据和其他设置"
|
||||
echo "📝 Please edit the .env file to configure your Jenkins credentials and other settings"
|
||||
fi
|
||||
|
||||
# 创建日志目录
|
||||
# Create logs directory
|
||||
mkdir -p logs
|
||||
|
||||
# 创建数据库目录
|
||||
# Create database directory
|
||||
mkdir -p data
|
||||
|
||||
echo "✅ 设置完成!"
|
||||
echo ""
|
||||
echo "📋 下一步操作:"
|
||||
echo "1. 编辑 .env 文件,配置 Jenkins 凭据"
|
||||
echo "2. 运行: source venv/bin/activate"
|
||||
echo "3. 启动 Redis: docker run -d -p 6379:6379 redis:alpine"
|
||||
echo "4. 启动服务: python -m uvicorn app.main:app --reload"
|
||||
echo "5. 启动 Celery worker: celery -A app.tasks.jenkins_tasks worker --loglevel=info"
|
||||
echo ""
|
||||
echo "🌐 访问地址: http://localhost:8000"
|
||||
echo "📊 监控面板: http://localhost:8000/health"
|
||||
echo "✅ Setup complete!"
|
||||
echo "📋 Next steps:"
|
||||
echo "1. Edit the .env file to configure Jenkins credentials"
|
||||
echo "2. Run: source venv/bin/activate"
|
||||
echo "3. Start Redis: docker run -d -p 6379:6379 redis:alpine"
|
||||
echo "4. Start the service: python -m uvicorn app.main:app --reload"
|
||||
echo "5. Start Celery worker: celery -A app.tasks.jenkins_tasks worker --loglevel=info"
|
||||
echo "🌐 Access: http://localhost:8000"
|
||||
echo "📊 Monitoring dashboard: http://localhost:8000/health"
|
||||
@ -1,64 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Gitea Webhook Ambassador 启动脚本
|
||||
# Gitea Webhook Ambassador start script
|
||||
|
||||
set -e
|
||||
|
||||
# 检查虚拟环境
|
||||
# Check virtual environment
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "❌ 虚拟环境不存在,请先运行 ./scripts/setup.sh"
|
||||
echo "❌ Virtual environment does not exist, please run ./scripts/setup.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# 检查环境文件
|
||||
# Check environment file
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "❌ .env 文件不存在,请先运行 ./scripts/setup.sh"
|
||||
echo "❌ .env file does not exist, please run ./scripts/setup.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 Redis 是否运行
|
||||
if ! docker ps | grep -q redis; then
|
||||
echo "🐳 启动 Redis..."
|
||||
docker run -d --name webhook-redis -p 6379:6379 redis:alpine
|
||||
sleep 3
|
||||
# Check if Redis is running
|
||||
if ! pgrep -f redis-server > /dev/null; then
|
||||
echo "🐳 Starting Redis..."
|
||||
docker run -d -p 6379:6379 redis:alpine
|
||||
fi
|
||||
|
||||
echo "🚀 启动 Gitea Webhook Ambassador..."
|
||||
# Start Gitea Webhook Ambassador
|
||||
echo "🚀 Starting Gitea Webhook Ambassador..."
|
||||
|
||||
# 启动 API 服务
|
||||
echo "🌐 启动 API 服务..."
|
||||
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload &
|
||||
# Start API service
|
||||
API_LOG="logs/api.log"
|
||||
echo "🌐 Starting API service..."
|
||||
nohup python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 > $API_LOG 2>&1 &
|
||||
API_PID=$!
|
||||
|
||||
# 等待 API 服务启动
|
||||
sleep 5
|
||||
# Wait for API service to start
|
||||
sleep 3
|
||||
|
||||
# 启动 Celery worker
|
||||
echo "⚙️ 启动 Celery worker..."
|
||||
celery -A app.tasks.jenkins_tasks worker --loglevel=info --concurrency=4 &
|
||||
# Start Celery worker
|
||||
WORKER_LOG="logs/worker.log"
|
||||
echo "⚙️ Starting Celery worker..."
|
||||
nohup celery -A app.tasks.jenkins_tasks worker --loglevel=info > $WORKER_LOG 2>&1 &
|
||||
WORKER_PID=$!
|
||||
|
||||
# 启动 Celery beat (定时任务)
|
||||
echo "⏰ 启动定时任务调度器..."
|
||||
celery -A app.tasks.jenkins_tasks beat --loglevel=info &
|
||||
# Start Celery beat (scheduled tasks)
|
||||
BEAT_LOG="logs/beat.log"
|
||||
echo "⏰ Starting scheduled task scheduler..."
|
||||
nohup celery -A app.tasks.jenkins_tasks beat --loglevel=info > $BEAT_LOG 2>&1 &
|
||||
BEAT_PID=$!
|
||||
|
||||
echo "✅ 所有服务已启动!"
|
||||
echo ""
|
||||
echo "📊 服务状态:"
|
||||
echo "- API 服务: http://localhost:8000 (PID: $API_PID)"
|
||||
echo "- 健康检查: http://localhost:8000/health"
|
||||
echo "- 监控指标: http://localhost:8000/metrics"
|
||||
echo "- Celery Worker: PID $WORKER_PID"
|
||||
echo "- Celery Beat: PID $BEAT_PID"
|
||||
echo ""
|
||||
echo "🛑 按 Ctrl+C 停止所有服务"
|
||||
# All services started
|
||||
sleep 2
|
||||
echo "✅ All services started!"
|
||||
echo "📊 Service status:"
|
||||
echo "- API service: http://localhost:8000 (PID: $API_PID)"
|
||||
echo "- Health check: http://localhost:8000/health"
|
||||
echo "- Metrics: http://localhost:8000/metrics"
|
||||
|
||||
# 等待中断信号
|
||||
trap 'echo "🛑 正在停止服务..."; kill $API_PID $WORKER_PID $BEAT_PID 2>/dev/null; exit 0' INT
|
||||
# Wait for Ctrl+C to stop all services
|
||||
echo "🛑 Press Ctrl+C to stop all services"
|
||||
|
||||
# 等待所有后台进程
|
||||
# Wait for interrupt signal
|
||||
trap 'echo "🛑 Stopping services..."; kill $API_PID $WORKER_PID $BEAT_PID 2>/dev/null; exit 0' INT
|
||||
|
||||
# Wait for all background processes
|
||||
wait
|
||||
@ -1,120 +1,120 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Gitea Webhook Ambassador Python 启动脚本
|
||||
# Gitea Webhook Ambassador Python Start Script
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SERVICE_NAME="gitea-webhook-ambassador-python"
|
||||
LOG_FILE="$SCRIPT_DIR/logs/service.log"
|
||||
PID_FILE="$SCRIPT_DIR/service.pid"
|
||||
|
||||
# 创建日志目录
|
||||
# Create logs directory
|
||||
mkdir -p "$SCRIPT_DIR/logs"
|
||||
|
||||
# 激活虚拟环境
|
||||
# Activate virtual environment
|
||||
source "$SCRIPT_DIR/venv/bin/activate"
|
||||
|
||||
# 函数:启动服务
|
||||
# Function: Start service
|
||||
start_service() {
|
||||
echo "🚀 启动 $SERVICE_NAME..."
|
||||
echo "🚀 Starting $SERVICE_NAME..."
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "❌ 服务已在运行 (PID: $PID)"
|
||||
echo "❌ Service is already running (PID: $PID)"
|
||||
return 1
|
||||
else
|
||||
echo "⚠️ 发现过期的 PID 文件,正在清理..."
|
||||
echo "⚠️ Found stale PID file, cleaning up..."
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 后台启动服务
|
||||
# Start service in background
|
||||
nohup python -m uvicorn app.main_demo:app --host 0.0.0.0 --port 8000 > "$LOG_FILE" 2>&1 &
|
||||
PID=$!
|
||||
echo $PID > "$PID_FILE"
|
||||
|
||||
# 等待服务启动
|
||||
# Wait for service to start
|
||||
sleep 3
|
||||
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "✅ 服务启动成功 (PID: $PID)"
|
||||
echo "📝 日志文件: $LOG_FILE"
|
||||
echo "🌐 访问地址: http://localhost:8000"
|
||||
echo "🔑 演示密钥: demo_admin_key, demo_user_key"
|
||||
echo "✅ Service started successfully (PID: $PID)"
|
||||
echo "📝 Log file: $LOG_FILE"
|
||||
echo "🌐 Access: http://localhost:8000"
|
||||
echo "🔑 Demo keys: demo_admin_key, demo_user_key"
|
||||
else
|
||||
echo "❌ 服务启动失败"
|
||||
echo "❌ Service failed to start"
|
||||
rm -f "$PID_FILE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:停止服务
|
||||
# Function: Stop service
|
||||
stop_service() {
|
||||
echo "🛑 停止 $SERVICE_NAME..."
|
||||
echo "🛑 Stopping $SERVICE_NAME..."
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
kill $PID
|
||||
echo "✅ 服务已停止 (PID: $PID)"
|
||||
echo "✅ Service stopped (PID: $PID)"
|
||||
else
|
||||
echo "⚠️ 服务未运行"
|
||||
echo "⚠️ Service not running"
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
else
|
||||
echo "⚠️ PID 文件不存在"
|
||||
echo "⚠️ PID file does not exist"
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:重启服务
|
||||
# Function: Restart service
|
||||
restart_service() {
|
||||
echo "🔄 重启 $SERVICE_NAME..."
|
||||
echo "🔄 Restarting $SERVICE_NAME..."
|
||||
stop_service
|
||||
sleep 2
|
||||
start_service
|
||||
}
|
||||
|
||||
# 函数:查看状态
|
||||
# Function: Show status
|
||||
status_service() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "✅ $SERVICE_NAME 正在运行 (PID: $PID)"
|
||||
echo "📝 日志文件: $LOG_FILE"
|
||||
echo "🌐 访问地址: http://localhost:8000"
|
||||
echo "✅ $SERVICE_NAME is running (PID: $PID)"
|
||||
echo "📝 Log file: $LOG_FILE"
|
||||
echo "🌐 Access: http://localhost:8000"
|
||||
else
|
||||
echo "❌ $SERVICE_NAME 未运行 (PID 文件存在但进程不存在)"
|
||||
echo "❌ $SERVICE_NAME is not running (PID file exists but process not found)"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
else
|
||||
echo "❌ $SERVICE_NAME 未运行"
|
||||
echo "❌ $SERVICE_NAME is not running"
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:查看日志
|
||||
# Function: Show logs
|
||||
show_logs() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo "📝 显示最新日志 (最后 50 行):"
|
||||
echo "📝 Showing latest logs (last 50 lines):"
|
||||
echo "----------------------------------------"
|
||||
tail -n 50 "$LOG_FILE"
|
||||
echo "----------------------------------------"
|
||||
echo "完整日志文件: $LOG_FILE"
|
||||
echo "Full log file: $LOG_FILE"
|
||||
else
|
||||
echo "❌ 日志文件不存在"
|
||||
echo "❌ Log file does not exist"
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:实时日志
|
||||
# Function: Follow logs
|
||||
follow_logs() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo "📝 实时日志 (按 Ctrl+C 退出):"
|
||||
echo "📝 Real-time logs (Ctrl+C to exit):"
|
||||
tail -f "$LOG_FILE"
|
||||
else
|
||||
echo "❌ 日志文件不存在"
|
||||
echo "❌ Log file does not exist"
|
||||
fi
|
||||
}
|
||||
|
||||
# 主逻辑
|
||||
# Main logic
|
||||
case "$1" in
|
||||
start)
|
||||
start_service
|
||||
@ -135,20 +135,20 @@ case "$1" in
|
||||
follow_logs
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 {start|stop|restart|status|logs|follow}"
|
||||
echo "Usage: $0 {start|stop|restart|status|logs|follow}"
|
||||
echo ""
|
||||
echo "命令说明:"
|
||||
echo " start - 启动服务"
|
||||
echo " stop - 停止服务"
|
||||
echo " restart - 重启服务"
|
||||
echo " status - 查看服务状态"
|
||||
echo " logs - 查看最新日志"
|
||||
echo " follow - 实时查看日志"
|
||||
echo "Command description:"
|
||||
echo " start - Start service"
|
||||
echo " stop - Stop service"
|
||||
echo " restart - Restart service"
|
||||
echo " status - Show service status"
|
||||
echo " logs - Show latest logs"
|
||||
echo " follow - Show real-time logs"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 start # 启动服务"
|
||||
echo " $0 status # 查看状态"
|
||||
echo " $0 logs # 查看日志"
|
||||
echo "Examples:"
|
||||
echo " $0 start # Start service"
|
||||
echo " $0 status # Show status"
|
||||
echo " $0 logs # Show logs"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
认证功能测试脚本
|
||||
演示如何正确使用 JWT 和 API 密钥认证
|
||||
Authentication feature test script
|
||||
Demonstrates how to properly use JWT and API key authentication
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -11,140 +11,138 @@ from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
async def test_jwt_authentication():
|
||||
"""测试 JWT 认证"""
|
||||
print("🔐 测试 JWT 认证")
|
||||
def print_divider():
|
||||
print("-" * 50)
|
||||
|
||||
async def test_jwt_authentication():
|
||||
"""Test JWT authentication"""
|
||||
print("🔐 Testing JWT authentication")
|
||||
print_divider()
|
||||
|
||||
# 注意:在实际应用中,JWT 令牌应该通过登录端点获取
|
||||
# 这里我们使用一个示例令牌(在实际环境中需要从登录端点获取)
|
||||
# Note: In actual applications, JWT tokens should be obtained via the login endpoint
|
||||
# Here we use a sample token (in real environments, obtain from login endpoint)
|
||||
|
||||
# 模拟 JWT 令牌(实际应用中应该从登录端点获取)
|
||||
# Simulate JWT token (should be obtained from login endpoint in real use)
|
||||
jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTczMjAwMDAwMH0.test"
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 使用 JWT 令牌访问管理端点
|
||||
# Use JWT token to access admin endpoint
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
|
||||
# 测试访问日志端点
|
||||
# Test access to logs endpoint
|
||||
async with session.get(f"{BASE_URL}/api/logs", headers=headers) as response:
|
||||
if response.status == 200:
|
||||
logs = await response.json()
|
||||
print("✅ JWT 认证成功 - 日志访问")
|
||||
print(f" 获取到 {len(logs)} 条日志")
|
||||
print("✅ JWT authentication succeeded - logs access")
|
||||
print(f" Retrieved {len(logs)} logs")
|
||||
else:
|
||||
print(f"❌ JWT 认证失败 - 日志访问: {response.status}")
|
||||
print(f"❌ JWT authentication failed - logs access: {response.status}")
|
||||
if response.status == 401:
|
||||
print(" 原因: JWT 令牌无效或已过期")
|
||||
|
||||
print(" Reason: JWT token is invalid or expired")
|
||||
print()
|
||||
|
||||
async def test_api_key_authentication():
|
||||
"""测试 API 密钥认证"""
|
||||
print("🔑 测试 API 密钥认证")
|
||||
print("-" * 50)
|
||||
"""Test API key authentication"""
|
||||
print("🔑 Testing API key authentication")
|
||||
print_divider()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 首先创建一个 API 密钥(需要管理员权限)
|
||||
# 注意:这里我们使用一个临时的认证方式
|
||||
# First, create an API key (requires admin privileges)
|
||||
# Note: Here we use a temporary authentication method
|
||||
|
||||
# 方法1:直接使用内存中的 API 密钥(仅用于演示)
|
||||
# 在实际应用中,API 密钥应该通过管理界面创建
|
||||
# Method 1: Use in-memory API key (for demo only)
|
||||
# In real applications, API keys should be created via the admin interface
|
||||
|
||||
# 模拟一个有效的 API 密钥
|
||||
# Simulate a valid API key
|
||||
api_key = "test_api_key_12345"
|
||||
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
|
||||
# 测试访问日志端点
|
||||
# Test access to logs endpoint
|
||||
async with session.get(f"{BASE_URL}/api/logs", headers=headers) as response:
|
||||
if response.status == 200:
|
||||
logs = await response.json()
|
||||
print("✅ API 密钥认证成功 - 日志访问")
|
||||
print(f" 获取到 {len(logs)} 条日志")
|
||||
print("✅ API key authentication succeeded - logs access")
|
||||
print(f" Retrieved {len(logs)} logs")
|
||||
else:
|
||||
print(f"❌ API 密钥认证失败 - 日志访问: {response.status}")
|
||||
print(f"❌ API key authentication failed - logs access: {response.status}")
|
||||
if response.status == 401:
|
||||
print(" 原因: API 密钥无效或已撤销")
|
||||
|
||||
print(" Reason: API key is invalid or revoked")
|
||||
print()
|
||||
|
||||
async def test_public_endpoints():
|
||||
"""测试公开端点(无需认证)"""
|
||||
print("🌐 测试公开端点")
|
||||
print("-" * 50)
|
||||
"""Test public endpoints (no authentication required)"""
|
||||
print("🌐 Testing public endpoints")
|
||||
print_divider()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 健康检查端点(无需认证)
|
||||
# Health check endpoint (no authentication required)
|
||||
async with session.get(f"{BASE_URL}/health") as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
print("✅ 健康检查端点访问成功")
|
||||
print(f" 状态: {data['status']}")
|
||||
print(f" Jenkins: {data['jenkins']['status']}")
|
||||
print("✅ Health check endpoint accessed successfully")
|
||||
print(f" Status: {data['status']}")
|
||||
else:
|
||||
print(f"❌ 健康检查端点访问失败: {response.status}")
|
||||
print(f"❌ Health check endpoint access failed: {response.status}")
|
||||
|
||||
# Webhook 端点(无需认证)
|
||||
# Webhook endpoint (no authentication required)
|
||||
webhook_data = {"test": "webhook_data"}
|
||||
async with session.post(f"{BASE_URL}/webhook/gitea", json=webhook_data) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
print("✅ Webhook 端点访问成功")
|
||||
print(f" 响应: {data['message']}")
|
||||
print("✅ Webhook endpoint accessed successfully")
|
||||
print(f" Response: {data['message']}")
|
||||
else:
|
||||
print(f"❌ Webhook 端点访问失败: {response.status}")
|
||||
|
||||
print(f"❌ Webhook endpoint access failed: {response.status}")
|
||||
print()
|
||||
|
||||
async def test_authentication_flow():
|
||||
"""测试完整的认证流程"""
|
||||
print("🔄 测试完整认证流程")
|
||||
print("-" * 50)
|
||||
"""Test the complete authentication flow"""
|
||||
print("🔄 Testing complete authentication flow")
|
||||
print_divider()
|
||||
|
||||
print("📋 认证流程说明:")
|
||||
print("1. 公开端点: /health, /webhook/gitea - 无需认证")
|
||||
print("2. 管理端点: /api/admin/* - 需要 JWT 或 API 密钥")
|
||||
print("3. 日志端点: /api/logs/* - 需要 JWT 或 API 密钥")
|
||||
print("📋 Authentication flow description:")
|
||||
print("1. Public endpoints: /health, /webhook/gitea - no authentication required")
|
||||
print("2. Admin endpoints: /api/admin/* - JWT or API key required")
|
||||
print("3. Logs endpoints: /api/logs/* - JWT or API key required")
|
||||
print()
|
||||
|
||||
print("🔧 如何获取认证令牌:")
|
||||
print("1. JWT 令牌: 通过登录端点获取(需要实现登录功能)")
|
||||
print("2. API 密钥: 通过管理界面创建(需要管理员权限)")
|
||||
print("🔧 How to obtain authentication tokens:")
|
||||
print("1. JWT token: Obtain via login endpoint (login feature required)")
|
||||
print("2. API key: Create via admin interface (admin privileges required)")
|
||||
print()
|
||||
|
||||
print("⚠️ 当前演示限制:")
|
||||
print("- 使用模拟的认证令牌")
|
||||
print("- 实际应用中需要实现完整的登录和密钥管理")
|
||||
print("- 建议在生产环境中使用真实的认证系统")
|
||||
|
||||
print("⚠️ Demo limitations:")
|
||||
print("- Using simulated authentication tokens")
|
||||
print("- In real applications, implement full login and key management")
|
||||
print("- It is recommended to use real authentication systems in production")
|
||||
print()
|
||||
|
||||
async def create_demo_api_key():
|
||||
"""创建演示用的 API 密钥"""
|
||||
print("🔧 创建演示 API 密钥")
|
||||
print("-" * 50)
|
||||
"""Create a demo API key"""
|
||||
print("🔧 Creating demo API key")
|
||||
print_divider()
|
||||
|
||||
# 注意:这是一个简化的演示
|
||||
# 在实际应用中,API 密钥应该通过安全的方式创建和存储
|
||||
# Note: This is a simplified demo
|
||||
# In real applications, API keys should be created and stored securely
|
||||
|
||||
demo_api_key = "demo_api_key_" + str(int(datetime.now().timestamp()))
|
||||
|
||||
print(f"✅ 演示 API 密钥已创建: {demo_api_key}")
|
||||
print("📝 使用方法:")
|
||||
print(f"✅ Demo API key created: {demo_api_key}")
|
||||
print("📝 Usage:")
|
||||
print(f" curl -H 'Authorization: Bearer {demo_api_key}' {BASE_URL}/api/logs")
|
||||
print()
|
||||
|
||||
return demo_api_key
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("🚀 开始认证功能测试")
|
||||
"""Main test function"""
|
||||
print("🚀 Starting authentication feature tests")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
try:
|
||||
# 等待服务启动
|
||||
# Wait for service to start
|
||||
await asyncio.sleep(2)
|
||||
|
||||
await test_public_endpoints()
|
||||
@ -152,21 +150,21 @@ async def main():
|
||||
await test_api_key_authentication()
|
||||
await test_authentication_flow()
|
||||
|
||||
# 创建演示 API 密钥
|
||||
# Create demo API key
|
||||
demo_key = await create_demo_api_key()
|
||||
|
||||
print("=" * 60)
|
||||
print("🎉 认证功能测试完成!")
|
||||
print("🎉 Authentication feature tests completed!")
|
||||
print()
|
||||
print("📚 下一步建议:")
|
||||
print("1. 实现完整的登录系统")
|
||||
print("2. 添加用户管理功能")
|
||||
print("3. 实现 API 密钥的安全存储")
|
||||
print("4. 添加权限控制机制")
|
||||
print("5. 实现会话管理")
|
||||
print("📚 Next steps:")
|
||||
print("1. Implement a full login system")
|
||||
print("2. Add user management features")
|
||||
print("3. Implement secure API key storage")
|
||||
print("4. Add permission control mechanisms")
|
||||
print("5. Implement session management")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 测试过程中出现错误: {str(e)}")
|
||||
print(f"❌ Error occurred during testing: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
增强版 Gitea Webhook Ambassador 功能测试脚本
|
||||
演示所有新增的监测和管理功能
|
||||
Enhanced Gitea Webhook Ambassador feature test script
|
||||
Demonstrates all new monitoring and management features
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -11,32 +11,34 @@ from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
async def test_health_check():
|
||||
"""测试增强的健康检查"""
|
||||
print("🧪 测试增强的健康检查")
|
||||
def print_divider():
|
||||
print("-" * 50)
|
||||
|
||||
async def test_health_check():
|
||||
"""Test enhanced health check"""
|
||||
print("🧪 Testing enhanced health check")
|
||||
print_divider()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{BASE_URL}/health") as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
print("✅ 健康检查通过")
|
||||
print(f" 状态: {data['status']}")
|
||||
print(f" 服务: {data['service']}")
|
||||
print("✅ Health check passed")
|
||||
print(f" Status: {data['status']}")
|
||||
print(f" Service: {data['service']}")
|
||||
print(f" Jenkins: {data['jenkins']['status']}")
|
||||
print(f" 工作池: {data['worker_pool']['active_workers']} 活跃工作线程")
|
||||
print(f" 队列大小: {data['worker_pool']['queue_size']}")
|
||||
print(f" 已处理: {data['worker_pool']['total_processed']}")
|
||||
print(f" 失败: {data['worker_pool']['total_failed']}")
|
||||
print(f" Worker pool: {data['worker_pool']['active_workers']} active workers")
|
||||
print(f" Queue size: {data['worker_pool']['queue_size']}")
|
||||
print(f" Processed: {data['worker_pool']['total_processed']}")
|
||||
print(f" Failed: {data['worker_pool']['total_failed']}")
|
||||
else:
|
||||
print(f"❌ 健康检查失败: {response.status}")
|
||||
|
||||
print(f"❌ Health check failed: {response.status}")
|
||||
print()
|
||||
|
||||
async def test_webhook():
|
||||
"""测试 Webhook 功能"""
|
||||
print("🧪 测试 Webhook 功能")
|
||||
print("-" * 50)
|
||||
"""Test webhook feature"""
|
||||
print("🧪 Testing webhook feature")
|
||||
print_divider()
|
||||
|
||||
webhook_data = {
|
||||
"ref": "refs/heads/dev",
|
||||
@ -59,22 +61,21 @@ async def test_webhook():
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
print("✅ Webhook 处理成功")
|
||||
print(f" 响应: {data['message']}")
|
||||
print(f" 数据大小: {data['data']['body_size']} bytes")
|
||||
print("✅ Webhook processed successfully")
|
||||
print(f" Response: {data['message']}")
|
||||
print(f" Data size: {data['data']['body_size']} bytes")
|
||||
else:
|
||||
print(f"❌ Webhook 处理失败: {response.status}")
|
||||
|
||||
print(f"❌ Webhook processing failed: {response.status}")
|
||||
print()
|
||||
|
||||
async def test_api_key_management():
|
||||
"""测试 API 密钥管理"""
|
||||
print("🧪 测试 API 密钥管理")
|
||||
print("-" * 50)
|
||||
"""Test API key management"""
|
||||
print("🧪 Testing API key management")
|
||||
print_divider()
|
||||
|
||||
# 创建 API 密钥
|
||||
# Create API key
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 创建密钥
|
||||
# Create key
|
||||
create_data = {"name": "test-api-key"}
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/admin/api-keys",
|
||||
@ -85,41 +86,40 @@ async def test_api_key_management():
|
||||
data = await response.json()
|
||||
api_key = data['key']
|
||||
key_id = data['id']
|
||||
print(f"✅ API 密钥创建成功")
|
||||
print(f"✅ API key created successfully")
|
||||
print(f" ID: {key_id}")
|
||||
print(f" 名称: {data['name']}")
|
||||
print(f" 密钥: {api_key[:8]}...{api_key[-8:]}")
|
||||
print(f" Name: {data['name']}")
|
||||
print(f" Key: {api_key[:8]}...{api_key[-8:]}")
|
||||
|
||||
# 使用新创建的密钥测试日志端点
|
||||
print("\n 测试使用新密钥访问日志端点...")
|
||||
# Test logs endpoint with new key
|
||||
print("\n Testing logs endpoint with new key...")
|
||||
async with session.get(
|
||||
f"{BASE_URL}/api/logs",
|
||||
headers={"Authorization": f"Bearer {api_key}"}
|
||||
) as log_response:
|
||||
if log_response.status == 200:
|
||||
logs = await log_response.json()
|
||||
print(f" ✅ 日志访问成功,获取到 {len(logs)} 条日志")
|
||||
print(f" ✅ Logs access succeeded, retrieved {len(logs)} logs")
|
||||
else:
|
||||
print(f" ❌ 日志访问失败: {log_response.status}")
|
||||
print(f" ❌ Logs access failed: {log_response.status}")
|
||||
|
||||
# 删除密钥
|
||||
# Delete key
|
||||
async with session.delete(
|
||||
f"{BASE_URL}/api/admin/api-keys/{key_id}",
|
||||
headers={"Authorization": f"Bearer {api_key}"}
|
||||
) as delete_response:
|
||||
if delete_response.status == 200:
|
||||
print(f" ✅ API 密钥删除成功")
|
||||
print(f" ✅ API key deleted successfully")
|
||||
else:
|
||||
print(f" ❌ API 密钥删除失败: {delete_response.status}")
|
||||
print(f" ❌ API key deletion failed: {delete_response.status}")
|
||||
else:
|
||||
print(f"❌ API 密钥创建失败: {response.status}")
|
||||
|
||||
print(f"❌ API key creation failed: {response.status}")
|
||||
print()
|
||||
|
||||
async def test_project_mapping():
|
||||
"""测试项目映射管理"""
|
||||
print("🧪 测试项目映射管理")
|
||||
print("-" * 50)
|
||||
"""Test project mapping management"""
|
||||
print("🧪 Testing project mapping management")
|
||||
print_divider()
|
||||
|
||||
mapping_data = {
|
||||
"repository_name": "freeleaps/test-project",
|
||||
@ -135,7 +135,7 @@ async def test_project_mapping():
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 创建项目映射
|
||||
# Create project mapping
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/admin/projects",
|
||||
json=mapping_data,
|
||||
@ -143,45 +143,43 @@ async def test_project_mapping():
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
print("✅ 项目映射创建成功")
|
||||
print("✅ Project mapping created successfully")
|
||||
print(f" ID: {data['id']}")
|
||||
print(f" 仓库: {data['repository_name']}")
|
||||
print(f" 默认任务: {data['default_job']}")
|
||||
print(f" 分支任务数: {len(data['branch_jobs'])}")
|
||||
print(f" 分支模式数: {len(data['branch_patterns'])}")
|
||||
print(f" Repository: {data['repository_name']}")
|
||||
print(f" Default job: {data['default_job']}")
|
||||
print(f" Branch jobs: {len(data['branch_jobs'])}")
|
||||
print(f" Branch patterns: {len(data['branch_patterns'])}")
|
||||
else:
|
||||
print(f"❌ 项目映射创建失败: {response.status}")
|
||||
|
||||
print(f"❌ Project mapping creation failed: {response.status}")
|
||||
print()
|
||||
|
||||
async def test_logs_and_stats():
|
||||
"""测试日志和统计功能"""
|
||||
print("🧪 测试日志和统计功能")
|
||||
print("-" * 50)
|
||||
"""Test logs and statistics features"""
|
||||
print("🧪 Testing logs and statistics features")
|
||||
print_divider()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 获取日志统计
|
||||
# Get log statistics
|
||||
async with session.get(
|
||||
f"{BASE_URL}/api/logs/stats",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
stats = await response.json()
|
||||
print("✅ 日志统计获取成功")
|
||||
print(f" 总日志数: {stats['total_logs']}")
|
||||
print(f" 成功日志: {stats['successful_logs']}")
|
||||
print(f" 失败日志: {stats['failed_logs']}")
|
||||
print(f" 最近24小时: {stats['recent_logs_24h']}")
|
||||
print(f" 仓库统计: {len(stats['repository_stats'])} 个仓库")
|
||||
print("✅ Log statistics retrieved successfully")
|
||||
print(f" Total logs: {stats['total_logs']}")
|
||||
print(f" Successful logs: {stats['successful_logs']}")
|
||||
print(f" Failed logs: {stats['failed_logs']}")
|
||||
print(f" Recent logs (24h): {stats['recent_logs_24h']}")
|
||||
print(f" Repository stats: {len(stats['repository_stats'])} repositories")
|
||||
else:
|
||||
print(f"❌ 日志统计获取失败: {response.status}")
|
||||
|
||||
print(f"❌ Log statistics retrieval failed: {response.status}")
|
||||
print()
|
||||
|
||||
async def test_admin_stats():
|
||||
"""测试管理统计"""
|
||||
print("🧪 测试管理统计")
|
||||
print("-" * 50)
|
||||
"""Test admin statistics"""
|
||||
print("🧪 Testing admin statistics")
|
||||
print_divider()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
@ -190,19 +188,18 @@ async def test_admin_stats():
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
stats = await response.json()
|
||||
print("✅ 管理统计获取成功")
|
||||
print(f" API 密钥总数: {stats['api_keys']['total']}")
|
||||
print(f" 活跃密钥: {stats['api_keys']['active']}")
|
||||
print(f" 最近使用: {stats['api_keys']['recently_used']}")
|
||||
print(f" 项目映射总数: {stats['project_mappings']['total']}")
|
||||
print("✅ Admin statistics retrieved successfully")
|
||||
print(f" Total API keys: {stats['api_keys']['total']}")
|
||||
print(f" Active keys: {stats['api_keys']['active']}")
|
||||
print(f" Recently used: {stats['api_keys']['recently_used']}")
|
||||
print(f" Total project mappings: {stats['project_mappings']['total']}")
|
||||
else:
|
||||
print(f"❌ 管理统计获取失败: {response.status}")
|
||||
|
||||
print(f"❌ Admin statistics retrieval failed: {response.status}")
|
||||
print()
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("🚀 开始增强版 Gitea Webhook Ambassador 功能测试")
|
||||
"""Main test function"""
|
||||
print("🚀 Starting enhanced Gitea Webhook Ambassador feature tests")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
@ -215,11 +212,11 @@ async def main():
|
||||
await test_admin_stats()
|
||||
|
||||
print("=" * 60)
|
||||
print("🎉 所有测试完成!")
|
||||
print("✅ Python 版本现在具备了与 Go 版本相同的监测和管理功能")
|
||||
print("🎉 All tests completed!")
|
||||
print("✅ Python version now has the same monitoring and management features as the Go version")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 测试过程中出现错误: {str(e)}")
|
||||
print(f"❌ Error occurred during testing: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Gitea Webhook Ambassador 增强版功能测试脚本
|
||||
Gitea Webhook Ambassador Enhanced Feature Test Script
|
||||
"""
|
||||
|
||||
import requests
|
||||
@ -11,29 +11,29 @@ BASE_URL = "http://localhost:8000"
|
||||
ADMIN_SECRET_KEY = "admin-secret-key-change-in-production"
|
||||
|
||||
def test_health_check():
|
||||
"""测试健康检查"""
|
||||
print("🔍 测试健康检查...")
|
||||
"""Test health check"""
|
||||
print("🔍 Testing health check...")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/health")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ 健康检查成功: {data['status']}")
|
||||
print(f" 版本: {data['version']}")
|
||||
print(f" 运行时间: {data['uptime']}")
|
||||
print(f" 内存使用: {data['memory']}")
|
||||
print(f"✅ Health check succeeded: {data['status']}")
|
||||
print(f" Version: {data['version']}")
|
||||
print(f" Uptime: {data['uptime']}")
|
||||
print(f" Memory usage: {data['memory']}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 健康检查失败: {response.status_code}")
|
||||
print(f"❌ Health check failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 健康检查异常: {e}")
|
||||
print(f"❌ Health check exception: {e}")
|
||||
return False
|
||||
|
||||
def test_login():
|
||||
"""测试登录功能"""
|
||||
print("\n🔐 测试登录功能...")
|
||||
"""Test login functionality"""
|
||||
print("\n🔐 Testing login functionality...")
|
||||
try:
|
||||
# 测试登录 API
|
||||
# Test login API
|
||||
login_data = {"secret_key": ADMIN_SECRET_KEY}
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/auth/login",
|
||||
@ -44,23 +44,23 @@ def test_login():
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
token = data.get("token")
|
||||
print(f"✅ 登录成功,获得 JWT 令牌")
|
||||
print(f"✅ Login succeeded, obtained JWT token")
|
||||
return token
|
||||
else:
|
||||
print(f"❌ 登录失败: {response.status_code} - {response.text}")
|
||||
print(f"❌ Login failed: {response.status_code} - {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"❌ 登录异常: {e}")
|
||||
print(f"❌ Login exception: {e}")
|
||||
return None
|
||||
|
||||
def test_api_key_management(token):
|
||||
"""测试 API 密钥管理"""
|
||||
print("\n🔑 测试 API 密钥管理...")
|
||||
"""Test API key management"""
|
||||
print("\n🔑 Testing API key management...")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
try:
|
||||
# 创建 API 密钥
|
||||
key_data = {"description": "测试 API 密钥"}
|
||||
# Create API key
|
||||
key_data = {"description": "Test API key"}
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/keys",
|
||||
json=key_data,
|
||||
@ -71,41 +71,41 @@ def test_api_key_management(token):
|
||||
data = response.json()
|
||||
api_key = data["key"]
|
||||
key_id = data["id"]
|
||||
print(f"✅ 创建 API 密钥成功: {api_key[:20]}...")
|
||||
print(f"✅ API key created successfully: {api_key[:20]}...")
|
||||
|
||||
# 列出 API 密钥
|
||||
# List API keys
|
||||
response = requests.get(f"{BASE_URL}/api/keys", headers=headers)
|
||||
if response.status_code == 200:
|
||||
keys_data = response.json()
|
||||
print(f"✅ 列出 API 密钥成功,共 {len(keys_data['keys'])} 个")
|
||||
print(f"✅ API keys listed successfully, total {len(keys_data['keys'])}")
|
||||
|
||||
# 删除 API 密钥
|
||||
# Delete API key
|
||||
response = requests.delete(f"{BASE_URL}/api/keys/{key_id}", headers=headers)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ 删除 API 密钥成功")
|
||||
print(f"✅ API key deleted successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 删除 API 密钥失败: {response.status_code}")
|
||||
print(f"❌ API key deletion failed: {response.status_code}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 列出 API 密钥失败: {response.status_code}")
|
||||
print(f"❌ API key listing failed: {response.status_code}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 创建 API 密钥失败: {response.status_code} - {response.text}")
|
||||
print(f"❌ API key creation failed: {response.status_code} - {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ API 密钥管理异常: {e}")
|
||||
print(f"❌ API key management exception: {e}")
|
||||
return False
|
||||
|
||||
def test_project_management(token):
|
||||
"""测试项目管理"""
|
||||
print("\n📁 测试项目管理...")
|
||||
"""Test project management"""
|
||||
print("\n📁 Testing project management...")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
try:
|
||||
# 创建项目
|
||||
# Create project
|
||||
project_data = {
|
||||
"name": "测试项目",
|
||||
"name": "Test Project",
|
||||
"jenkinsJob": "test-job",
|
||||
"giteaRepo": "test-owner/test-repo"
|
||||
}
|
||||
@ -118,109 +118,108 @@ def test_project_management(token):
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
project_id = data["id"]
|
||||
print(f"✅ 创建项目成功: {data['name']}")
|
||||
print(f"✅ Project created successfully: {data['name']}")
|
||||
|
||||
# 列出项目
|
||||
# List projects
|
||||
response = requests.get(f"{BASE_URL}/api/projects/", headers=headers)
|
||||
if response.status_code == 200:
|
||||
projects_data = response.json()
|
||||
print(f"✅ 列出项目成功,共 {len(projects_data['projects'])} 个")
|
||||
print(f"✅ Projects listed successfully, total {len(projects_data['projects'])}")
|
||||
|
||||
# 删除项目
|
||||
# Delete project
|
||||
response = requests.delete(f"{BASE_URL}/api/projects/{project_id}", headers=headers)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ 删除项目成功")
|
||||
print(f"✅ Project deleted successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 删除项目失败: {response.status_code}")
|
||||
print(f"❌ Project deletion failed: {response.status_code}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 列出项目失败: {response.status_code}")
|
||||
print(f"❌ Project listing failed: {response.status_code}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 创建项目失败: {response.status_code} - {response.text}")
|
||||
print(f"❌ Project creation failed: {response.status_code} - {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 项目管理异常: {e}")
|
||||
print(f"❌ Project management exception: {e}")
|
||||
return False
|
||||
|
||||
def test_stats(token):
|
||||
"""测试统计信息"""
|
||||
print("\n📊 测试统计信息...")
|
||||
"""Test statistics information"""
|
||||
print("\n📊 Testing statistics information...")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/api/stats", headers=headers)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ 获取统计信息成功:")
|
||||
print(f" 总项目数: {data['total_projects']}")
|
||||
print(f" API 密钥数: {data['total_api_keys']}")
|
||||
print(f" 今日触发次数: {data['today_triggers']}")
|
||||
print(f" 成功触发次数: {data['successful_triggers']}")
|
||||
print(f"✅ Statistics retrieved successfully:")
|
||||
print(f" Total projects: {data['total_projects']}")
|
||||
print(f" API keys: {data['total_api_keys']}")
|
||||
print(f" Triggers today: {data['today_triggers']}")
|
||||
print(f" Successful triggers: {data['successful_triggers']}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 获取统计信息失败: {response.status_code}")
|
||||
print(f"❌ Statistics retrieval failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 统计信息异常: {e}")
|
||||
print(f"❌ Statistics exception: {e}")
|
||||
return False
|
||||
|
||||
def test_logs(token):
|
||||
"""测试日志功能"""
|
||||
print("\n📝 测试日志功能...")
|
||||
"""Test log functionality"""
|
||||
print("\n📝 Testing log functionality...")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/api/logs", headers=headers)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ 获取日志成功,共 {len(data['logs'])} 条记录")
|
||||
print(f"✅ Logs retrieved successfully, total {len(data['logs'])} records")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 获取日志失败: {response.status_code}")
|
||||
print(f"❌ Log retrieval failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 日志功能异常: {e}")
|
||||
print(f"❌ Log functionality exception: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("🚀 Gitea Webhook Ambassador 增强版功能测试")
|
||||
"""Main test function"""
|
||||
print("🚀 Gitea Webhook Ambassador Enhanced Feature Test")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试健康检查
|
||||
# Test health check
|
||||
if not test_health_check():
|
||||
print("❌ 健康检查失败,服务可能未启动")
|
||||
print("❌ Health check failed, service may not be running")
|
||||
return
|
||||
|
||||
# 测试登录
|
||||
# Test login
|
||||
token = test_login()
|
||||
if not token:
|
||||
print("❌ 登录失败,无法继续测试")
|
||||
print("❌ Login failed, cannot continue testing")
|
||||
return
|
||||
|
||||
# 测试各项功能
|
||||
# Test features
|
||||
test_api_key_management(token)
|
||||
test_project_management(token)
|
||||
test_stats(token)
|
||||
test_logs(token)
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("🎉 增强版功能测试完成!")
|
||||
print("\n📋 已实现的功能:")
|
||||
print(" ✅ Web 登录界面")
|
||||
print(" ✅ 数据库存储 API 密钥")
|
||||
print(" ✅ 延长 JWT 有效期 (7天)")
|
||||
print(" ✅ 前端仪表板")
|
||||
print(" ✅ 项目管理")
|
||||
print(" ✅ API 密钥管理")
|
||||
print(" ✅ 日志查看")
|
||||
print(" ✅ 健康状态监控")
|
||||
print("\n🌐 访问地址:")
|
||||
print(f" 登录页面: {BASE_URL}/login")
|
||||
print(f" 仪表板: {BASE_URL}/dashboard")
|
||||
print(f" 管理员密钥: {ADMIN_SECRET_KEY}")
|
||||
print("🎉 Enhanced feature test completed!")
|
||||
print("\n📋 Implemented features:")
|
||||
print(" ✅ Web login interface")
|
||||
print(" ✅ Database storage for API keys")
|
||||
print(" ✅ Extended JWT validity (7 days)")
|
||||
print(" ✅ Frontend dashboard")
|
||||
print(" ✅ Project management")
|
||||
print(" ✅ API key management")
|
||||
print(" ✅ Log viewing")
|
||||
print(" ✅ Health status monitoring")
|
||||
print("\n🌐 Access URLs:")
|
||||
print(f" Login page: {BASE_URL}/login")
|
||||
print(f" Dashboard: {BASE_URL}/dashboard")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Webhook 功能测试脚本
|
||||
用于验证 Gitea Webhook Ambassador 的各项功能
|
||||
Webhook feature test script
|
||||
Used to verify all features of Gitea Webhook Ambassador
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -10,11 +10,11 @@ import httpx
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# 测试配置
|
||||
# Test configuration
|
||||
BASE_URL = "http://localhost:8000"
|
||||
WEBHOOK_SECRET = "your-secret-key-here-make-it-long-and-random"
|
||||
|
||||
# 测试数据
|
||||
# Test data
|
||||
TEST_WEBHOOK_DATA = {
|
||||
"ref": "refs/heads/dev",
|
||||
"before": "abc1234567890abcdef1234567890abcdef123456",
|
||||
@ -59,46 +59,46 @@ TEST_WEBHOOK_DATA = {
|
||||
|
||||
|
||||
async def test_health_check():
|
||||
"""测试健康检查"""
|
||||
print("🔍 测试健康检查...")
|
||||
"""Test health check"""
|
||||
print("🔍 Testing health check...")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(f"{BASE_URL}/health")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ 健康检查通过: {data['status']}")
|
||||
print(f"✅ Health check passed: {data['status']}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 健康检查失败: {response.status_code}")
|
||||
print(f"❌ Health check failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 健康检查异常: {e}")
|
||||
print(f"❌ Health check exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_queue_status():
|
||||
"""测试队列状态"""
|
||||
print("🔍 测试队列状态...")
|
||||
"""Test queue status"""
|
||||
print("🔍 Testing queue status...")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(f"{BASE_URL}/health/queue")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ 队列状态: {data['queue_stats']}")
|
||||
print(f"✅ Queue status: {data['queue_stats']}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 队列状态检查失败: {response.status_code}")
|
||||
print(f"❌ Queue status check failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 队列状态检查异常: {e}")
|
||||
print(f"❌ Queue status exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_webhook_endpoint():
|
||||
"""测试 Webhook 端点"""
|
||||
print("🔍 测试 Webhook 端点...")
|
||||
"""Test webhook endpoint"""
|
||||
print("🔍 Testing webhook endpoint...")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
@ -113,47 +113,47 @@ async def test_webhook_endpoint():
|
||||
json=TEST_WEBHOOK_DATA
|
||||
)
|
||||
|
||||
print(f"📊 响应状态: {response.status_code}")
|
||||
print(f"📊 响应内容: {response.text}")
|
||||
print(f"📊 Response status: {response.status_code}")
|
||||
print(f"📊 Response content: {response.text}")
|
||||
|
||||
if response.status_code in [200, 202]:
|
||||
print("✅ Webhook 端点测试通过")
|
||||
print("✅ Webhook endpoint test passed")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Webhook 端点测试失败: {response.status_code}")
|
||||
print(f"❌ Webhook endpoint test failed: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Webhook 端点测试异常: {e}")
|
||||
print(f"❌ Webhook endpoint exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_metrics_endpoint():
|
||||
"""测试监控指标端点"""
|
||||
print("🔍 测试监控指标端点...")
|
||||
"""Test metrics endpoint"""
|
||||
print("🔍 Testing metrics endpoint...")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(f"{BASE_URL}/metrics")
|
||||
if response.status_code == 200:
|
||||
print("✅ 监控指标端点测试通过")
|
||||
# 打印一些关键指标
|
||||
print("✅ Metrics endpoint test passed")
|
||||
# Print some key metrics
|
||||
content = response.text
|
||||
for line in content.split('\n'):
|
||||
if 'webhook_requests_total' in line or 'queue_size' in line:
|
||||
print(f"📊 {line}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 监控指标端点测试失败: {response.status_code}")
|
||||
print(f"❌ Metrics endpoint test failed: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 监控指标端点测试异常: {e}")
|
||||
print(f"❌ Metrics endpoint exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_deduplication():
|
||||
"""测试防抖功能"""
|
||||
print("🔍 测试防抖功能...")
|
||||
"""Test deduplication feature"""
|
||||
print("🔍 Testing deduplication feature...")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
@ -162,64 +162,64 @@ async def test_deduplication():
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
# 第一次请求
|
||||
print("📤 发送第一次请求...")
|
||||
# First request
|
||||
print("📤 Sending first request...")
|
||||
response1 = await client.post(
|
||||
f"{BASE_URL}/webhook/gitea",
|
||||
headers=headers,
|
||||
json=TEST_WEBHOOK_DATA
|
||||
)
|
||||
print(f"📊 第一次响应: {response1.status_code}")
|
||||
print(f"📊 First response: {response1.status_code}")
|
||||
|
||||
# 等待一秒
|
||||
# Wait one second
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 第二次请求(相同数据,应该被防抖)
|
||||
print("📤 发送第二次请求(相同数据)...")
|
||||
# Second request (same data, should be deduplicated)
|
||||
print("📤 Sending second request (same data)...")
|
||||
response2 = await client.post(
|
||||
f"{BASE_URL}/webhook/gitea",
|
||||
headers=headers,
|
||||
json=TEST_WEBHOOK_DATA
|
||||
)
|
||||
print(f"📊 第二次响应: {response2.status_code}")
|
||||
print(f"📊 Second response: {response2.status_code}")
|
||||
|
||||
# 修改提交哈希,发送第三次请求
|
||||
# Modify commit hash, send third request
|
||||
modified_data = TEST_WEBHOOK_DATA.copy()
|
||||
modified_data["after"] = "ghi1234567890abcdef1234567890abcdef123456"
|
||||
|
||||
print("📤 发送第三次请求(不同提交哈希)...")
|
||||
print("📤 Sending third request (different commit hash)...")
|
||||
response3 = await client.post(
|
||||
f"{BASE_URL}/webhook/gitea",
|
||||
headers=headers,
|
||||
json=modified_data
|
||||
)
|
||||
print(f"📊 第三次响应: {response3.status_code}")
|
||||
print(f"📊 Third response: {response3.status_code}")
|
||||
|
||||
print("✅ 防抖功能测试完成")
|
||||
print("✅ Deduplication feature test completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 防抖功能测试异常: {e}")
|
||||
print(f"❌ Deduplication feature exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_invalid_webhook():
|
||||
"""测试无效的 Webhook 请求"""
|
||||
print("🔍 测试无效的 Webhook 请求...")
|
||||
"""Test invalid webhook requests"""
|
||||
print("🔍 Testing invalid webhook requests...")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
# 测试缺少签名
|
||||
print("📤 测试缺少签名...")
|
||||
# Test missing signature
|
||||
print("📤 Testing missing signature...")
|
||||
response1 = await client.post(
|
||||
f"{BASE_URL}/webhook/gitea",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=TEST_WEBHOOK_DATA
|
||||
)
|
||||
print(f"📊 缺少签名响应: {response1.status_code}")
|
||||
print(f"📊 Missing signature response: {response1.status_code}")
|
||||
|
||||
# 测试错误的签名
|
||||
print("📤 测试错误的签名...")
|
||||
# Test wrong signature
|
||||
print("📤 Testing wrong signature...")
|
||||
response2 = await client.post(
|
||||
f"{BASE_URL}/webhook/gitea",
|
||||
headers={
|
||||
@ -228,10 +228,10 @@ async def test_invalid_webhook():
|
||||
},
|
||||
json=TEST_WEBHOOK_DATA
|
||||
)
|
||||
print(f"📊 错误签名响应: {response2.status_code}")
|
||||
print(f"📊 Wrong signature response: {response2.status_code}")
|
||||
|
||||
# 测试无效的 JSON
|
||||
print("📤 测试无效的 JSON...")
|
||||
# Test invalid JSON
|
||||
print("📤 Testing invalid JSON...")
|
||||
response3 = await client.post(
|
||||
f"{BASE_URL}/webhook/gitea",
|
||||
headers={
|
||||
@ -240,28 +240,28 @@ async def test_invalid_webhook():
|
||||
},
|
||||
content="invalid json"
|
||||
)
|
||||
print(f"📊 无效 JSON 响应: {response3.status_code}")
|
||||
print(f"📊 Invalid JSON response: {response3.status_code}")
|
||||
|
||||
print("✅ 无效请求测试完成")
|
||||
print("✅ Invalid request tests completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 无效请求测试异常: {e}")
|
||||
print(f"❌ Invalid request tests exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("🚀 开始 Gitea Webhook Ambassador 功能测试")
|
||||
"""Main test function"""
|
||||
print("🚀 Starting Gitea Webhook Ambassador feature tests")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
("健康检查", test_health_check),
|
||||
("队列状态", test_queue_status),
|
||||
("Webhook 端点", test_webhook_endpoint),
|
||||
("监控指标", test_metrics_endpoint),
|
||||
("防抖功能", test_deduplication),
|
||||
("无效请求", test_invalid_webhook),
|
||||
("Health Check", test_health_check),
|
||||
("Queue Status", test_queue_status),
|
||||
("Webhook Endpoint", test_webhook_endpoint),
|
||||
("Metrics", test_metrics_endpoint),
|
||||
("Deduplication", test_deduplication),
|
||||
("Invalid Requests", test_invalid_webhook),
|
||||
]
|
||||
|
||||
results = []
|
||||
@ -274,34 +274,34 @@ async def main():
|
||||
result = await test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f"❌ {test_name} 测试异常: {e}")
|
||||
print(f"❌ {test_name} test exception: {e}")
|
||||
results.append((test_name, False))
|
||||
|
||||
# 等待一下再进行下一个测试
|
||||
# Wait a bit before next test
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 输出测试结果
|
||||
# Output test results
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 测试结果汇总")
|
||||
print("📊 Test Results Summary")
|
||||
print("=" * 50)
|
||||
|
||||
passed = 0
|
||||
total = len(results)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✅ 通过" if result else "❌ 失败"
|
||||
status = "✅ Passed" if result else "❌ Failed"
|
||||
print(f"{test_name}: {status}")
|
||||
if result:
|
||||
passed += 1
|
||||
|
||||
print(f"\n📈 总体结果: {passed}/{total} 测试通过")
|
||||
print(f"\n📈 Overall: {passed}/{total} tests passed")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 所有测试通过!服务运行正常。")
|
||||
print("🎉 All tests passed! Service is running normally.")
|
||||
else:
|
||||
print("⚠️ 部分测试失败,请检查服务配置和日志。")
|
||||
print("⚠️ Some tests failed, please check service configuration and logs.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 运行测试
|
||||
# Run tests
|
||||
asyncio.run(main())
|
||||
Loading…
Reference in New Issue
Block a user