From cb8bc9463caed4b1d1f4ba342baee548c3194769 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 27 Jun 2026 16:39:38 +0800 Subject: [PATCH] fix(api): prefer anonymous gitea weekly stats --- .../api/src/services/weekly_report_service.py | 104 ++++++++++-------- .../tests/test_report_generation_service.py | 38 ++++++- 2 files changed, 93 insertions(+), 49 deletions(-) diff --git a/apps/api/src/services/weekly_report_service.py b/apps/api/src/services/weekly_report_service.py index c26bddf2..d20bd299 100644 --- a/apps/api/src/services/weekly_report_service.py +++ b/apps/api/src/services/weekly_report_service.py @@ -154,9 +154,7 @@ class WeeklyReportService: 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") - headers = {"Accept": "application/json", "User-Agent": "awoooi-weekly-report/1.0"} - if settings.GITEA_API_TOKEN: - headers["Authorization"] = f"token {settings.GITEA_API_TOKEN}" + header_candidates = self._gitea_read_headers() commits = 0 deploys = 0 @@ -165,52 +163,54 @@ class WeeklyReportService: max_pages = 4 try: for api_url in self._gitea_api_candidates(): - commits = deploys = 0 - page = 1 - total_commits: int | None = None - try: - while page <= max_pages: - 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: - total_header = response.headers.get("X-Total-Count") or response.headers.get("X-Total") - if total_header and total_header.isdigit(): - total_commits = int(total_header) - 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 total_commits or commits, deploys, True - page += 1 - if total_commits is not None and total_commits > 0: - logger.info( - "gitea_git_stats_sampled", - total_commits=total_commits, - sampled_pages=max_pages, - sampled_deploys=deploys, + for headers in header_candidates: + commits = deploys = 0 + page = 1 + total_commits: int | None = None + try: + while page <= max_pages: + 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=2) as response: + total_header = response.headers.get("X-Total-Count") or response.headers.get("X-Total") + if total_header and total_header.isdigit(): + total_commits = int(total_header) + 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 total_commits or commits, deploys, True + page += 1 + if total_commits is not None and total_commits > 0: + logger.info( + "gitea_git_stats_sampled", + total_commits=total_commits, + sampled_pages=max_pages, + sampled_deploys=deploys, + ) + return total_commits, deploys, True + except Exception as candidate_exc: + logger.warning( + "gitea_git_stats_candidate_failed", + api_url=api_url, + auth_mode="token" if "Authorization" in headers else "anonymous", + error=str(candidate_exc), ) - return total_commits, deploys, True - except Exception as candidate_exc: - logger.warning( - "gitea_git_stats_candidate_failed", - api_url=api_url, - error=str(candidate_exc), - ) - continue + continue except Exception as exc: logger.warning("gitea_git_stats_failed", error=str(exc)) return 0, 0, False @@ -224,6 +224,14 @@ class WeeklyReportService: deduped.append(candidate) return deduped + def _gitea_read_headers(self) -> list[dict[str, str]]: + """取得 Gitea read-only 統計用 headers,匿名讀優先。""" + base = {"Accept": "application/json", "User-Agent": "awoooi-weekly-report/1.0"} + if not settings.GITEA_API_TOKEN: + return [base] + token_headers = {**base, "Authorization": f"token {settings.GITEA_API_TOKEN}"} + return [base, token_headers] + 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 52f9c45e..9fdd30cd 100644 --- a/apps/api/tests/test_report_generation_service.py +++ b/apps/api/tests/test_report_generation_service.py @@ -151,7 +151,8 @@ class TestWeeklyReportGitStats: def fake_urlopen(request, timeout): assert "/api/v1/repos/wooo/awoooi/commits?" in request.full_url - assert timeout == 3 + assert timeout == 2 + assert "Authorization" not in request.headers return Response() monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen) @@ -167,6 +168,41 @@ class TestWeeklyReportGitStats: assert deploys == 2 assert source_ok is True + def test_gitea_commit_stats_prefers_anonymous_read_before_token(self, monkeypatch): + payload = [{"commit": {"message": "fix(api): one\n"}}] + seen_authorization = [] + + class Response: + headers = {"X-Total-Count": "1"} + + def __enter__(self): + return self + + def __exit__(self, *_args): + return None + + def read(self): + return weekly_report_module.json.dumps(payload).encode("utf-8") + + def fake_urlopen(request, timeout): + seen_authorization.append(request.headers.get("Authorization")) + return Response() + + monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen) + monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_URL", "https://gitea.example.test") + monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_OWNER", "wooo") + monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_NAME", "awoooi") + monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_TOKEN", "redacted-token") + + svc = WeeklyReportService(stats_service=object(), k3s_monitor=object()) + monkeypatch.setattr(svc, "_gitea_api_candidates", lambda: ["https://gitea.example.test"]) + commits, deploys, source_ok = svc._get_gitea_commit_stats(datetime.now(_TZ_TAIPEI)) + + assert commits == 1 + assert deploys == 0 + assert source_ok is True + assert seen_authorization == [None] + def test_gitea_commit_stats_uses_total_header_with_limited_scan(self, monkeypatch): payload = [{"commit": {"message": "fix(api): one\n"}} for _ in range(50)] calls = []