710 lines
25 KiB
Python
710 lines
25 KiB
Python
"""
|
||
AWOOOI API - Gitea Webhook Service
|
||
====================================
|
||
ADR-059: GitHub → Gitea Webhook 遷移
|
||
|
||
整合流程:
|
||
1. Gitea Webhook (PR/Push) → AWOOOI API
|
||
2. HMAC-SHA256 簽章驗證 (X-Gitea-Signature)
|
||
3. 解析 PR diff / Push commits
|
||
4. 呼叫 OpenClaw 進行 AI 代碼審查
|
||
5. 儲存審查結果到 Redis
|
||
6. 發送 Telegram 通知
|
||
7. (可選) 建立 Approval 等待人工確認
|
||
|
||
支援事件:
|
||
- pull_request: PR 代碼審查
|
||
- push: 主分支推送審查
|
||
|
||
版本: v1.0
|
||
最後修改: 2026-04-05 (台北時區)
|
||
修改者: Claude Code (ADR-059 GitHub → Gitea 遷移)
|
||
"""
|
||
|
||
import json
|
||
import uuid
|
||
from typing import Protocol
|
||
|
||
import structlog
|
||
from pydantic import BaseModel, Field
|
||
|
||
from src.core.config import settings
|
||
from src.core.redis_client import get_redis
|
||
from src.utils.timezone import now_taipei_iso
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
# Redis TTL: 7 天
|
||
GITEA_REVIEW_TTL_SECONDS = 7 * 24 * 60 * 60
|
||
|
||
|
||
# =============================================================================
|
||
# Result Models (Service 層數據契約)
|
||
# =============================================================================
|
||
|
||
class CodeReviewResult(BaseModel):
|
||
"""AI 代碼審查結果"""
|
||
summary: str = Field(..., description="審查摘要")
|
||
issues: list[dict] = Field(default=[], description="發現的問題列表")
|
||
suggestions: list[dict] = Field(default=[], description="改進建議")
|
||
security_concerns: list[str] = Field(default=[], description="安全疑慮")
|
||
quality_score: float = Field(..., ge=0, le=100, description="代碼品質分數 0-100")
|
||
analyzed_by: str = Field(..., description="分析模型 (ollama/claude)")
|
||
confidence: float = Field(..., ge=0, le=1, description="分析信心度 0-1")
|
||
|
||
|
||
|
||
# =============================================================================
|
||
# Repository Interface & Implementation
|
||
# =============================================================================
|
||
|
||
class IGiteaReviewRepository(Protocol):
|
||
"""Gitea Review Repository Interface"""
|
||
|
||
async def save_review(self, review_id: str, review_data: dict) -> bool:
|
||
"""儲存審查結果"""
|
||
...
|
||
|
||
async def get_review(self, review_id: str) -> dict | None:
|
||
"""取得審查結果"""
|
||
...
|
||
|
||
|
||
class GiteaReviewRedisRepository:
|
||
"""Redis 實作的 Gitea Review Repository"""
|
||
|
||
KEY_PREFIX = "gitea_review:"
|
||
|
||
async def save_review(self, review_id: str, review_data: dict) -> bool:
|
||
"""
|
||
儲存審查結果到 Redis
|
||
|
||
Args:
|
||
review_id: 審查 ID
|
||
review_data: 審查結果資料
|
||
|
||
Returns:
|
||
bool: 儲存是否成功
|
||
"""
|
||
try:
|
||
redis_client = get_redis()
|
||
key = f"{self.KEY_PREFIX}{review_id}"
|
||
await redis_client.set(
|
||
key,
|
||
json.dumps(review_data, ensure_ascii=False),
|
||
ex=GITEA_REVIEW_TTL_SECONDS,
|
||
)
|
||
logger.debug("gitea_review_saved", review_id=review_id)
|
||
return True
|
||
except Exception as e:
|
||
logger.error("gitea_review_save_failed", review_id=review_id, error=str(e))
|
||
return False
|
||
|
||
async def get_review(self, review_id: str) -> dict | None:
|
||
"""
|
||
取得審查結果
|
||
|
||
Args:
|
||
review_id: 審查 ID
|
||
|
||
Returns:
|
||
審查結果資料,或 None
|
||
"""
|
||
try:
|
||
redis_client = get_redis()
|
||
key = f"{self.KEY_PREFIX}{review_id}"
|
||
result = await redis_client.get(key)
|
||
if result:
|
||
return json.loads(result)
|
||
return None
|
||
except Exception as e:
|
||
logger.error("gitea_review_get_failed", review_id=review_id, error=str(e))
|
||
return None
|
||
|
||
|
||
# =============================================================================
|
||
# Service
|
||
# =============================================================================
|
||
|
||
class GiteaWebhookService:
|
||
"""
|
||
Gitea Webhook 服務
|
||
|
||
封裝審查結果的儲存、查詢以及全部業務協調流程:
|
||
- PR 代碼審查 (review_pull_request)
|
||
- Push 代碼審查 (review_push)
|
||
- OpenClaw 呼叫封裝
|
||
- Telegram 通知
|
||
- Approval 建立
|
||
"""
|
||
|
||
def __init__(self, repository: IGiteaReviewRepository | None = None):
|
||
self._repository = repository or GiteaReviewRedisRepository()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Redis CRUD
|
||
# ------------------------------------------------------------------
|
||
|
||
async def save_review_result(
|
||
self,
|
||
review_id: str,
|
||
review_data: dict,
|
||
ttl: int | None = None,
|
||
) -> bool:
|
||
"""儲存審查結果 (支援自訂 TTL)"""
|
||
if ttl is not None and ttl != GITEA_REVIEW_TTL_SECONDS:
|
||
# 直接寫 Redis 以使用自訂 TTL
|
||
try:
|
||
redis_client = get_redis()
|
||
key = f"{self._repository.KEY_PREFIX}{review_id}" # type: ignore[attr-defined]
|
||
await redis_client.set(
|
||
key,
|
||
json.dumps(review_data, ensure_ascii=False),
|
||
ex=ttl,
|
||
)
|
||
logger.debug("gitea_review_saved_custom_ttl", review_id=review_id, ttl=ttl)
|
||
return True
|
||
except Exception as e:
|
||
logger.error("gitea_review_save_failed", review_id=review_id, error=str(e))
|
||
return False
|
||
return await self._repository.save_review(review_id, review_data)
|
||
|
||
async def get_review_result(self, review_id: str) -> dict | None:
|
||
"""取得審查結果"""
|
||
return await self._repository.get_review(review_id)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Internal helpers: OpenClaw
|
||
# ------------------------------------------------------------------
|
||
|
||
async def _fetch_pr_diff(self, diff_url: str) -> str:
|
||
"""取得 PR diff 內容 (直接 HTTP GET Gitea diff URL)"""
|
||
try:
|
||
import httpx
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
response = await client.get(diff_url)
|
||
if response.status_code == 200:
|
||
content = response.text
|
||
if len(content) > 50000:
|
||
content = content[:50000] + "\n... (truncated)"
|
||
return content
|
||
logger.warning("gitea_diff_fetch_failed", url=diff_url, status=response.status_code)
|
||
return ""
|
||
except Exception as e:
|
||
logger.warning("gitea_diff_fetch_error", url=diff_url, error=str(e))
|
||
return ""
|
||
|
||
async def _call_openclaw_code_review(
|
||
self,
|
||
repo_name: str,
|
||
pr_title: str,
|
||
pr_body: str,
|
||
diff_content: str,
|
||
changed_files: int,
|
||
additions: int,
|
||
deletions: int,
|
||
) -> CodeReviewResult | None:
|
||
"""
|
||
呼叫 OpenClaw 進行 PR 代碼審查
|
||
|
||
優先使用 Ollama (本地,零成本)
|
||
Fallback: Claude (大型 PR)
|
||
|
||
Phase 22 P0 修復: 使用 OpenClawHttpService (2026-03-31)
|
||
"""
|
||
try:
|
||
from src.services.local_code_review_service import get_local_code_review_service
|
||
svc = get_local_code_review_service()
|
||
pr_id = f"{repo_name}-{pr_title[:20]}"
|
||
result = await svc.review_pr(
|
||
pr_id=pr_id,
|
||
repo=repo_name,
|
||
title=pr_title,
|
||
diff=diff_content,
|
||
)
|
||
if result and isinstance(result, dict):
|
||
review_text = result.get("review_text", "")
|
||
issues_count = result.get("issues_count", 0)
|
||
return CodeReviewResult(
|
||
summary=review_text[:300] if review_text else "PR review completed",
|
||
issues=[{"text": line} for line in review_text.split("\n") if "⚠️" in line],
|
||
suggestions=[],
|
||
security_concerns=[],
|
||
quality_score=max(0.0, 100.0 - issues_count * 15),
|
||
analyzed_by=result.get("model", "ollama"),
|
||
confidence=0.7,
|
||
)
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.exception("openclaw_code_review_error", error=str(e))
|
||
return None
|
||
|
||
async def _call_openclaw_push_review(
|
||
self,
|
||
repo_name: str,
|
||
ref: str,
|
||
commits: list[dict],
|
||
files_changed: dict,
|
||
) -> CodeReviewResult | None:
|
||
"""
|
||
Phase 32 ADR-067: Ollama qwen2.5-coder:7b push 代碼審查
|
||
2026-04-10 Claude Sonnet 4.6: OpenClaw 無 push-review 端點 → 改用 Ollama 本地推理
|
||
"""
|
||
try:
|
||
from src.services.local_code_review_service import get_local_code_review_service
|
||
svc = get_local_code_review_service()
|
||
|
||
branch = ref.split("/")[-1]
|
||
commit_msgs = "\n".join(f'- {c["sha"]}: {c["message"]}' for c in commits[:5])
|
||
added = files_changed.get("added", [])
|
||
modified = files_changed.get("modified", [])
|
||
removed = files_changed.get("removed", [])
|
||
files_summary = (
|
||
f"Added: {', '.join(added[:5]) or '無'}\n"
|
||
f"Modified: {', '.join(modified[:5]) or '無'}\n"
|
||
f"Removed: {', '.join(removed[:5]) or '無'}"
|
||
)
|
||
|
||
result = await svc.review_push(
|
||
repo_name=repo_name,
|
||
branch=branch,
|
||
commit_summary=commit_msgs,
|
||
files_summary=files_summary,
|
||
)
|
||
# local_code_review_service 回傳 dict,需轉成 CodeReviewResult
|
||
if result and isinstance(result, dict):
|
||
issues_count = result.get("issues_count", 0)
|
||
review_text = result.get("review_text", "")
|
||
return CodeReviewResult(
|
||
summary=review_text[:300] if review_text else "Push review completed",
|
||
issues=[{"text": line} for line in review_text.split("\n") if "⚠️" in line],
|
||
suggestions=[],
|
||
security_concerns=[],
|
||
quality_score=max(0.0, 100.0 - issues_count * 15),
|
||
analyzed_by=result.get("model", "ollama"),
|
||
confidence=0.7,
|
||
)
|
||
return result # type: ignore[return-value] # None case
|
||
|
||
except Exception as e:
|
||
logger.exception("openclaw_push_review_error", error=str(e))
|
||
return None
|
||
|
||
# ------------------------------------------------------------------
|
||
# Internal helpers: persist
|
||
# ------------------------------------------------------------------
|
||
|
||
async def _save_review_with_analysis(
|
||
self,
|
||
review_id: str,
|
||
event_type: str,
|
||
repo: str,
|
||
target: str,
|
||
analysis: CodeReviewResult | None,
|
||
metadata: dict,
|
||
) -> None:
|
||
"""
|
||
組裝並儲存代碼審查結果到 Redis (透過 Service)
|
||
|
||
Key: gitea_review:{review_id}
|
||
TTL: 7 天
|
||
"""
|
||
result = {
|
||
"review_id": review_id,
|
||
"event_type": event_type,
|
||
"repo": repo,
|
||
"target": target,
|
||
"created_at": now_taipei_iso(),
|
||
# 2026-04-12 ogt: _call_openclaw_push_review 回傳 dict,無 model_dump()
|
||
"analysis": analysis.model_dump() if hasattr(analysis, "model_dump") else analysis,
|
||
"metadata": metadata,
|
||
}
|
||
|
||
success = await self._repository.save_review(review_id, result)
|
||
|
||
if success:
|
||
logger.info("gitea_review_saved", review_id=review_id, ttl_days=7)
|
||
else:
|
||
logger.error("gitea_review_save_failed", review_id=review_id)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Internal helpers: Telegram
|
||
# ------------------------------------------------------------------
|
||
|
||
async def _send_gitea_telegram_alert(
|
||
self,
|
||
review_id: str,
|
||
event_type: str,
|
||
repo: str,
|
||
target: str,
|
||
url: str,
|
||
author: str,
|
||
analysis: CodeReviewResult | None,
|
||
) -> None:
|
||
"""
|
||
發送 Gitea 審查告警到 Telegram
|
||
|
||
格式:
|
||
═══════════════════════════
|
||
🔍 GITEA CODE REVIEW
|
||
═══════════════════════════
|
||
📦 repo/name
|
||
🔀 PR #123: Feature title
|
||
👤 @author
|
||
───────────────────────────
|
||
📊 品質分數: 85/100
|
||
⚠️ 發現 2 個問題
|
||
🔐 1 個安全疑慮
|
||
───────────────────────────
|
||
🧠 AI 摘要:
|
||
「代碼品質良好,但建議...」
|
||
───────────────────────────
|
||
[ 🔗 查看 PR ] [ 📋 詳情 ]
|
||
"""
|
||
try:
|
||
from src.services.telegram_gateway import get_telegram_gateway
|
||
telegram = get_telegram_gateway()
|
||
|
||
# 檢查是否有設定 Bot Token
|
||
if not settings.OPENCLAW_TG_BOT_TOKEN:
|
||
logger.debug("gitea_telegram_skipped", reason="Bot token not configured")
|
||
return
|
||
|
||
await telegram.initialize()
|
||
|
||
# 構建訊息
|
||
quality_emoji = (
|
||
"🟢" if analysis and analysis.quality_score >= 80
|
||
else "🟡" if analysis and analysis.quality_score >= 60
|
||
else "🔴"
|
||
)
|
||
|
||
# 2026-04-15 ogt: 改為 ADR-075 TYPE-1 格式(禁止 ═══ raw 格式)
|
||
message_lines = [
|
||
f"{quality_emoji} <b>TYPE-1 | Gitea Code Review</b>",
|
||
"──────────────────────",
|
||
f"├─ 倉庫: <code>{repo}</code>",
|
||
f"├─ 目標: <code>{target}</code>",
|
||
f"├─ 作者: @{author}",
|
||
]
|
||
|
||
if analysis:
|
||
message_lines.append(f"├─ 品質分數: {analysis.quality_score:.0f}/100")
|
||
if analysis.issues:
|
||
message_lines.append(f"├─ 問題: {len(analysis.issues)} 個")
|
||
if analysis.security_concerns:
|
||
message_lines.append(f"├─ 安全疑慮: {len(analysis.security_concerns)} 個")
|
||
message_lines.append(f"├─ AI 摘要: 「{analysis.summary[:150]}」")
|
||
else:
|
||
message_lines.append("├─ AI 分析失敗")
|
||
|
||
message_lines.extend([
|
||
f"└─ <a href=\"{url}\">Review #{review_id}</a>",
|
||
])
|
||
|
||
message = "\n".join(message_lines)
|
||
|
||
await telegram.send_alert_notification(message)
|
||
|
||
logger.info("gitea_telegram_sent", review_id=review_id, repo=repo, event_type=event_type)
|
||
|
||
except Exception as e:
|
||
logger.exception("gitea_telegram_failed", error=str(e))
|
||
|
||
# ------------------------------------------------------------------
|
||
# Internal helpers: Approval
|
||
# ------------------------------------------------------------------
|
||
|
||
async def _create_gitea_approval(
|
||
self,
|
||
review_id: str,
|
||
repo: str,
|
||
target: str,
|
||
url: str,
|
||
analysis: CodeReviewResult,
|
||
) -> str:
|
||
"""
|
||
為有安全疑慮的 PR 建立 Approval 記錄
|
||
|
||
Returns:
|
||
str: Approval ID
|
||
"""
|
||
try:
|
||
from src.models.approval import (
|
||
ApprovalRequestCreate,
|
||
BlastRadius,
|
||
DataImpact,
|
||
RiskLevel,
|
||
)
|
||
from src.services.approval_db import get_approval_service
|
||
approval_service = get_approval_service()
|
||
|
||
# 決定風險等級
|
||
if len(analysis.security_concerns) > 2 or analysis.quality_score < 50:
|
||
risk_level = RiskLevel.CRITICAL
|
||
elif analysis.security_concerns or analysis.quality_score < 70:
|
||
risk_level = RiskLevel.HIGH
|
||
else:
|
||
risk_level = RiskLevel.MEDIUM
|
||
|
||
# P1-2 修正: 欄位對齊 ApprovalRequestBase (2026-03-29)
|
||
root_cause = f"Code review found security concerns in {target}"
|
||
suggestion = f"Review {len(analysis.security_concerns)} security concern(s): {', '.join(analysis.security_concerns[:3])}"
|
||
approval_request = ApprovalRequestCreate(
|
||
action=f"Code Review Security: {repo}",
|
||
description=f"Root Cause: {root_cause}\nSuggestion: {suggestion}",
|
||
risk_level=risk_level,
|
||
blast_radius=BlastRadius(
|
||
affected_pods=1,
|
||
estimated_downtime="0",
|
||
related_services=[repo],
|
||
data_impact=DataImpact.READ_ONLY,
|
||
),
|
||
dry_run_checks=[],
|
||
requested_by="gitea-webhook",
|
||
metadata={
|
||
"source": "gitea",
|
||
"alert_type": "code_review_security",
|
||
"target_resource": repo,
|
||
"namespace": "gitea",
|
||
"gitea_review_id": review_id,
|
||
"target": target,
|
||
"url": url,
|
||
"quality_score": analysis.quality_score,
|
||
"security_concerns": analysis.security_concerns,
|
||
"issues_count": len(analysis.issues),
|
||
"llm_provider": analysis.analyzed_by,
|
||
"llm_confidence": analysis.confidence,
|
||
},
|
||
)
|
||
|
||
# 創建 Approval
|
||
approval_id = str(uuid.uuid4())
|
||
await approval_service.create_approval(
|
||
approval_id=approval_id,
|
||
request=approval_request,
|
||
)
|
||
|
||
logger.info(
|
||
"gitea_approval_created",
|
||
approval_id=approval_id,
|
||
review_id=review_id,
|
||
risk_level=risk_level.value,
|
||
)
|
||
|
||
return approval_id
|
||
|
||
except Exception as e:
|
||
logger.exception("gitea_approval_creation_failed", error=str(e))
|
||
return f"temp-{uuid.uuid4().hex[:8]}"
|
||
|
||
# ------------------------------------------------------------------
|
||
# Public: Orchestration (Background Tasks)
|
||
# ------------------------------------------------------------------
|
||
|
||
async def review_pull_request(
|
||
self,
|
||
repo, # GiteaRepository
|
||
pr, # GiteaPullRequest
|
||
sender, # GiteaUser
|
||
review_id: str,
|
||
action: str,
|
||
) -> None:
|
||
"""
|
||
背景任務: PR 代碼審查
|
||
|
||
1. 取得 PR diff
|
||
2. 呼叫 OpenClaw 分析
|
||
3. 儲存結果到 Redis
|
||
4. 發送 Telegram 通知
|
||
5. 建立 Approval (可選)
|
||
"""
|
||
try:
|
||
logger.info(
|
||
"gitea_pr_review_started",
|
||
review_id=review_id,
|
||
repo=repo.full_name,
|
||
pr_number=pr.number,
|
||
sender=sender.login,
|
||
)
|
||
|
||
# 1. 取得 PR diff
|
||
diff_content = await self._fetch_pr_diff(pr.diff_url)
|
||
|
||
# 2. 呼叫 OpenClaw 進行代碼審查
|
||
analysis = await self._call_openclaw_code_review(
|
||
repo_name=repo.full_name,
|
||
pr_title=pr.title,
|
||
pr_body=pr.body or "",
|
||
diff_content=diff_content,
|
||
changed_files=pr.changed_files,
|
||
additions=pr.additions,
|
||
deletions=pr.deletions,
|
||
)
|
||
|
||
# 3. 儲存結果到 Redis
|
||
await self._save_review_with_analysis(
|
||
review_id=review_id,
|
||
event_type="pull_request",
|
||
repo=repo.full_name,
|
||
target=f"PR #{pr.number}",
|
||
analysis=analysis,
|
||
metadata={
|
||
"pr_number": pr.number,
|
||
"pr_title": pr.title,
|
||
"pr_url": pr.html_url,
|
||
"author": pr.user.login,
|
||
"action": action,
|
||
"changed_files": pr.changed_files,
|
||
"additions": pr.additions,
|
||
"deletions": pr.deletions,
|
||
},
|
||
)
|
||
|
||
# 4. 發送 Telegram 通知
|
||
await self._send_gitea_telegram_alert(
|
||
review_id=review_id,
|
||
event_type="pull_request",
|
||
repo=repo.full_name,
|
||
target=f"PR #{pr.number}: {pr.title[:50]}",
|
||
url=pr.html_url,
|
||
author=pr.user.login,
|
||
analysis=analysis,
|
||
)
|
||
|
||
# 5. 如果有安全疑慮,建立 Approval
|
||
if analysis and analysis.security_concerns:
|
||
await self._create_gitea_approval(
|
||
review_id=review_id,
|
||
repo=repo.full_name,
|
||
target=f"PR #{pr.number}",
|
||
url=pr.html_url,
|
||
analysis=analysis,
|
||
)
|
||
|
||
logger.info(
|
||
"gitea_pr_review_completed",
|
||
review_id=review_id,
|
||
quality_score=analysis.quality_score if analysis else None,
|
||
has_security_concerns=bool(analysis and analysis.security_concerns),
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(
|
||
"gitea_pr_review_failed",
|
||
review_id=review_id,
|
||
error=str(e),
|
||
)
|
||
|
||
async def review_push(
|
||
self,
|
||
repo, # GiteaRepository
|
||
commits: list, # list[GiteaCommit]
|
||
sender, # GiteaUser
|
||
review_id: str,
|
||
ref: str,
|
||
before_sha: str | None,
|
||
after_sha: str | None,
|
||
) -> None:
|
||
"""
|
||
背景任務: Push 代碼審查
|
||
|
||
1. 整理 commit 資訊
|
||
2. 呼叫 OpenClaw 分析
|
||
3. 儲存結果到 Redis
|
||
4. 發送 Telegram 通知 (只有發現問題時才通知)
|
||
"""
|
||
try:
|
||
logger.info(
|
||
"gitea_push_review_started",
|
||
review_id=review_id,
|
||
repo=repo.full_name,
|
||
commit_count=len(commits),
|
||
)
|
||
|
||
# 1. 整理 commit 資訊
|
||
commit_summary = []
|
||
all_files: dict[str, list] = {"added": [], "modified": [], "removed": []}
|
||
for commit in commits:
|
||
commit_summary.append({
|
||
"sha": commit.id[:8],
|
||
"message": commit.message[:100],
|
||
"author": commit.author.get("name", "unknown"),
|
||
})
|
||
all_files["added"].extend(commit.added)
|
||
all_files["modified"].extend(commit.modified)
|
||
all_files["removed"].extend(commit.removed)
|
||
|
||
# 2. 呼叫 OpenClaw 進行代碼審查 (Push 版)
|
||
analysis = await self._call_openclaw_push_review(
|
||
repo_name=repo.full_name,
|
||
ref=ref,
|
||
commits=commit_summary,
|
||
files_changed=all_files,
|
||
)
|
||
|
||
# 3. 儲存結果到 Redis
|
||
await self._save_review_with_analysis(
|
||
review_id=review_id,
|
||
event_type="push",
|
||
repo=repo.full_name,
|
||
target=f"push to {ref.split('/')[-1]}",
|
||
analysis=analysis,
|
||
metadata={
|
||
"ref": ref,
|
||
"before_sha": before_sha,
|
||
"after_sha": after_sha,
|
||
"commit_count": len(commits),
|
||
"pusher": sender.login,
|
||
"files": all_files,
|
||
},
|
||
)
|
||
|
||
# 4. 發送 Telegram 通知 (只有發現問題時才通知)
|
||
if analysis and (
|
||
analysis.issues
|
||
or analysis.security_concerns
|
||
or analysis.quality_score < 70
|
||
):
|
||
await self._send_gitea_telegram_alert(
|
||
review_id=review_id,
|
||
event_type="push",
|
||
repo=repo.full_name,
|
||
target=f"push to {ref.split('/')[-1]} ({len(commits)} commits)",
|
||
url=repo.html_url,
|
||
author=sender.login,
|
||
analysis=analysis,
|
||
)
|
||
|
||
logger.info(
|
||
"gitea_push_review_completed",
|
||
review_id=review_id,
|
||
quality_score=analysis.quality_score if analysis else None,
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(
|
||
"gitea_push_review_failed",
|
||
review_id=review_id,
|
||
error=str(e),
|
||
)
|
||
|
||
|
||
|
||
# =============================================================================
|
||
# Singleton
|
||
# =============================================================================
|
||
|
||
# 單例
|
||
_service: GiteaWebhookService | None = None
|
||
|
||
|
||
def get_gitea_webhook_service() -> GiteaWebhookService:
|
||
"""取得 GiteaWebhookService 單例"""
|
||
global _service
|
||
if _service is None:
|
||
_service = GiteaWebhookService()
|
||
return _service
|