From 1847d5a2e51d675eeedd1437cfcf83303ff5b7ed Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 27 Jun 2026 16:23:28 +0800 Subject: [PATCH] fix(api): avoid slow weekly git fallback --- .../api/src/services/weekly_report_service.py | 84 ++++++++++++------- .../tests/test_report_generation_service.py | 20 ++++- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/apps/api/src/services/weekly_report_service.py b/apps/api/src/services/weekly_report_service.py index 1a2bd824..1ac1a3c1 100644 --- a/apps/api/src/services/weekly_report_service.py +++ b/apps/api/src/services/weekly_report_service.py @@ -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: """ diff --git a/apps/api/tests/test_report_generation_service.py b/apps/api/tests/test_report_generation_service.py index c6f8706a..373c0d82 100644 --- a/apps/api/tests/test_report_generation_service.py +++ b/apps/api/tests/test_report_generation_service.py @@ -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)