This commit is contained in:
Dang Zerong
2026-03-10 11:18:39 +08:00
parent 31e3b7b497
commit 8594cf4d77
4 changed files with 208 additions and 14 deletions

96
app.py
View File

@@ -3,6 +3,7 @@
import os
import logging
from typing import Dict, Tuple, Any
os.environ.setdefault('FLASK_RUN_HOST', '0.0.0.0')
@@ -79,7 +80,11 @@ def handle_gitea_webhook():
event_type = request.headers.get('X-Gitea-Event', 'push')
logger.info(f'收到 Gitea Webhook 事件: {event_type}')
# 处理 push 事件
# 处理 Pull Request 事件
if event_type == 'pull_request':
return handle_pull_request(payload)
# 处理 Push 事件
if event_type != 'push':
return jsonify({'message': 'Event ignored'}), 200
@@ -161,6 +166,95 @@ def handle_gitea_webhook():
return jsonify({'error': str(e)}), 500
def handle_pull_request(payload: Dict[str, Any]) -> Tuple[Dict, int]:
"""
处理 Pull Request 事件
Args:
payload: Webhook payload
Returns:
JSON 响应和状态码
"""
try:
# 解析 PR 事件
pr_info = webhook_handler.parse_pull_request_event(payload)
if not pr_info:
logger.info('PR 事件不需要处理(如关闭、合并等)')
return jsonify({'message': 'PR event ignored'}), 200
repo_name = pr_info['repo_name']
source_branch = pr_info['source_branch']
source_sha = pr_info['source_sha']
pr_number = pr_info['pr_number']
pr_title = pr_info['pr_title']
pr_url = pr_info['pr_url']
target_branch = pr_info['target_branch']
author = pr_info['author']
logger.info(f'处理 PR #{pr_number}: {pr_title} ({source_branch} -> {target_branch})')
logger.info(f'扫描 PR 分支: {source_branch}, commit: {source_sha}')
try:
# 获取仓库 URL
repo = payload.get('repository', {})
clone_url = repo.get('clone_url')
if not clone_url:
web_url = repo.get('web_url', '')
if web_url:
clone_url = web_url.rstrip('/') + '.git'
# 执行代码扫描
scan_results = {}
# Python 扫描
if 'python' in config.get('scanner', {}).get('languages', []):
scan_results['python'] = python_scanner.scan(
clone_url, source_sha, source_branch
)
# JavaScript/TypeScript 扫描
if any(lang in config.get('scanner', {}).get('languages', [])
for lang in ['javascript', 'typescript']):
scan_results['javascript'] = js_scanner.scan(
clone_url, source_sha, source_branch
)
# 安全扫描
scan_results['security'] = security_scanner.scan(
clone_url, source_sha, source_branch
)
# 生成报告
commit_message = f'PR #{pr_number}: {pr_title}'
report = report_generator.generate(
repo_name=repo_name,
branch=source_branch,
commit_id=source_sha,
commit_message=commit_message,
author=author,
scan_results=scan_results,
pr_url=pr_url,
target_branch=target_branch
)
# 发送飞书通知
feishu_notifier.send_report(report)
logger.info(f'PR #{pr_number} 扫描完成')
except Exception as e:
logger.error(f'扫描 PR #{pr_number} 失败: {str(e)}')
return jsonify({'error': str(e)}), 500
return jsonify({'status': 'ok', 'message': 'PR scan completed'}), 200
except Exception as e:
logger.error(f'处理 PR Webhook 失败: {str(e)}', exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/scan/manual', methods=['POST'])
def manual_scan():
"""手动触发扫描接口"""

View File

@@ -155,12 +155,33 @@ class FeishuNotifier:
scan_details.append(detail_text)
# 检查是否为 PR 扫描
pr_url = report.get('pr_url')
target_branch = report.get('target_branch')
# 构建基本信息文本
if pr_url and target_branch:
# PR 扫描
title = f"{status_icon} PR 代码质量扫描报告"
basic_info = (f"**仓库:** `{report.get('repo_name', 'unknown')}`\n"
f"**源分支:** `{report.get('branch', 'unknown')}` → **目标分支:** `{target_branch}`\n"
f"**PR链接:** [查看PR]({pr_url})\n"
f"**提交:** `{report.get('commit_id', 'unknown')}`\n"
f"**提交者:** {report.get('author', 'unknown')}")
else:
# Push 扫描
title = f"{status_icon} 代码质量扫描报告"
basic_info = (f"**仓库:** `{report.get('repo_name', 'unknown')}`\n"
f"**分支:** `{report.get('branch', 'unknown')}`\n"
f"**提交:** `{report.get('commit_id', 'unknown')}`\n"
f"**提交者:** {report.get('author', 'unknown')}")
# 构建卡片消息
card = {
"header": {
"title": {
"tag": "plain_text",
"content": f"{status_icon} 代码质量扫描报告"
"content": title
},
"template": theme_color
},
@@ -169,10 +190,7 @@ class FeishuNotifier:
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**仓库:** `{report.get('repo_name', 'unknown')}`\n"
f"**分支:** `{report.get('branch', 'unknown')}`\n"
f"**提交:** `{report.get('commit_id', 'unknown')}`\n"
f"**提交者:** {report.get('author', 'unknown')}"
"content": basic_info
}
},
{

View File

@@ -37,7 +37,9 @@ class ReportGenerator:
commit_id: str,
commit_message: str,
author: str,
scan_results: Dict[str, Any]
scan_results: Dict[str, Any],
pr_url: str = None,
target_branch: str = None
) -> Dict[str, Any]:
"""
生成扫描报告
@@ -49,6 +51,8 @@ class ReportGenerator:
commit_message: 提交信息
author: 提交者
scan_results: 扫描结果
pr_url: PR 链接(可选)
target_branch: 目标分支(可选,用于 PR 扫描)
Returns:
报告数据
@@ -89,8 +93,10 @@ class ReportGenerator:
'total_errors': total_errors,
'total_warnings': total_warnings,
'scan_results': scan_results,
'pr_url': pr_url,
'target_branch': target_branch,
'markdown': self._generate_markdown(
repo_name, branch, commit_id, commit_message, author, scan_results, status, status_text
repo_name, branch, commit_id, commit_message, author, scan_results, status, status_text, pr_url, target_branch
)
}
@@ -109,13 +115,18 @@ class ReportGenerator:
author: str,
scan_results: Dict[str, Any],
status: str,
status_text: str
status_text: str,
pr_url: str = None,
target_branch: str = None
) -> str:
"""生成 Markdown 格式的报告"""
lines = []
# 标题
lines.append('# 📊 代码质量扫描报告')
# 标题 - 根据是否为 PR 扫描显示不同标题
if pr_url:
lines.append('# 📊 PR 代码质量扫描报告')
else:
lines.append('# 📊 代码质量扫描报告')
lines.append('')
# 基本信息
@@ -124,7 +135,15 @@ class ReportGenerator:
lines.append(f'| 项目 | 内容 |')
lines.append(f'|------|------|')
lines.append(f'| 仓库 | `{repo_name}` |')
lines.append(f'| 分支 | `{branch}` |')
# 如果是 PR显示 PR 特有信息
if pr_url and target_branch:
lines.append(f'| 源分支 | `{branch}` |')
lines.append(f'| 目标分支 | `{target_branch}` |')
lines.append(f'| PR 链接 | [查看 PR]({pr_url}) |')
else:
lines.append(f'| 分支 | `{branch}` |')
lines.append(f'| 提交 | `{commit_id}` |')
lines.append(f'| 提交者 | {author} |')
lines.append(f'| 提交信息 | {commit_message[:50]}... |' if len(commit_message) > 50 else f'| 提交信息 | {commit_message} |')

View File

@@ -124,3 +124,66 @@ class GiteaWebhookHandler:
f for f in files
if any(f.endswith(ext) for ext in extensions)
]
def parse_pull_request_event(self, payload: Dict[str, Any]) -> Dict[str, Any]:
"""
解析 Pull Request 事件
Args:
payload: Webhook payload
Returns:
解析后的 PR 信息
"""
action = payload.get('action', '')
pr = payload.get('pull_request', {})
repo = payload.get('repository', {})
# 只处理 PR 创建和更新事件
if action not in ['opened', 'reopened', 'synchronize', 'ready_for_review']:
return None
# 获取 PR 的源分支和目标分支
head = pr.get('head', {})
base = pr.get('base', {})
return {
'action': action,
'repo_name': repo.get('full_name', ''),
'repo_url': repo.get('clone_url', ''),
'web_url': repo.get('web_url', ''),
'pr_number': pr.get('number', 0),
'pr_title': pr.get('title', ''),
'pr_body': pr.get('body', ''),
'pr_url': pr.get('html_url', ''),
'source_branch': head.get('ref', ''),
'source_sha': head.get('sha', '')[:8],
'target_branch': base.get('ref', ''),
'target_sha': base.get('sha', '')[:8],
'author': pr.get('user', {}).get('login', ''),
'author_email': pr.get('user', {}).get('email', ''),
'state': pr.get('state', ''),
'merged': pr.get('merged', False),
}
def get_pr_diff_files(self, payload: Dict[str, Any]) -> list:
"""
从 Pull Request 事件中获取变更的文件列表
Args:
payload: Webhook payload
Returns:
变更的文件列表
"""
pr = payload.get('pull_request', {})
files = pr.get('changed_files', [])
# 如果没有 changed_files 字段,尝试从 commits 中获取
if not files:
commits = pr.get('commits', [])
files = []
for commit in commits:
files.extend(self.get_changed_files(commit))
return files