diff --git a/app.py b/app.py index 14b50b7..0de1f35 100644 --- a/app.py +++ b/app.py @@ -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(): """手动触发扫描接口""" diff --git a/notify/feishu.py b/notify/feishu.py index 5ca5e2a..69a289f 100644 --- a/notify/feishu.py +++ b/notify/feishu.py @@ -147,20 +147,41 @@ class FeishuNotifier: summary = result.get('summary', {}) files_scanned = result.get('files_scanned', 0) total = summary.get('total', 0) - + if total > 0: detail_text = f"{tool_name}: 扫描 {files_scanned} 个文件,发现 {total} 个问题" else: detail_text = f"{tool_name}: 扫描 {files_scanned} 个文件,无问题" - + 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 } }, { diff --git a/report/generator.py b/report/generator.py index 447f436..1b674f7 100644 --- a/report/generator.py +++ b/report/generator.py @@ -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} |') diff --git a/webhook/handler.py b/webhook/handler.py index 7d32c26..4a054f3 100644 --- a/webhook/handler.py +++ b/webhook/handler.py @@ -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 \ No newline at end of file