524 lines
17 KiB
Python
524 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
|
|
|
|
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 = datetime.utcnow().timestamp() + self.CONFIRMATION_TIMEOUT
|
|
|
|
confirmation = ConfirmationRequest(
|
|
token=confirmation_token,
|
|
merge_request=request,
|
|
created_at=datetime.utcnow().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 datetime.utcnow().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 = (datetime.utcnow() - 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 = (datetime.utcnow() - 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 = datetime.utcnow()
|
|
|
|
# 检查是否需要进入熔断期
|
|
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 = datetime.utcnow().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
|