fix(api): avoid slow weekly git fallback
This commit is contained in:
@@ -23,6 +23,7 @@ import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
from typing import Protocol, runtime_checkable
|
||||
@@ -100,6 +101,9 @@ class WeeklyReportService:
|
||||
|
||||
def _get_git_stats(self, since: datetime) -> tuple[int, int, bool]:
|
||||
"""取得 Git 統計 (commits, deploys)"""
|
||||
git_cwd = "/app"
|
||||
if not self._has_local_git_worktree(git_cwd):
|
||||
return self._get_gitea_commit_stats(since)
|
||||
try:
|
||||
# 取得本週 commits 數量
|
||||
since_str = since.strftime("%Y-%m-%d")
|
||||
@@ -108,7 +112,7 @@ class WeeklyReportService:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
cwd="/app", # K8s 容器內的工作目錄
|
||||
cwd=git_cwd, # K8s 容器內的工作目錄
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(
|
||||
@@ -125,7 +129,7 @@ class WeeklyReportService:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
cwd="/app",
|
||||
cwd=git_cwd,
|
||||
)
|
||||
if result_deploy.returncode != 0:
|
||||
logger.warning(
|
||||
@@ -141,9 +145,12 @@ class WeeklyReportService:
|
||||
logger.warning("git_stats_failed", error=str(e))
|
||||
return self._get_gitea_commit_stats(since)
|
||||
|
||||
def _has_local_git_worktree(self, cwd: str) -> bool:
|
||||
"""確認容器內是否真的保留 Git metadata。"""
|
||||
return Path(cwd, ".git").exists()
|
||||
|
||||
def _get_gitea_commit_stats(self, since: datetime) -> tuple[int, int, bool]:
|
||||
"""從 Gitea commits API 讀取週報開發活動統計。"""
|
||||
api_url = settings.GITEA_API_URL.rstrip("/")
|
||||
owner = quote(str(settings.GITEA_REPO_OWNER), safe="")
|
||||
repo = quote(str(settings.GITEA_REPO_NAME), safe="")
|
||||
since_utc = since.astimezone(ZoneInfo("UTC")).isoformat().replace("+00:00", "Z")
|
||||
@@ -156,34 +163,53 @@ class WeeklyReportService:
|
||||
page = 1
|
||||
limit = 50
|
||||
try:
|
||||
while page <= 20:
|
||||
query = urlencode({
|
||||
"sha": "main",
|
||||
"since": since_utc,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
url = f"{api_url}/api/v1/repos/{owner}/{repo}/commits?{query}"
|
||||
request = Request(url, headers=headers)
|
||||
with urlopen(request, timeout=8) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
if not isinstance(payload, list):
|
||||
logger.warning("gitea_git_stats_unexpected_payload", payload_type=type(payload).__name__)
|
||||
return 0, 0, False
|
||||
commits += len(payload)
|
||||
for item in payload:
|
||||
commit = item.get("commit") if isinstance(item, dict) else {}
|
||||
message = str((commit or {}).get("message") or "")
|
||||
subject = message.splitlines()[0].lower()
|
||||
if "deploy" in subject or subject.startswith("chore(cd):"):
|
||||
deploys += 1
|
||||
if len(payload) < limit:
|
||||
break
|
||||
page += 1
|
||||
return commits, deploys, True
|
||||
for api_url in self._gitea_api_candidates():
|
||||
commits = deploys = 0
|
||||
page = 1
|
||||
try:
|
||||
while page <= 20:
|
||||
query = urlencode({
|
||||
"sha": "main",
|
||||
"since": since_utc,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
url = f"{api_url}/api/v1/repos/{owner}/{repo}/commits?{query}"
|
||||
request = Request(url, headers=headers)
|
||||
with urlopen(request, timeout=3) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
if not isinstance(payload, list):
|
||||
logger.warning("gitea_git_stats_unexpected_payload", payload_type=type(payload).__name__)
|
||||
break
|
||||
commits += len(payload)
|
||||
for item in payload:
|
||||
commit = item.get("commit") if isinstance(item, dict) else {}
|
||||
message = str((commit or {}).get("message") or "")
|
||||
subject = message.splitlines()[0].lower()
|
||||
if "deploy" in subject or subject.startswith("chore(cd):"):
|
||||
deploys += 1
|
||||
if len(payload) < limit:
|
||||
return commits, deploys, True
|
||||
page += 1
|
||||
except Exception as candidate_exc:
|
||||
logger.warning(
|
||||
"gitea_git_stats_candidate_failed",
|
||||
api_url=api_url,
|
||||
error=str(candidate_exc),
|
||||
)
|
||||
continue
|
||||
except Exception as exc:
|
||||
logger.warning("gitea_git_stats_failed", error=str(exc))
|
||||
return 0, 0, False
|
||||
return 0, 0, False
|
||||
|
||||
def _gitea_api_candidates(self) -> list[str]:
|
||||
"""取得 Git 統計可用的 Gitea read-only API base URLs。"""
|
||||
candidates = [settings.GITEA_API_URL.rstrip("/"), "https://gitea.wooo.work"]
|
||||
deduped: list[str] = []
|
||||
for candidate in candidates:
|
||||
if candidate and candidate not in deduped:
|
||||
deduped.append(candidate)
|
||||
return deduped
|
||||
|
||||
async def generate_report(self) -> WeeklyReportMessage:
|
||||
"""
|
||||
|
||||
@@ -58,6 +58,7 @@ class TestWeeklyReportGitStats:
|
||||
)
|
||||
|
||||
svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
|
||||
monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: True)
|
||||
monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (0, 0, False))
|
||||
commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))
|
||||
|
||||
@@ -78,6 +79,7 @@ class TestWeeklyReportGitStats:
|
||||
)
|
||||
|
||||
svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
|
||||
monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: True)
|
||||
monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (12, 4, True))
|
||||
commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))
|
||||
|
||||
@@ -105,6 +107,7 @@ class TestWeeklyReportGitStats:
|
||||
monkeypatch.setattr(weekly_report_module.subprocess, "run", fake_run)
|
||||
|
||||
svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
|
||||
monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: True)
|
||||
monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (0, 0, False))
|
||||
commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))
|
||||
|
||||
@@ -112,6 +115,21 @@ class TestWeeklyReportGitStats:
|
||||
assert deploys == 0
|
||||
assert source_ok is False
|
||||
|
||||
def test_missing_local_git_worktree_uses_gitea_without_subprocess(self, monkeypatch):
|
||||
def fail_run(*_args, **_kwargs):
|
||||
raise AssertionError("subprocess git log should not run without local .git")
|
||||
|
||||
monkeypatch.setattr(weekly_report_module.subprocess, "run", fail_run)
|
||||
|
||||
svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
|
||||
monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: False)
|
||||
monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (21, 7, True))
|
||||
commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))
|
||||
|
||||
assert commits == 21
|
||||
assert deploys == 7
|
||||
assert source_ok is True
|
||||
|
||||
def test_gitea_commit_stats_counts_deploy_markers(self, monkeypatch):
|
||||
payload = [
|
||||
{"commit": {"message": "chore(cd): deploy abc123 [skip ci]\n"}},
|
||||
@@ -131,7 +149,7 @@ class TestWeeklyReportGitStats:
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
assert "/api/v1/repos/wooo/awoooi/commits?" in request.full_url
|
||||
assert timeout == 8
|
||||
assert timeout == 3
|
||||
return Response()
|
||||
|
||||
monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen)
|
||||
|
||||
Reference in New Issue
Block a user