PMFlow/backend/app/services/auto_merger.py

525 lines
17 KiB
Python

"""
Neural Sync - Auto Merge Service
自动合并服务 - 严格模式
- 审计评分 >= 98 分
- 全量测试通过
- 二次人工确认
- 熔断机制
"""
import asyncio
import logging
from typing import Optional, Dict, List, Any, Tuple
from pydantic import BaseModel, Field
from datetime import datetime, timedelta
from enum import Enum
from ..events import Event, EventType
from ..utils.timezone import now_cn_aware, now_cn_naive
logger = logging.getLogger(__name__)
class MergeStatus(str, Enum):
"""合并状态"""
PENDING = "pending"
WAITING_CONFIRMATION = "waiting_confirmation"
APPROVED = "approved"
REJECTED = "rejected"
FAILED = "failed"
COOLDOWN = "cooldown"
class MergeRequest(BaseModel):
"""合并请求"""
signal_id: str
commit_sha: str
task_name: str
audit_score: float = Field(ge=0.0, le=100.0)
test_results: Dict[str, Any]
requester: Optional[str] = None
class MergeResponse(BaseModel):
"""合并响应"""
status: MergeStatus
success: bool
merge_sha: Optional[str] = None
reason: Optional[str] = None
requires_human_confirmation: bool = False
confirmation_token: Optional[str] = None
cooldown_remaining: Optional[float] = None
details: Dict[str, Any] = Field(default_factory=dict)
class ConfirmationRequest(BaseModel):
"""人工确认请求"""
token: str
merge_request: MergeRequest
created_at: float
expires_at: float
class AutoMergeService:
"""
自动合并服务 - 严格模式
条件:
1. 审计评分 >= 98 分
2. 全量测试通过
3. 二次人工确认
4. 不在熔断期内
特性:
- 熔断机制(连续失败后暂停)
- 人工确认流程
- 回滚支持
- 事件通知
"""
# 严格模式阈值
AUDIT_SCORE_THRESHOLD = 98.0
HUMAN_CONFIRMATION_REQUIRED = True
CONFIRMATION_TIMEOUT = 300 # 5 minutes
# 熔断机制
MAX_CONSECUTIVE_FAILURES = 3
FAILURE_COOLDOWN_SECONDS = 3600 # 1 hour
def __init__(self, git_service=None, plane_service=None, lark_service=None, event_bus=None):
self.git = git_service
self.plane = plane_service
self.lark = lark_service
self.event_bus = event_bus
# 状态跟踪
self._failure_count = 0
self._last_failure_time: Optional[datetime] = None
self._successful_merges = 0
# 待确认的请求
self._pending_confirmations: Dict[str, ConfirmationRequest] = {}
async def try_auto_merge(self, request: MergeRequest) -> MergeResponse:
"""
尝试自动合并
"""
# 1. 检查熔断状态
if self._is_in_cooldown():
return MergeResponse(
status=MergeStatus.COOLDOWN,
success=False,
reason=f"系统处于熔断期,请在 {self._get_cooldown_remaining()}s 后重试",
requires_human_confirmation=True,
cooldown_remaining=self._get_cooldown_remaining(),
)
# 2. 验证审计评分
if request.audit_score < self.AUDIT_SCORE_THRESHOLD:
return MergeResponse(
status=MergeStatus.REJECTED,
success=False,
reason=f"审计评分 {request.audit_score} 低于阈值 {self.AUDIT_SCORE_THRESHOLD}",
requires_human_confirmation=True,
)
# 3. 验证测试结果
if not self._all_tests_passed(request.test_results):
return MergeResponse(
status=MergeStatus.REJECTED,
success=False,
reason="测试未全部通过",
requires_human_confirmation=True,
details={
"test_results": request.test_results,
"failed_tests": self._get_failed_tests(request.test_results),
},
)
# 4. 创建人工确认请求
if self.HUMAN_CONFIRMATION_REQUIRED:
confirmation_token = self._generate_confirmation_token()
expires_at = now_cn_aware().timestamp() + self.CONFIRMATION_TIMEOUT
confirmation = ConfirmationRequest(
token=confirmation_token,
merge_request=request,
created_at=now_cn_aware().timestamp(),
expires_at=expires_at,
)
self._pending_confirmations[confirmation_token] = confirmation
# 发送确认请求事件
await self._notify_confirmation_request(confirmation)
return MergeResponse(
status=MergeStatus.WAITING_CONFIRMATION,
success=False,
reason="等待人工二次确认",
requires_human_confirmation=True,
confirmation_token=confirmation_token,
details={
"expires_at": expires_at,
"timeout_seconds": self.CONFIRMATION_TIMEOUT,
},
)
# 5. 直接执行自动合并(不需要确认)
return await self._execute_merge(request)
async def confirm_merge(self, confirmation_token: str, requester: str) -> MergeResponse:
"""
人工确认合并
"""
confirmation = self._pending_confirmations.get(confirmation_token)
if not confirmation:
return MergeResponse(
status=MergeStatus.REJECTED,
success=False,
reason="确认令牌无效或已过期",
)
# 检查是否过期
if now_cn_aware().timestamp() > confirmation.expires_at:
del self._pending_confirmations[confirmation_token]
return MergeResponse(
status=MergeStatus.REJECTED,
success=False,
reason="确认请求已过期",
)
# 执行合并
result = await self._execute_merge(confirmation.merge_request)
# 清理确认请求
if result.success:
del self._pending_confirmations[confirmation_token]
return result
async def reject_merge(self, confirmation_token: str, reason: str) -> MergeResponse:
"""
拒绝合并
"""
confirmation = self._pending_confirmations.get(confirmation_token)
if not confirmation:
return MergeResponse(
status=MergeStatus.REJECTED,
success=False,
reason="确认令牌无效",
)
# 清理确认请求
del self._pending_confirmations[confirmation_token]
return MergeResponse(
status=MergeStatus.REJECTED,
success=False,
reason=f"合并被拒绝: {reason}",
)
async def _execute_merge(self, request: MergeRequest) -> MergeResponse:
"""
执行实际的合并操作
"""
try:
# 1. 执行 Git 合并
merge_sha = await self._perform_git_merge(request.commit_sha)
# 2. 推送到远程
await self._push_to_remote()
# 3. 更新 Plane 任务状态
if self.plane:
await self._update_plane_status(request.signal_id, merge_sha)
# 4. 发送飞书通知
if self.lark:
await self._send_lark_notification(request, merge_sha)
# 5. 发送成功事件
await self._publish_merge_success_event(request, merge_sha)
# 重置失败计数
self._reset_failure_count()
self._successful_merges += 1
return MergeResponse(
status=MergeStatus.APPROVED,
success=True,
merge_sha=merge_sha,
reason="自动合并成功",
details={
"successful_merges": self._successful_merges,
},
)
except Exception as e:
logger.error(f"Merge failed: {e}", exc_info=True)
self._record_failure()
await self._publish_merge_failed_event(request, str(e))
return MergeResponse(
status=MergeStatus.FAILED,
success=False,
reason=f"合并失败: {str(e)}",
requires_human_confirmation=True,
)
async def _perform_git_merge(self, commit_sha: str) -> str:
"""执行 Git 合并"""
# 如果有 git 服务,使用它;否则使用 subprocess
if self.git:
return await self.git.merge(commit_sha)
# 使用 subprocess 执行 git 命令
import subprocess
# 合并提交(不创建合并提交,直接快进)
result = subprocess.run(
["git", "merge", "--ff", commit_sha],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
# 尝试 cherry-pick
result = subprocess.run(
["git", "cherry-pick", commit_sha],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
raise Exception(f"Git merge/cherry-pick failed: {result.stderr}")
# 获取合并后的 HEAD
result = subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
async def _push_to_remote(self):
"""推送到远程仓库"""
import subprocess
result = subprocess.run(
["git", "push"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
raise Exception(f"Git push failed: {result.stderr}")
async def _update_plane_status(self, signal_id: str, merge_sha: str):
"""更新 Plane 任务状态"""
if not self.plane:
return
try:
# 这里应该根据 signal_id 找到对应的 Plane issue
# 简化实现:假设任务 ID 已知
await self.plane.update_issue_status(
signal_id, # 或提取的任务 ID
status="Done"
)
except Exception as e:
logger.warning(f"Failed to update Plane status: {e}")
async def _send_lark_notification(self, request: MergeRequest, merge_sha: str):
"""发送飞书通知"""
if not self.lark:
return
try:
await self.lark.send_merge_notification(
task_name=request.task_name,
merge_sha=merge_sha,
audit_score=request.audit_score,
)
except Exception as e:
logger.warning(f"Failed to send Lark notification: {e}")
async def _notify_confirmation_request(self, confirmation: ConfirmationRequest):
"""通知确认请求"""
if self.event_bus:
from .events import EventType
await self.event_bus.publish(
Event(
type=EventType.AUTO_MERGE_REQUESTED,
data={
"token": confirmation.token,
"task_name": confirmation.merge_request.task_name,
"commit_sha": confirmation.merge_request.commit_sha,
"audit_score": confirmation.merge_request.audit_score,
"expires_at": confirmation.expires_at,
},
)
)
async def _publish_merge_success_event(self, request: MergeRequest, merge_sha: str):
"""发布合并成功事件"""
if self.event_bus:
from .events import EventType
await self.event_bus.publish(
Event(
type=EventType.AUTO_MERGE_SUCCESS,
data={
"signal_id": request.signal_id,
"commit_sha": request.commit_sha,
"merge_sha": merge_sha,
"task_name": request.task_name,
"audit_score": request.audit_score,
},
)
)
async def _publish_merge_failed_event(self, request: MergeRequest, error: str):
"""发布合并失败事件"""
if self.event_bus:
from .events import EventType
await self.event_bus.publish(
Event(
type=EventType.AUTO_MERGE_FAILED,
data={
"signal_id": request.signal_id,
"commit_sha": request.commit_sha,
"task_name": request.task_name,
"error": error,
},
)
)
def _is_in_cooldown(self) -> bool:
"""检查是否在熔断期"""
if self._last_failure_time is None:
return False
elapsed = (now_cn_naive() - self._last_failure_time).total_seconds()
return elapsed < self.FAILURE_COOLDOWN_SECONDS
def _get_cooldown_remaining(self) -> float:
"""获取熔断剩余时间"""
if self._last_failure_time is None:
return 0.0
elapsed = (now_cn_naive() - self._last_failure_time).total_seconds()
return max(0, self.FAILURE_COOLDOWN_SECONDS - elapsed)
def _all_tests_passed(self, test_results: Dict[str, Any]) -> bool:
"""检查所有测试是否通过"""
if not test_results:
return True # 没有测试结果视为通过
# 检查是否有失败的测试
for test_name, result in test_results.items():
if isinstance(result, dict):
if result.get("status") == "failed":
return False
if result.get("passed") is False:
return False
elif isinstance(result, bool):
if not result:
return False
return True
def _get_failed_tests(self, test_results: Dict[str, Any]) -> List[str]:
"""获取失败的测试列表"""
failed = []
for test_name, result in test_results.items():
if isinstance(result, dict) and result.get("status") == "failed":
failed.append(test_name)
elif isinstance(result, bool) and not result:
failed.append(test_name)
return failed
def _generate_confirmation_token(self) -> str:
"""生成确认令牌"""
import uuid
return str(uuid.uuid4())
def _record_failure(self):
"""记录失败"""
self._failure_count += 1
self._last_failure_time = now_cn_naive()
# 检查是否需要进入熔断期
if self._failure_count >= self.MAX_CONSECUTIVE_FAILURES:
logger.warning(
f"[AutoMerge] {self._failure_count} consecutive failures. "
f"Entering cooldown for {self.FAILURE_COOLDOWN_SECONDS}s"
)
if self.event_bus:
import asyncio
asyncio.create_task(
self.event_bus.publish(
Event(
type=EventType.AUTO_MERGE_COOLDOWN,
data={
"failure_count": self._failure_count,
"cooldown_seconds": self.FAILURE_COOLDOWN_SECONDS,
},
)
)
)
def _reset_failure_count(self):
"""重置失败计数"""
self._failure_count = 0
self._last_failure_time = None
async def get_pending_confirmations(self) -> List[ConfirmationRequest]:
"""获取待确认的请求"""
# 清理过期的确认请求
now = now_cn_aware().timestamp()
expired = [
token for token, conf in self._pending_confirmations.items()
if now > conf.expires_at
]
for token in expired:
del self._pending_confirmations[token]
return list(self._pending_confirmations.values())
def get_status(self) -> Dict[str, Any]:
"""获取服务状态"""
return {
"audit_threshold": self.AUDIT_SCORE_THRESHOLD,
"human_confirmation_required": self.HUMAN_CONFIRMATION_REQUIRED,
"failure_count": self._failure_count,
"successful_merges": self._successful_merges,
"in_cooldown": self._is_in_cooldown(),
"cooldown_remaining": self._get_cooldown_remaining() if self._is_in_cooldown() else 0,
"pending_confirmations": len(self._pending_confirmations),
}
# 全局服务实例
_auto_merger = None
def get_auto_merger(
git_service=None,
plane_service=None,
lark_service=None,
event_bus=None,
) -> AutoMergeService:
"""获取自动合并服务实例"""
global _auto_merger
if _auto_merger is None:
_auto_merger = AutoMergeService(
git_service=git_service,
plane_service=plane_service,
lark_service=lark_service,
event_bus=event_bus,
)
return _auto_merger