Publish curated ITWorx_MCP_Hub source from 7fde2770ba344c684b7e7a6b74f763dc19e7c560
This commit is contained in:
1 parent
18cf749bf5
commit
89703431af
11 files changed
+1247
-114
No files matched your search
@@ -3,11 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from typing import Any, Protocol, cast
|
||||
@@ -331,6 +333,13 @@ class GiteaClient:
|
||||
"""Small bounded Gitea API client with same-origin redirect enforcement."""
|
||||
|
||||
_path_part = re.compile(r"^[^/\\?#\x00-\x1f\x7f]+$")
|
||||
_semver = re.compile(
|
||||
r"v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
|
||||
r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
|
||||
r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
|
||||
)
|
||||
_version_cache_seconds = 30
|
||||
_supported_ranges = (">=1.14.0 <1.15.0", ">=1.25.0 <1.28.0", ">=28.0.0 <29.0.0")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -347,7 +356,8 @@ class GiteaClient:
|
||||
follow_redirects=False,
|
||||
transport=transport,
|
||||
)
|
||||
self._merge_strategy_key: str | None = None
|
||||
self._version_lock = RLock()
|
||||
self._version_cache: tuple[float, dict[str, Any]] | None = None
|
||||
|
||||
def close(self) -> None:
|
||||
self._client.close()
|
||||
@@ -813,27 +823,100 @@ class GiteaClient:
|
||||
),
|
||||
)
|
||||
|
||||
def _merge_strategy_field(self) -> str:
|
||||
"""Resolve the versioned Gitea merge field before the single write attempt."""
|
||||
def upstream_compatibility(self, *, force_refresh: bool = False) -> dict[str, Any]:
|
||||
"""Read the upstream version, independent of this connector's build version.
|
||||
|
||||
if self._merge_strategy_key is not None:
|
||||
return self._merge_strategy_key
|
||||
payload = self._request(self._path("version"))
|
||||
version = payload.get("version") if isinstance(payload, dict) else None
|
||||
legacy_match = re.fullmatch(r"1\.(14|25|26|27)(?:\.\d+)?(?:[-+].*)?", str(version or ""))
|
||||
modern_match = re.fullmatch(r"(28)(?:\.\d+){1,2}(?:[-+].*)?", str(version or ""))
|
||||
if legacy_match is None and modern_match is None:
|
||||
Diagnostics cache for 30 seconds; each merge negotiates again immediately
|
||||
before its one write. Failed probes replace cached success and never fall
|
||||
back to the connector version. Gitea 28 drops the historical ``1.`` prefix.
|
||||
"""
|
||||
with self._version_lock:
|
||||
now = time.monotonic()
|
||||
if (
|
||||
not force_refresh
|
||||
and self._version_cache is not None
|
||||
and now - self._version_cache[0] < self._version_cache_seconds
|
||||
):
|
||||
return copy.deepcopy(self._version_cache[1])
|
||||
diagnostics: dict[str, Any] = {
|
||||
"source": "/api/v1/version",
|
||||
"upstreamVersion": None,
|
||||
"normalizedVersion": None,
|
||||
"compatibility": "UNKNOWN",
|
||||
"reason": "upstream_version_missing_or_malformed",
|
||||
"mergeStrategyField": None,
|
||||
"supportedRanges": list(self._supported_ranges),
|
||||
"prereleasesSupported": False,
|
||||
"checkedAt": datetime.now(UTC).isoformat(),
|
||||
"cacheSeconds": self._version_cache_seconds,
|
||||
"errorCode": None,
|
||||
}
|
||||
try:
|
||||
payload = self._request(self._path("version"))
|
||||
except GiteaError as exc:
|
||||
diagnostics.update(
|
||||
compatibility="UNAVAILABLE",
|
||||
reason="upstream_version_probe_failed",
|
||||
errorCode=exc.code,
|
||||
)
|
||||
else:
|
||||
version = payload.get("version") if isinstance(payload, dict) else None
|
||||
# Do not reflect arbitrary upstream data (or secret-like content).
|
||||
if isinstance(version, str) and len(version) <= 128:
|
||||
version = version.strip()
|
||||
match = self._semver.fullmatch(version)
|
||||
if match is not None:
|
||||
release = tuple(int(match.group(index)) for index in (1, 2, 3))
|
||||
prerelease = match.group(4)
|
||||
invalid_prerelease = prerelease is not None and any(
|
||||
part.isdigit() and len(part) > 1 and part.startswith("0")
|
||||
for part in prerelease.split(".")
|
||||
)
|
||||
if not invalid_prerelease:
|
||||
# Arbitrary SemVer suffixes can contain opaque credentials.
|
||||
# Display only the numeric release; negotiate the full parsed version.
|
||||
display_version = ("v" if version.startswith("v") else "") + ".".join(
|
||||
str(part) for part in release
|
||||
)
|
||||
diagnostics.update(
|
||||
upstreamVersion=display_version,
|
||||
normalizedVersion=display_version.removeprefix("v"),
|
||||
compatibility="UNSUPPORTED",
|
||||
reason="upstream_release_outside_tested_ranges",
|
||||
)
|
||||
supported = (
|
||||
(1, 14, 0) <= release < (1, 15, 0)
|
||||
or (1, 25, 0) <= release < (1, 28, 0)
|
||||
or (28, 0, 0) <= release < (29, 0, 0)
|
||||
)
|
||||
if prerelease is not None:
|
||||
diagnostics["reason"] = "upstream_prerelease_not_tested"
|
||||
elif supported:
|
||||
diagnostics.update(
|
||||
compatibility="SUPPORTED",
|
||||
reason="upstream_release_in_tested_ranges",
|
||||
mergeStrategyField=("Do" if release < (1, 26, 0) else "do"),
|
||||
)
|
||||
self._version_cache = (time.monotonic(), copy.deepcopy(diagnostics))
|
||||
return diagnostics
|
||||
|
||||
def _merge_strategy_field(self) -> str:
|
||||
"""Negotiate against the current upstream immediately before the merge write."""
|
||||
diagnostics = self.upstream_compatibility(force_refresh=True)
|
||||
if diagnostics["compatibility"] != "SUPPORTED":
|
||||
raise GiteaError(
|
||||
"GITEA_VERSION_UNSUPPORTED",
|
||||
diagnostics["errorCode"] or "GITEA_VERSION_UNSUPPORTED",
|
||||
(
|
||||
"Gitea merge compatibility requires an identified "
|
||||
"1.14, 1.25, 1.26, 1.27, or 28.x server"
|
||||
"1.14, 1.25, 1.26, 1.27, or 28.x stable server; "
|
||||
f"{diagnostics['reason']}"
|
||||
),
|
||||
category="compatibility",
|
||||
expected_state="Stable upstream release in tested compatibility ranges",
|
||||
actual_state=str(diagnostics["compatibility"]),
|
||||
recovery_hint="Inspect gitea.capabilities.get upstreamGitea diagnostics",
|
||||
)
|
||||
self._merge_strategy_key = (
|
||||
"Do" if legacy_match is not None and legacy_match.group(1) in {"14", "25"} else "do"
|
||||
)
|
||||
return self._merge_strategy_key
|
||||
return str(diagnostics["mergeStrategyField"])
|
||||
|
||||
def update_pull_request_branch(
|
||||
self,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""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.
|
||||
Native Gitea PR runs can omit their head branch. This client reads actual
|
||||
exact-head runs without a branch filter and checks available PR identity.
|
||||
Older status-only adapters retain an explicitly labeled legacy fallback;
|
||||
exact-head native reads never synthesize a workflow run or merge proof.
|
||||
|
||||
Large repositories can require longer than the historical 15-second client
|
||||
budget to commit files, apply changesets, or materialize a server-side squash
|
||||
@@ -15,6 +15,8 @@ base client and are not duplicated here.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
@@ -40,6 +42,174 @@ class GiteaClient(BaseGiteaClient):
|
||||
)
|
||||
super().__init__(resilient_settings, token_provider, transport=transport)
|
||||
|
||||
@staticmethod
|
||||
def matches_exact_head_action_run(
|
||||
run: dict[str, Any],
|
||||
*,
|
||||
branch: str,
|
||||
head_sha: str,
|
||||
pull_request_number: int | None = None,
|
||||
) -> bool:
|
||||
if run.get("head_sha") != head_sha:
|
||||
return False
|
||||
head_branch = run.get("head_branch", "")
|
||||
event = run.get("event")
|
||||
if head_branch is None and event == "pull_request":
|
||||
head_branch = ""
|
||||
if event == "workflow_dispatch" and head_branch == f"refs/heads/{branch}":
|
||||
head_branch = branch
|
||||
if not isinstance(head_branch, str) or head_branch not in {"", branch}:
|
||||
return False
|
||||
if not head_branch and event != "pull_request":
|
||||
return False
|
||||
path = run.get("path")
|
||||
path_number: int | None = None
|
||||
if path is not None:
|
||||
if not isinstance(path, str) or len(path) > 512 or path.count("@") > 1:
|
||||
return False
|
||||
if "@" in path:
|
||||
ref = path.split("@", 1)[1]
|
||||
match = re.fullmatch(r"refs/pull/([1-9][0-9]*)/head", ref)
|
||||
if match:
|
||||
path_number = int(match[1])
|
||||
if event != "pull_request" or path_number > 2_147_483_647:
|
||||
return False
|
||||
elif ref != f"refs/heads/{branch}" or event == "pull_request":
|
||||
return False
|
||||
raw_references = run.get("pull_requests")
|
||||
references = [] if raw_references is None else raw_references
|
||||
if not isinstance(references, list):
|
||||
return False
|
||||
numbers: list[int] = []
|
||||
for reference in references:
|
||||
if not isinstance(reference, dict):
|
||||
return False
|
||||
number = reference.get("number")
|
||||
if number is not None:
|
||||
if type(number) is not int or not 1 <= number <= 2_147_483_647:
|
||||
return False
|
||||
numbers.append(number)
|
||||
head = reference.get("head")
|
||||
if head is not None:
|
||||
if not isinstance(head, dict):
|
||||
return False
|
||||
if head.get("ref") not in (None, "", branch) or head.get("sha") not in (
|
||||
None,
|
||||
"",
|
||||
head_sha,
|
||||
):
|
||||
return False
|
||||
if event == "pull_request" and pull_request_number is not None:
|
||||
if path_number not in {None, pull_request_number} or any(
|
||||
number != pull_request_number for number in numbers
|
||||
):
|
||||
return False
|
||||
# Legacy adapters identify PR runs by a real branch and SHA. Native
|
||||
# blank-branch runs need affirmative PR number/ref evidence.
|
||||
if path_number != pull_request_number and pull_request_number not in numbers:
|
||||
return head_branch == branch and not path
|
||||
return True
|
||||
|
||||
def exact_head_action_runs(
|
||||
self,
|
||||
repository: RegisteredRepository,
|
||||
*,
|
||||
branch: str,
|
||||
head_sha: str,
|
||||
pull_request_number: int | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Read bounded actual run evidence; never synthesize a native validation."""
|
||||
if not re.fullmatch(r"(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})", head_sha):
|
||||
raise GiteaError("GITEA_TARGET_INVALID", "exact Actions head SHA is invalid")
|
||||
if pull_request_number is not None and (
|
||||
type(pull_request_number) is not int or not 1 <= pull_request_number <= 2_147_483_647
|
||||
):
|
||||
raise GiteaError("GITEA_TARGET_INVALID", "exact Actions PR number is invalid")
|
||||
output: list[dict[str, Any]] = []
|
||||
seen: set[tuple[int, int]] = set()
|
||||
expected_count: int | None = None
|
||||
deadline = time.monotonic() + 20
|
||||
for page in range(1, 11):
|
||||
if time.monotonic() >= deadline:
|
||||
raise GiteaError(
|
||||
"GITEA_ACTION_RUNS_TRUNCATED", "exact Actions read exceeded its bound"
|
||||
)
|
||||
payload = self._request(
|
||||
self._path("repos", repository.owner, repository.name, "actions", "runs"),
|
||||
{"head_sha": head_sha, "page": page, "limit": 100},
|
||||
timeout_seconds=min(10.0, max(0.1, deadline - time.monotonic())),
|
||||
max_attempts=1,
|
||||
)
|
||||
count = payload.get("total_count") if isinstance(payload, dict) else None
|
||||
if isinstance(payload, dict) and "total_count" in payload:
|
||||
if type(count) is not int or not 0 <= count <= 1000:
|
||||
raise GiteaError("GITEA_ACTION_RUNS_INVALID", "invalid exact Actions count")
|
||||
if expected_count is not None and count != expected_count:
|
||||
raise GiteaError("GITEA_ACTION_RUNS_TRUNCATED", "exact Actions count changed")
|
||||
expected_count = count
|
||||
elif expected_count is not None:
|
||||
raise GiteaError("GITEA_ACTION_RUNS_TRUNCATED", "exact Actions count disappeared")
|
||||
if isinstance(payload, dict):
|
||||
payload = (
|
||||
payload.get("workflow_runs")
|
||||
if "workflow_runs" in payload
|
||||
else payload.get("runs")
|
||||
)
|
||||
runs = self._as_list(payload)
|
||||
if len(runs) > 100:
|
||||
raise GiteaError("GITEA_ACTION_RUNS_INVALID", "excess exact Actions page")
|
||||
for run in runs:
|
||||
identifier, attempt = run.get("id"), run.get("run_attempt", 0)
|
||||
number = run.get("run_number", identifier)
|
||||
if (
|
||||
type(identifier) is not int
|
||||
or identifier < 1
|
||||
or type(attempt) is not int
|
||||
or attempt < 0
|
||||
or ("run_attempt" in run and attempt == 0)
|
||||
or type(number) is not int
|
||||
or number < 1
|
||||
or (identifier, attempt) in seen
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_ACTION_RUNS_INVALID", "invalid or repeated exact Actions run"
|
||||
)
|
||||
seen.add((identifier, attempt))
|
||||
output.append(run)
|
||||
if expected_count is not None:
|
||||
if len(output) > expected_count:
|
||||
raise GiteaError("GITEA_ACTION_RUNS_INVALID", "excess exact Actions runs")
|
||||
complete = len(output) == expected_count
|
||||
else:
|
||||
complete = len(runs) < 100
|
||||
if complete:
|
||||
if time.monotonic() >= deadline:
|
||||
raise GiteaError(
|
||||
"GITEA_ACTION_RUNS_TRUNCATED", "exact Actions read exceeded its bound"
|
||||
)
|
||||
matching = [
|
||||
run
|
||||
for run in output
|
||||
if self.matches_exact_head_action_run(
|
||||
run,
|
||||
branch=branch,
|
||||
head_sha=head_sha,
|
||||
pull_request_number=pull_request_number,
|
||||
)
|
||||
]
|
||||
return sorted(
|
||||
matching,
|
||||
key=lambda run: (
|
||||
run.get("run_number", run["id"]),
|
||||
run.get("run_attempt", 0),
|
||||
run["id"],
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
if not runs:
|
||||
raise GiteaError("GITEA_ACTION_RUNS_TRUNCATED", "incomplete exact Actions page")
|
||||
raise GiteaError("GITEA_ACTION_RUNS_TRUNCATED", "exact Actions pages exceeded their bound")
|
||||
|
||||
def action_runs(
|
||||
self,
|
||||
repository: RegisteredRepository,
|
||||
@@ -78,6 +248,11 @@ class GiteaClient(BaseGiteaClient):
|
||||
):
|
||||
return []
|
||||
|
||||
native_runs = self.exact_head_action_runs(repository, branch=branch, head_sha=head_sha)
|
||||
if native_runs:
|
||||
offset = (page - 1) * limit
|
||||
return native_runs[offset : offset + limit]
|
||||
|
||||
statuses = self.commit_statuses(repository, head_sha, 1, 100)
|
||||
if not statuses:
|
||||
return []
|
||||
|
||||
Reference in new issue
Block a user