feat(webhook): Task 1+2 — config + service GitHub→Gitea 遷移 (ADR-059)
- config.py: GITHUB_WEBHOOK_SECRET/ALLOWED_REPOS → GITEA_* - 新增 gitea_webhook_service.py: PR/Push review only, 移除 CI diagnosis - 移除 CIFailureDiagnosis, diagnose_ci_failure, _call_openclaw_ci_diagnosis Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
684
apps/api/src/services/gitea_webhook_service.py
Normal file
684
apps/api/src/services/gitea_webhook_service.py
Normal file
@@ -0,0 +1,684 @@
|
||||
"""
|
||||
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("github_review_saved", review_id=review_id)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("github_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("github_review_get_failed", review_id=review_id, error=str(e))
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Service
|
||||
# =============================================================================
|
||||
|
||||
class GiteaWebhookService:
|
||||
"""
|
||||
GitHub Webhook 服務
|
||||
|
||||
封裝審查結果的儲存、查詢以及全部業務協調流程:
|
||||
- PR 代碼審查 (review_pull_request)
|
||||
- Push 代碼審查 (review_push)
|
||||
- CI 失敗診斷 (diagnose_ci_failure)
|
||||
- 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("github_review_saved_custom_ttl", review_id=review_id, ttl=ttl)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("github_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 內容 (委派給 GitHubApiService)"""
|
||||
from src.services.github_api_service import get_github_api_service
|
||||
service = get_github_api_service()
|
||||
return await service.fetch_pr_diff(diff_url)
|
||||
|
||||
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.openclaw_http_service import get_openclaw_http_service
|
||||
service = get_openclaw_http_service()
|
||||
data = await service.code_review(
|
||||
repo_name=repo_name,
|
||||
pr_title=pr_title,
|
||||
pr_body=pr_body,
|
||||
diff_content=diff_content,
|
||||
changed_files=changed_files,
|
||||
additions=additions,
|
||||
deletions=deletions,
|
||||
prefer_local=True,
|
||||
timeout=120.0,
|
||||
)
|
||||
|
||||
if data:
|
||||
return CodeReviewResult(**data)
|
||||
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:
|
||||
"""
|
||||
呼叫 OpenClaw 進行 Push 代碼審查
|
||||
|
||||
Phase 22 P0 修復: 使用 OpenClawHttpService (2026-03-31)
|
||||
"""
|
||||
try:
|
||||
from src.services.openclaw_http_service import get_openclaw_http_service
|
||||
service = get_openclaw_http_service()
|
||||
data = await service.push_review(
|
||||
repo_name=repo_name,
|
||||
ref=ref,
|
||||
commits=commits,
|
||||
files_changed=files_changed,
|
||||
prefer_local=True,
|
||||
timeout=120.0,
|
||||
)
|
||||
|
||||
if data:
|
||||
return CodeReviewResult(**data)
|
||||
return None
|
||||
|
||||
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(),
|
||||
"analysis": analysis.model_dump() if analysis else None,
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
success = await self._repository.save_review(review_id, result)
|
||||
|
||||
if success:
|
||||
logger.info("github_review_saved", review_id=review_id, ttl_days=7)
|
||||
else:
|
||||
logger.error("github_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:
|
||||
"""
|
||||
發送 GitHub 審查告警到 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("github_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 "🔴"
|
||||
)
|
||||
|
||||
message_lines = [
|
||||
"═══════════════════════════",
|
||||
"🔍 GITEA CODE REVIEW",
|
||||
"═══════════════════════════",
|
||||
f"📦 {repo}",
|
||||
f"🔀 {target}",
|
||||
f"👤 @{author}",
|
||||
"───────────────────────────",
|
||||
]
|
||||
|
||||
if analysis:
|
||||
message_lines.extend([
|
||||
f"{quality_emoji} 品質分數: {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.extend([
|
||||
"───────────────────────────",
|
||||
"🧠 AI 摘要:",
|
||||
f"「{analysis.summary[:150]}」",
|
||||
])
|
||||
else:
|
||||
message_lines.append("❌ AI 分析失敗")
|
||||
|
||||
message_lines.extend([
|
||||
"───────────────────────────",
|
||||
f"🔗 {url}",
|
||||
f"📋 Review ID: {review_id}",
|
||||
])
|
||||
|
||||
message = "\n".join(message_lines)
|
||||
|
||||
# 發送訊息 (使用 send_notification 而非 send_message)
|
||||
await telegram.send_notification(message)
|
||||
|
||||
logger.info("github_telegram_sent", review_id=review_id, repo=repo, event_type=event_type)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("github_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": "github",
|
||||
"alert_type": "code_review_security",
|
||||
"target_resource": repo,
|
||||
"namespace": "github",
|
||||
"github_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(
|
||||
"github_approval_created",
|
||||
approval_id=approval_id,
|
||||
review_id=review_id,
|
||||
risk_level=risk_level.value,
|
||||
)
|
||||
|
||||
return approval_id
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("github_approval_creation_failed", error=str(e))
|
||||
return f"temp-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public: Orchestration (Background Tasks)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def review_pull_request(
|
||||
self,
|
||||
repo, # GitHubRepository
|
||||
pr, # GitHubPullRequest
|
||||
sender, # GitHubUser
|
||||
review_id: str,
|
||||
action: str,
|
||||
) -> None:
|
||||
"""
|
||||
背景任務: PR 代碼審查
|
||||
|
||||
1. 取得 PR diff
|
||||
2. 呼叫 OpenClaw 分析
|
||||
3. 儲存結果到 Redis
|
||||
4. 發送 Telegram 通知
|
||||
5. 建立 Approval (可選)
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
"github_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(
|
||||
"github_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(
|
||||
"github_pr_review_failed",
|
||||
review_id=review_id,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
async def review_push(
|
||||
self,
|
||||
repo, # GitHubRepository
|
||||
commits: list, # list[GitHubCommit]
|
||||
sender, # GitHubUser
|
||||
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(
|
||||
"github_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(
|
||||
"github_push_review_completed",
|
||||
review_id=review_id,
|
||||
quality_score=analysis.quality_score if analysis else None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"github_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
|
||||
Reference in New Issue
Block a user