""" CI Auto-Repair Service - Phase 13.1 #78 ======================================== CI 失敗自動修復服務,根據風險分級決定執行策略 策略: - LOW: 自動執行修復 (如重啟 Runner、清理快取) - MEDIUM: 發送 Telegram 確認,快速批准後執行 - HIGH: 建立 Approval,等待人工審核 - CRITICAL: 禁止自動修復,僅通知 整合: - Intent Classifier: 判斷修復意圖類型 - Complexity Scorer: 評估修復複雜度 - AI Router: 選擇最適 AI 進行分析 版本: v1.0 建立: 2026-03-26 16:50 (台北時區) 建立者: Claude Code """ from __future__ import annotations from dataclasses import dataclass from enum import Enum import structlog from src.services.complexity_scorer import get_complexity_scorer from src.services.intent_classifier import IntentType, RiskLevel, get_intent_classifier logger = structlog.get_logger(__name__) # ============================================================================= # Types # ============================================================================= class RepairAction(Enum): """修復動作類型""" RESTART_RUNNER = "restart_runner" CLEAR_CACHE = "clear_cache" RETRY_WORKFLOW = "retry_workflow" ROLLBACK_COMMIT = "rollback_commit" FIX_CONFIG = "fix_config" FIX_DEPENDENCY = "fix_dependency" SCALE_RESOURCE = "scale_resource" MANUAL_REQUIRED = "manual_required" class ExecutionDecision(Enum): """執行決策""" AUTO_EXECUTE = "auto_execute" # 直接自動執行 TELEGRAM_CONFIRM = "telegram_confirm" # Telegram 快速確認 APPROVAL_REQUIRED = "approval_required" # 建立 Approval 等待審核 BLOCKED = "blocked" # 禁止執行,僅通知 @dataclass class RepairRecommendation: """修復建議""" action: RepairAction command: str | None reason: str risk_level: RiskLevel execution_decision: ExecutionDecision confidence: float estimated_duration_seconds: int rollback_command: str | None = None @dataclass class CIRepairDecision: """CI 修復決策結果""" should_repair: bool execution_decision: ExecutionDecision recommendations: list[RepairRecommendation] risk_level: RiskLevel complexity_score: int intent_type: IntentType reason: str metadata: dict # ============================================================================= # Repair Strategy Mapping # ============================================================================= # 錯誤類型 → 修復動作映射 ERROR_TYPE_REPAIR_MAP: dict[str, list[RepairAction]] = { "build": [RepairAction.CLEAR_CACHE, RepairAction.FIX_DEPENDENCY], "test": [RepairAction.RETRY_WORKFLOW, RepairAction.FIX_CONFIG], "lint": [RepairAction.RETRY_WORKFLOW], "deploy": [RepairAction.ROLLBACK_COMMIT, RepairAction.FIX_CONFIG], "timeout": [RepairAction.RESTART_RUNNER, RepairAction.SCALE_RESOURCE], "runner": [RepairAction.RESTART_RUNNER], "dependency": [RepairAction.CLEAR_CACHE, RepairAction.FIX_DEPENDENCY], "unknown": [RepairAction.MANUAL_REQUIRED], } # 修復動作 → 風險等級映射 ACTION_RISK_MAP: dict[RepairAction, RiskLevel] = { RepairAction.RETRY_WORKFLOW: RiskLevel.LOW, RepairAction.CLEAR_CACHE: RiskLevel.LOW, RepairAction.RESTART_RUNNER: RiskLevel.LOW, RepairAction.FIX_CONFIG: RiskLevel.MEDIUM, RepairAction.FIX_DEPENDENCY: RiskLevel.MEDIUM, RepairAction.SCALE_RESOURCE: RiskLevel.MEDIUM, RepairAction.ROLLBACK_COMMIT: RiskLevel.HIGH, RepairAction.MANUAL_REQUIRED: RiskLevel.CRITICAL, } # 風險等級 → 執行決策映射 RISK_EXECUTION_MAP: dict[RiskLevel, ExecutionDecision] = { RiskLevel.LOW: ExecutionDecision.AUTO_EXECUTE, RiskLevel.MEDIUM: ExecutionDecision.TELEGRAM_CONFIRM, RiskLevel.HIGH: ExecutionDecision.APPROVAL_REQUIRED, RiskLevel.CRITICAL: ExecutionDecision.BLOCKED, } # ============================================================================= # CI Auto-Repair Service # ============================================================================= class CIAutoRepairService: """ CI 自動修復服務 整合智能路由 (Phase 13.3) 進行風險評估和修復決策 """ def __init__(self): self._intent_classifier = get_intent_classifier() self._complexity_scorer = get_complexity_scorer() async def evaluate_repair( self, error_type: str, workflow_name: str, repo: str, failure_context: dict, diagnosis_summary: str | None = None, ) -> CIRepairDecision: """ 評估 CI 失敗的修復策略 Args: error_type: 錯誤類型 (build/test/lint/deploy/timeout) workflow_name: Workflow 名稱 repo: 倉庫名稱 failure_context: 失敗上下文 diagnosis_summary: AI 診斷摘要 (可選) Returns: CIRepairDecision: 修復決策 """ logger.info( "ci_repair_evaluation_started", error_type=error_type, workflow_name=workflow_name, repo=repo, ) # 1. 構建分析文字 analysis_text = self._build_analysis_text( error_type=error_type, workflow_name=workflow_name, diagnosis_summary=diagnosis_summary, ) # 2. 意圖分類 # 2026-04-27 P3.1-T3 by Claude — 修復缺失 await(classify 是 async method) intent_result = await self._intent_classifier.classify(analysis_text) # 3. 複雜度評估 complexity_result = self._complexity_scorer.score( text=analysis_text, context={ "error_type": error_type, "workflow_name": workflow_name, "repo": repo, **failure_context, }, ) # 4. 獲取可能的修復動作 possible_actions = ERROR_TYPE_REPAIR_MAP.get( error_type.lower(), [RepairAction.MANUAL_REQUIRED], ) # 5. 生成修復建議 recommendations = self._generate_recommendations( possible_actions=possible_actions, error_type=error_type, workflow_name=workflow_name, complexity_score=complexity_result.score, ) # 6. 決定整體風險等級和執行策略 overall_risk = self._determine_overall_risk( recommendations=recommendations, intent_risk=intent_result.risk_level, complexity_score=complexity_result.score, ) execution_decision = RISK_EXECUTION_MAP.get( overall_risk, ExecutionDecision.APPROVAL_REQUIRED, ) # 7. 特殊規則覆蓋 execution_decision = self._apply_special_rules( execution_decision=execution_decision, error_type=error_type, workflow_name=workflow_name, repo=repo, ) decision = CIRepairDecision( should_repair=execution_decision != ExecutionDecision.BLOCKED, execution_decision=execution_decision, recommendations=recommendations, risk_level=overall_risk, complexity_score=complexity_result.score, intent_type=intent_result.intent, reason=self._generate_decision_reason( execution_decision=execution_decision, overall_risk=overall_risk, error_type=error_type, ), metadata={ "intent_confidence": intent_result.confidence, "complexity_factors": complexity_result.factors, "workflow_name": workflow_name, "repo": repo, }, ) logger.info( "ci_repair_evaluation_completed", should_repair=decision.should_repair, execution_decision=execution_decision.value, risk_level=overall_risk.value, recommendations_count=len(recommendations), ) return decision def _build_analysis_text( self, error_type: str, workflow_name: str, diagnosis_summary: str | None, ) -> str: """構建意圖分類用的分析文字""" parts = [ f"CI workflow '{workflow_name}' failed", f"Error type: {error_type}", ] if diagnosis_summary: parts.append(f"Diagnosis: {diagnosis_summary}") return ". ".join(parts) def _generate_recommendations( self, possible_actions: list[RepairAction], error_type: str, workflow_name: str, complexity_score: int, ) -> list[RepairRecommendation]: """生成修復建議列表""" recommendations = [] for action in possible_actions: risk = ACTION_RISK_MAP.get(action, RiskLevel.HIGH) # 根據複雜度調整風險 if complexity_score >= 4: risk = RiskLevel.HIGH if risk == RiskLevel.MEDIUM else risk command, rollback = self._get_repair_command( action=action, workflow_name=workflow_name, ) recommendations.append(RepairRecommendation( action=action, command=command, reason=self._get_action_reason(action, error_type), risk_level=risk, execution_decision=RISK_EXECUTION_MAP.get(risk, ExecutionDecision.APPROVAL_REQUIRED), confidence=self._calculate_confidence(action, error_type), estimated_duration_seconds=self._estimate_duration(action), rollback_command=rollback, )) # 按風險等級排序 (低風險優先) risk_order = {RiskLevel.LOW: 0, RiskLevel.MEDIUM: 1, RiskLevel.HIGH: 2, RiskLevel.CRITICAL: 3} recommendations.sort(key=lambda r: risk_order.get(r.risk_level, 99)) return recommendations def _get_repair_command( self, action: RepairAction, workflow_name: str, ) -> tuple[str | None, str | None]: """獲取修復指令和回滾指令""" commands: dict[RepairAction, tuple[str | None, str | None]] = { RepairAction.RETRY_WORKFLOW: ( f"gh workflow run {workflow_name}", None, ), RepairAction.CLEAR_CACHE: ( "gh cache delete --all", None, ), RepairAction.RESTART_RUNNER: ( "sudo systemctl restart actions.runner.*", None, ), RepairAction.SCALE_RESOURCE: ( "kubectl scale deployment/actions-runner --replicas=3", "kubectl scale deployment/actions-runner --replicas=2", ), RepairAction.ROLLBACK_COMMIT: ( "git revert HEAD --no-edit && git push", "git revert HEAD --no-edit && git push", ), RepairAction.FIX_CONFIG: ( None, # 需要 AI 生成具體指令 None, ), RepairAction.FIX_DEPENDENCY: ( "pnpm install --force && uv sync", None, ), RepairAction.MANUAL_REQUIRED: ( None, None, ), } return commands.get(action, (None, None)) def _get_action_reason(self, action: RepairAction, error_type: str) -> str: """獲取修復動作的原因說明""" reasons = { RepairAction.RETRY_WORKFLOW: f"Retry workflow to recover from transient {error_type} failure", RepairAction.CLEAR_CACHE: "Clear build/dependency cache to resolve potential cache corruption", RepairAction.RESTART_RUNNER: "Restart GitHub Actions runner to recover from runner issues", RepairAction.SCALE_RESOURCE: "Scale runner resources to handle timeout issues", RepairAction.ROLLBACK_COMMIT: "Rollback recent commit that may have introduced the failure", RepairAction.FIX_CONFIG: "Fix configuration that may be causing the failure", RepairAction.FIX_DEPENDENCY: "Update or fix dependencies to resolve compatibility issues", RepairAction.MANUAL_REQUIRED: "Manual investigation required due to complex failure", } return reasons.get(action, "Unknown action") def _calculate_confidence(self, _action: RepairAction, _error_type: str) -> float: """ 計算修復信心度 🔴 2026-03-29 修正: 規則匹配不是 AI 分析,統一返回 0.0 根據 feedback_confidence_truthfulness.md 鐵律 """ # 規則匹配/規則引擎判斷,非 AI 分析 return 0.0 def _estimate_duration(self, action: RepairAction) -> int: """估算修復時間 (秒)""" durations = { RepairAction.RETRY_WORKFLOW: 300, # 5 分鐘 RepairAction.CLEAR_CACHE: 30, RepairAction.RESTART_RUNNER: 60, RepairAction.SCALE_RESOURCE: 120, RepairAction.ROLLBACK_COMMIT: 180, RepairAction.FIX_CONFIG: 600, RepairAction.FIX_DEPENDENCY: 300, RepairAction.MANUAL_REQUIRED: 3600, } return durations.get(action, 300) def _determine_overall_risk( self, recommendations: list[RepairRecommendation], intent_risk: RiskLevel, complexity_score: int, ) -> RiskLevel: """決定整體風險等級""" if not recommendations: return RiskLevel.CRITICAL # 取最低風險的建議作為基礎 min_risk = min( recommendations, key=lambda r: {RiskLevel.LOW: 0, RiskLevel.MEDIUM: 1, RiskLevel.HIGH: 2, RiskLevel.CRITICAL: 3}.get(r.risk_level, 99), ).risk_level # 如果複雜度高,提升風險等級 if complexity_score >= 4 and min_risk == RiskLevel.LOW: min_risk = RiskLevel.MEDIUM elif complexity_score >= 5 and min_risk == RiskLevel.MEDIUM: min_risk = RiskLevel.HIGH # 如果意圖分類顯示高風險,取較高值 risk_order = {RiskLevel.LOW: 0, RiskLevel.MEDIUM: 1, RiskLevel.HIGH: 2, RiskLevel.CRITICAL: 3} if risk_order.get(intent_risk, 0) > risk_order.get(min_risk, 0): return intent_risk return min_risk def _apply_special_rules( self, execution_decision: ExecutionDecision, error_type: str, workflow_name: str, repo: str, ) -> ExecutionDecision: """應用特殊規則覆蓋""" # 生產部署相關的 workflow 強制需要審核 production_keywords = ["prod", "production", "release", "deploy"] if any(kw in workflow_name.lower() for kw in production_keywords): if execution_decision == ExecutionDecision.AUTO_EXECUTE: return ExecutionDecision.TELEGRAM_CONFIRM # rollback 錯誤類型強制需要審核 if error_type == "deploy": if execution_decision in (ExecutionDecision.AUTO_EXECUTE, ExecutionDecision.TELEGRAM_CONFIRM): return ExecutionDecision.APPROVAL_REQUIRED return execution_decision def _generate_decision_reason( self, execution_decision: ExecutionDecision, overall_risk: RiskLevel, error_type: str, ) -> str: """生成決策原因說明""" reasons = { ExecutionDecision.AUTO_EXECUTE: f"Low risk {error_type} failure, safe for auto-repair", ExecutionDecision.TELEGRAM_CONFIRM: f"Medium risk {error_type} failure, quick Telegram confirmation recommended", ExecutionDecision.APPROVAL_REQUIRED: f"High risk {error_type} failure, human approval required before repair", ExecutionDecision.BLOCKED: f"Critical {error_type} failure, auto-repair blocked for safety", } return reasons.get(execution_decision, "Unknown decision") # ============================================================================= # Singleton # ============================================================================= _ci_auto_repair_service: CIAutoRepairService | None = None def get_ci_auto_repair_service() -> CIAutoRepairService: """取得全域 CI Auto-Repair Service 實例""" global _ci_auto_repair_service if _ci_auto_repair_service is None: _ci_auto_repair_service = CIAutoRepairService() return _ci_auto_repair_service