115 lines
4.3 KiB
Python
115 lines
4.3 KiB
Python
"""Compatibility hardening for Gitea validation discovery and request latency.
|
|
|
|
Some Gitea versions expose pull-request check results as commit statuses while
|
|
returning an empty branch-filtered Actions run wrapper. The base client treats
|
|
collections strictly; this subclass preserves that strictness while translating
|
|
a fully-green exact-head status set into one synthetic completed validation run.
|
|
|
|
Large repositories can require longer than the historical 15-second client
|
|
budget to commit files, apply changesets, or materialize a server-side squash
|
|
merge. The resilient client therefore uses the already allowlisted 60-second
|
|
maximum from ``GiteaSettings``. All HTTP method allowlisting, redirect handling,
|
|
response budgets, access checks and non-forcing merge behavior remain in the
|
|
base client and are not duplicated here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from .gitea import GiteaClient as BaseGiteaClient
|
|
from .gitea import GiteaError, GiteaSettings, RegisteredRepository, TokenProvider
|
|
|
|
GITEA_REQUEST_TIMEOUT_SECONDS = 60.0
|
|
|
|
|
|
class GiteaClient(BaseGiteaClient):
|
|
"""Gitea client with exact-head validation fallback and bounded latency."""
|
|
|
|
def __init__(
|
|
self,
|
|
settings: GiteaSettings,
|
|
token_provider: TokenProvider,
|
|
*,
|
|
transport: httpx.BaseTransport | None = None,
|
|
) -> None:
|
|
resilient_settings = settings.model_copy(
|
|
update={"timeout_seconds": GITEA_REQUEST_TIMEOUT_SECONDS}
|
|
)
|
|
super().__init__(resilient_settings, token_provider, transport=transport)
|
|
|
|
def action_runs(
|
|
self,
|
|
repository: RegisteredRepository,
|
|
*,
|
|
branch: str,
|
|
page: int,
|
|
limit: int,
|
|
) -> list[dict[str, Any]]:
|
|
payload = self._request(
|
|
self._path("repos", repository.owner, repository.name, "actions", "runs"),
|
|
{"branch": branch, "page": page, "limit": limit},
|
|
)
|
|
if isinstance(payload, dict):
|
|
if "workflow_runs" in payload:
|
|
payload = payload.get("workflow_runs")
|
|
elif "runs" in payload:
|
|
payload = payload.get("runs")
|
|
else:
|
|
payload = None
|
|
runs = self._as_list(payload)
|
|
if runs:
|
|
return runs
|
|
|
|
# Only an actually empty Actions result can fall back. Malformed
|
|
# collections still fail above, preserving strict response validation.
|
|
try:
|
|
branch_payload = self.branch(repository, branch)
|
|
except GiteaError:
|
|
return []
|
|
commit = branch_payload.get("commit")
|
|
if not isinstance(commit, dict):
|
|
return []
|
|
head_sha = str(commit.get("id") or commit.get("sha") or "")
|
|
if len(head_sha) != 40 or any(
|
|
character not in "0123456789abcdefABCDEF" for character in head_sha
|
|
):
|
|
return []
|
|
|
|
statuses = self.commit_statuses(repository, head_sha, 1, 100)
|
|
if not statuses:
|
|
return []
|
|
latest_by_context: dict[str, dict[str, Any]] = {}
|
|
for item in statuses:
|
|
status_context = str(item.get("context") or "__default__")
|
|
previous = latest_by_context.get(status_context)
|
|
if previous is None or str(item.get("created_at") or "") > str(
|
|
previous.get("created_at") or ""
|
|
):
|
|
latest_by_context[status_context] = item
|
|
latest_states = {
|
|
str(item.get("status") or "").casefold() for item in latest_by_context.values()
|
|
}
|
|
# Gitea can omit ``head_branch`` for pull_request runs, causing the
|
|
# branch-filtered run list above to be empty even though the exact-head
|
|
# commit statuses are available. Skipped jobs are neutral evidence;
|
|
# require at least one success and reject every non-terminal state.
|
|
if "success" not in latest_states or not latest_states <= {"success", "skipped"}:
|
|
return []
|
|
|
|
return [
|
|
{
|
|
"id": 0,
|
|
"name": "Exact-head commit status validation",
|
|
"status": "completed",
|
|
"conclusion": "success",
|
|
"event": "commit_status_fallback",
|
|
"head_branch": branch,
|
|
"head_sha": head_sha,
|
|
"html_url": "",
|
|
"validation_evidence": "commit-statuses",
|
|
}
|
|
]
|