fix(phase25): 首席架構師 Review R2 修正 (I1/I2/I3/I4/C3/M1)
I1: auto_repair_service — 失敗分支 anti_pattern task 補齊 _pending_tasks GC 防護
C3: drift_remediator — _kubectl_apply() 實作 resource_key 範圍過濾(修復虛設參數 bug)
M1: drift_remediator — _git_push() 標記 DISABLED,防止誤啟用
I2: drift.py — Telegram 通知移除失效的 adopt() 端點連結
I3: drift/page.tsx — handleScan POST body namespace→namespaces(對齊後端 DriftScanRequest)
I4: drift/page.tsx — 移除硬編碼英文字串,改用 t('loading')/t('highCount')/t('mediumCount')
i18n: zh-TW.json + en.json 補齊 drift.loading key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -182,7 +182,7 @@ async def _analyze_and_notify(report: DriftReport) -> None:
|
||||
f"<b>漂移詳情</b>:\n{diff_summary}\n\n"
|
||||
f"Report ID: <code>{report.report_id}</code>\n"
|
||||
f"POST /api/v1/drift/reports/{report.report_id}/rollback — 覆蓋回 Git\n"
|
||||
f"POST /api/v1/drift/reports/{report.report_id}/adopt — 承認變更"
|
||||
f"(adopt 端點暫停開放,待 ADR-057 實作後啟用)"
|
||||
)
|
||||
except Exception as e:
|
||||
import structlog
|
||||
|
||||
@@ -407,15 +407,18 @@ class AutoRepairService:
|
||||
)
|
||||
|
||||
# 2026-04-04 Claude Code: Phase 25 P1 — 失敗修復後 fire-and-forget 生成 ANTI_PATTERN
|
||||
# 2026-04-05 Claude Code: I1 修正 — 補齊 _pending_tasks GC 防護(對稱化)
|
||||
try:
|
||||
from src.services.runbook_generator import get_runbook_generator
|
||||
import asyncio as _asyncio
|
||||
symptoms = self._extract_symptoms(incident)
|
||||
symptoms_hash = symptoms.compute_hash()
|
||||
gen = get_runbook_generator()
|
||||
import asyncio as _asyncio
|
||||
_asyncio.create_task(
|
||||
_ap_task = _asyncio.create_task(
|
||||
gen.generate_anti_pattern(incident, playbook, fail_result, symptoms_hash)
|
||||
)
|
||||
self._pending_tasks.add(_ap_task)
|
||||
_ap_task.add_done_callback(self._pending_tasks.discard)
|
||||
except Exception as _ap_e:
|
||||
logger.warning("anti_pattern_task_failed", error=str(_ap_e))
|
||||
|
||||
|
||||
@@ -166,9 +166,31 @@ class DriftRemediator:
|
||||
# =========================================================================
|
||||
|
||||
def _kubectl_apply(self, namespace: str, resource_key: str | None) -> dict:
|
||||
"""執行 kubectl apply(同步)"""
|
||||
"""
|
||||
執行 kubectl apply(同步)
|
||||
|
||||
2026-04-05 Claude Code: C3 修正 — resource_key 現在實際影響 apply 範圍
|
||||
- resource_key=None: apply 整個 k8s/ 目錄
|
||||
- resource_key="Deployment/api": 只 apply 匹配前綴的 YAML 檔
|
||||
"""
|
||||
try:
|
||||
cmd = ["kubectl", "apply", "-f", self._k8s_dir, "-n", namespace, "--dry-run=none"]
|
||||
if resource_key:
|
||||
# 從 resource_key (e.g. "Deployment/api") 推斷檔名前綴
|
||||
kind_lower = resource_key.split("/")[0].lower() if "/" in resource_key else resource_key.lower()
|
||||
import pathlib
|
||||
k8s_path = pathlib.Path(self._k8s_dir)
|
||||
matched = list(k8s_path.glob(f"*{kind_lower}*.yaml")) + list(k8s_path.glob(f"*{kind_lower}*.yml"))
|
||||
if matched:
|
||||
target = str(matched[0])
|
||||
logger.info("kubectl_apply_targeted", resource_key=resource_key, file=target)
|
||||
else:
|
||||
# 找不到匹配檔案,fallback 整目錄但記錄警告
|
||||
logger.warning("kubectl_apply_no_match_fallback", resource_key=resource_key, k8s_dir=self._k8s_dir)
|
||||
target = self._k8s_dir
|
||||
else:
|
||||
target = self._k8s_dir
|
||||
|
||||
cmd = ["kubectl", "apply", "-f", target, "-n", namespace, "--dry-run=none"]
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
@@ -184,34 +206,18 @@ class DriftRemediator:
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _git_push(self, commit_msg: str) -> dict:
|
||||
"""執行 git add + commit + push gitea(同步)"""
|
||||
try:
|
||||
# git add
|
||||
subprocess.run(["git", "add", "-A"], check=True, timeout=10)
|
||||
# git commit
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", commit_msg],
|
||||
check=True,
|
||||
timeout=10,
|
||||
)
|
||||
# git push gitea main
|
||||
proc = subprocess.run(
|
||||
["git", "push", "gitea", "main"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
return {"success": True, "message": "已推送至 gitea main"}
|
||||
else:
|
||||
return {"success": False, "message": proc.stderr[:500]}
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"success": False, "message": f"git 操作失敗: {e}"}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "message": "git push 超時"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
def _git_push(self, _commit_msg: str) -> dict:
|
||||
"""
|
||||
執行 git add + commit + push gitea(同步)
|
||||
|
||||
2026-04-05 Claude Code: M1 — DISABLED 標記,避免誤啟用
|
||||
adopt() 端點已回傳 501,此方法目前不可到達。
|
||||
ADR-057 起草後改由 Gitea PR API 實作,屆時此方法整體移除。
|
||||
"""
|
||||
# DISABLED: adopt() 端點已返回 501,此方法不應被呼叫
|
||||
# 保留程式碼僅作歷史參考,ADR-057 完成後刪除
|
||||
return {"success": False, "message": "git_push DISABLED — 請參考 ADR-057"}
|
||||
|
||||
|
||||
async def _notify_telegram(self, message: str) -> None:
|
||||
"""推送通知到 Telegram"""
|
||||
|
||||
@@ -934,6 +934,7 @@
|
||||
"subtitle": "GitOps Guardian — Detects drift between K8s actual state and Git YAML",
|
||||
"scan": "Scan Now",
|
||||
"scanning": "Scanning...",
|
||||
"loading": "Loading...",
|
||||
"noReports": "No drift reports yet",
|
||||
"noReportsHint": "CronJob scans hourly automatically, or click \"Scan Now\" to trigger manually",
|
||||
"noDrift": "No Drift",
|
||||
|
||||
@@ -935,6 +935,7 @@
|
||||
"subtitle": "GitOps 守門員 — 偵測 K8s 實際狀態 vs Git YAML 的漂移",
|
||||
"scan": "立即掃描",
|
||||
"scanning": "掃描中...",
|
||||
"loading": "載入中...",
|
||||
"noReports": "目前無漂移報告",
|
||||
"noReportsHint": "CronJob 每小時自動掃描,或點擊「立即掃描」手動觸發",
|
||||
"noDrift": "無漂移",
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function DriftPage({ params }: { params: { locale: string } }) {
|
||||
const res = await fetch(`${getApiBase()}/api/v1/drift/scan`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ namespace: 'awoooi-prod', triggered_by: 'web_manual' }),
|
||||
body: JSON.stringify({ namespaces: ['awoooi-prod'], triggered_by: 'web_manual' }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const data: ScanResult = await res.json()
|
||||
@@ -241,7 +241,7 @@ export default function DriftPage({ params }: { params: { locale: string } }) {
|
||||
{scanResult.summary}
|
||||
{(scanResult.high_count > 0 || scanResult.medium_count > 0) && (
|
||||
<span className="ml-2 text-neutral-500 font-normal">
|
||||
— High: {scanResult.high_count}, Medium: {scanResult.medium_count}
|
||||
— {t('highCount')} {scanResult.high_count}, {t('mediumCount')} {scanResult.medium_count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -265,7 +265,7 @@ export default function DriftPage({ params }: { params: { locale: string } }) {
|
||||
{loading && reports.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-32 text-neutral-400">
|
||||
<RefreshCw size={16} className="animate-spin mr-2" />
|
||||
<span className="text-[12px]">Loading...</span>
|
||||
<span className="text-[12px]">{t('loading')}</span>
|
||||
</div>
|
||||
) : reports.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-48 text-neutral-400">
|
||||
|
||||
Reference in New Issue
Block a user