fix(api): prefer anonymous gitea weekly stats
This commit is contained in:
@@ -154,9 +154,7 @@ class WeeklyReportService:
|
|||||||
owner = quote(str(settings.GITEA_REPO_OWNER), safe="")
|
owner = quote(str(settings.GITEA_REPO_OWNER), safe="")
|
||||||
repo = quote(str(settings.GITEA_REPO_NAME), safe="")
|
repo = quote(str(settings.GITEA_REPO_NAME), safe="")
|
||||||
since_utc = since.astimezone(ZoneInfo("UTC")).isoformat().replace("+00:00", "Z")
|
since_utc = since.astimezone(ZoneInfo("UTC")).isoformat().replace("+00:00", "Z")
|
||||||
headers = {"Accept": "application/json", "User-Agent": "awoooi-weekly-report/1.0"}
|
header_candidates = self._gitea_read_headers()
|
||||||
if settings.GITEA_API_TOKEN:
|
|
||||||
headers["Authorization"] = f"token {settings.GITEA_API_TOKEN}"
|
|
||||||
|
|
||||||
commits = 0
|
commits = 0
|
||||||
deploys = 0
|
deploys = 0
|
||||||
@@ -165,52 +163,54 @@ class WeeklyReportService:
|
|||||||
max_pages = 4
|
max_pages = 4
|
||||||
try:
|
try:
|
||||||
for api_url in self._gitea_api_candidates():
|
for api_url in self._gitea_api_candidates():
|
||||||
commits = deploys = 0
|
for headers in header_candidates:
|
||||||
page = 1
|
commits = deploys = 0
|
||||||
total_commits: int | None = None
|
page = 1
|
||||||
try:
|
total_commits: int | None = None
|
||||||
while page <= max_pages:
|
try:
|
||||||
query = urlencode({
|
while page <= max_pages:
|
||||||
"sha": "main",
|
query = urlencode({
|
||||||
"since": since_utc,
|
"sha": "main",
|
||||||
"page": page,
|
"since": since_utc,
|
||||||
"limit": limit,
|
"page": page,
|
||||||
})
|
"limit": limit,
|
||||||
url = f"{api_url}/api/v1/repos/{owner}/{repo}/commits?{query}"
|
})
|
||||||
request = Request(url, headers=headers)
|
url = f"{api_url}/api/v1/repos/{owner}/{repo}/commits?{query}"
|
||||||
with urlopen(request, timeout=3) as response:
|
request = Request(url, headers=headers)
|
||||||
total_header = response.headers.get("X-Total-Count") or response.headers.get("X-Total")
|
with urlopen(request, timeout=2) as response:
|
||||||
if total_header and total_header.isdigit():
|
total_header = response.headers.get("X-Total-Count") or response.headers.get("X-Total")
|
||||||
total_commits = int(total_header)
|
if total_header and total_header.isdigit():
|
||||||
payload = json.loads(response.read().decode("utf-8"))
|
total_commits = int(total_header)
|
||||||
if not isinstance(payload, list):
|
payload = json.loads(response.read().decode("utf-8"))
|
||||||
logger.warning("gitea_git_stats_unexpected_payload", payload_type=type(payload).__name__)
|
if not isinstance(payload, list):
|
||||||
break
|
logger.warning("gitea_git_stats_unexpected_payload", payload_type=type(payload).__name__)
|
||||||
commits += len(payload)
|
break
|
||||||
for item in payload:
|
commits += len(payload)
|
||||||
commit = item.get("commit") if isinstance(item, dict) else {}
|
for item in payload:
|
||||||
message = str((commit or {}).get("message") or "")
|
commit = item.get("commit") if isinstance(item, dict) else {}
|
||||||
subject = message.splitlines()[0].lower()
|
message = str((commit or {}).get("message") or "")
|
||||||
if "deploy" in subject or subject.startswith("chore(cd):"):
|
subject = message.splitlines()[0].lower()
|
||||||
deploys += 1
|
if "deploy" in subject or subject.startswith("chore(cd):"):
|
||||||
if len(payload) < limit:
|
deploys += 1
|
||||||
return total_commits or commits, deploys, True
|
if len(payload) < limit:
|
||||||
page += 1
|
return total_commits or commits, deploys, True
|
||||||
if total_commits is not None and total_commits > 0:
|
page += 1
|
||||||
logger.info(
|
if total_commits is not None and total_commits > 0:
|
||||||
"gitea_git_stats_sampled",
|
logger.info(
|
||||||
total_commits=total_commits,
|
"gitea_git_stats_sampled",
|
||||||
sampled_pages=max_pages,
|
total_commits=total_commits,
|
||||||
sampled_deploys=deploys,
|
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
|
continue
|
||||||
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:
|
except Exception as exc:
|
||||||
logger.warning("gitea_git_stats_failed", error=str(exc))
|
logger.warning("gitea_git_stats_failed", error=str(exc))
|
||||||
return 0, 0, False
|
return 0, 0, False
|
||||||
@@ -224,6 +224,14 @@ class WeeklyReportService:
|
|||||||
deduped.append(candidate)
|
deduped.append(candidate)
|
||||||
return deduped
|
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:
|
async def generate_report(self) -> WeeklyReportMessage:
|
||||||
"""
|
"""
|
||||||
生成週報
|
生成週報
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ class TestWeeklyReportGitStats:
|
|||||||
|
|
||||||
def fake_urlopen(request, timeout):
|
def fake_urlopen(request, timeout):
|
||||||
assert "/api/v1/repos/wooo/awoooi/commits?" in request.full_url
|
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()
|
return Response()
|
||||||
|
|
||||||
monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen)
|
monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen)
|
||||||
@@ -167,6 +168,41 @@ class TestWeeklyReportGitStats:
|
|||||||
assert deploys == 2
|
assert deploys == 2
|
||||||
assert source_ok is True
|
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):
|
def test_gitea_commit_stats_uses_total_header_with_limited_scan(self, monkeypatch):
|
||||||
payload = [{"commit": {"message": "fix(api): one\n"}} for _ in range(50)]
|
payload = [{"commit": {"message": "fix(api): one\n"}} for _ in range(50)]
|
||||||
calls = []
|
calls = []
|
||||||
|
|||||||
Reference in New Issue
Block a user