Publish curated ITWorx_MCP_Hub source from 7fde2770ba344c684b7e7a6b74f763dc19e7c560
This commit is contained in:
1 parent
18cf749bf5
commit
89703431af
11 files changed
+1236
-103
No files matched your search
@@ -1,3 +1,3 @@
|
|||||||
# Public source export
|
# Public source export
|
||||||
|
|
||||||
This source snapshot was generated from the private canonical repository at revision `0253e7acef01ad952266ff180ed434e22b986ddc`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
This source snapshot was generated from the private canonical repository at revision `7fde2770ba344c684b7e7a6b74f763dc19e7c560`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||||
+25
-15
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"sourceRevision": "0253e7acef01ad952266ff180ed434e22b986ddc",
|
"sourceRevision": "7fde2770ba344c684b7e7a6b74f763dc19e7c560",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": ".dockerignore",
|
"path": ".dockerignore",
|
||||||
@@ -1759,8 +1759,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "connectors/gitea/server.py",
|
"path": "connectors/gitea/server.py",
|
||||||
"sha256": "4a07c6171893742d7a25e5cbdaa29eef438fc2d381ae93249a4bb6c1be493421",
|
"sha256": "bf1219aa38d650c5821167511137e9c6e27e21714436b430782f1c75418db441",
|
||||||
"bytes": 228331
|
"bytes": 232805
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "connectors/knowledge/__init__.py",
|
"path": "connectors/knowledge/__init__.py",
|
||||||
@@ -2169,13 +2169,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/connector_kit/gitea.py",
|
"path": "packages/connector_kit/gitea.py",
|
||||||
"sha256": "118e657d06142f71db40741b08d25504e30f13754bf0f67fc5f45f865cb41e2e",
|
"sha256": "fbc116935c7221ffcad4a41fddfa9c40a21a725364f8fa8bb52d9ef2ea9db333",
|
||||||
"bytes": 50270
|
"bytes": 54778
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/connector_kit/gitea_resilient.py",
|
"path": "packages/connector_kit/gitea_resilient.py",
|
||||||
"sha256": "78e6f6a43023721c686630f39107bda92b5c4cc77a978ed5bf5a105247abe72f",
|
"sha256": "83a27fdb883f25ab17d0ebea9f41c9fe61679cd2bfeea4535abdd057a17d1ab9",
|
||||||
"bytes": 4442
|
"bytes": 12348
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/connector_kit/knowledge.py",
|
"path": "packages/connector_kit/knowledge.py",
|
||||||
@@ -3239,8 +3239,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_gitea_connector.py",
|
"path": "tests/test_gitea_connector.py",
|
||||||
"sha256": "4fcd0e2a2aa822ca24958d3dcae2eb11f0c2c223da3fbdcc763631a57734b29f",
|
"sha256": "fe6d6d9e83f5f96239f22653e4f1815fe824cffdef2e7f8c26c52df61aa6fc1c",
|
||||||
"bytes": 135127
|
"bytes": 138362
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_gitea_merge_eligibility.py",
|
"path": "tests/test_gitea_merge_eligibility.py",
|
||||||
@@ -3254,14 +3254,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_gitea_merge_validation_fallback.py",
|
"path": "tests/test_gitea_merge_validation_fallback.py",
|
||||||
"sha256": "44a7825b2c9b591cb4c64f8dec539ed7c36298bdf7b2418e2855e56e2496a84d",
|
"sha256": "9ce7be992b4878209ee232ec1cf30681059284128a713088de47f2fa22c08b5c",
|
||||||
"bytes": 7467
|
"bytes": 6987
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_gitea_merge_version_compat.py",
|
"path": "tests/test_gitea_merge_version_compat.py",
|
||||||
"sha256": "8ae5c98a3056d2e1afe9c50fbd7b39f019e31cce744c21d89feeb513668d024e",
|
"sha256": "8ae5c98a3056d2e1afe9c50fbd7b39f019e31cce744c21d89feeb513668d024e",
|
||||||
"bytes": 2717
|
"bytes": 2717
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/test_gitea_native_pr_discovery.py",
|
||||||
|
"sha256": "46249e45eaa7bdd3810f803cd8ab8416983e442d44f40bbf5289ba2925fe448c",
|
||||||
|
"bytes": 18959
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_gitea_pr_status_fallback.py",
|
"path": "tests/test_gitea_pr_status_fallback.py",
|
||||||
"sha256": "7cc54cfb5172b657613fe26da1f08518eb43c79f7c180e264f8f263a9b80b5ee",
|
"sha256": "7cc54cfb5172b657613fe26da1f08518eb43c79f7c180e264f8f263a9b80b5ee",
|
||||||
@@ -3277,6 +3282,11 @@
|
|||||||
"sha256": "0b8357ea0a39319d80f5bcac8250de806866667329872d17429f2565cba6d8e0",
|
"sha256": "0b8357ea0a39319d80f5bcac8250de806866667329872d17429f2565cba6d8e0",
|
||||||
"bytes": 1861
|
"bytes": 1861
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/test_gitea_upstream_discovery.py",
|
||||||
|
"sha256": "184cd84aa7647989893c3f62b96bcd1df82a70c3fa8daced33532ded9aab38af",
|
||||||
|
"bytes": 9661
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_gitea_validation_dedup.py",
|
"path": "tests/test_gitea_validation_dedup.py",
|
||||||
"sha256": "8e29587c67183aaf5abdd5851b0b2a283074ecb33e953ebd75c7dcfce9b171d4",
|
"sha256": "8e29587c67183aaf5abdd5851b0b2a283074ecb33e953ebd75c7dcfce9b171d4",
|
||||||
@@ -3334,8 +3344,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_managed_validation_action_pins.py",
|
"path": "tests/test_managed_validation_action_pins.py",
|
||||||
"sha256": "8e4d3e81862570d281259b6eabadbe9568d2d48e56c240b1fa1da6a73efd3eb4",
|
"sha256": "e80aeed714214a611399d703dc72a114ea9dde987ce36e7e821768ae5cc6d327",
|
||||||
"bytes": 721
|
"bytes": 792
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_mcp_hub_ingestion_publisher.py",
|
"path": "tests/test_mcp_hub_ingestion_publisher.py",
|
||||||
@@ -3424,8 +3434,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_release_images_workflow.py",
|
"path": "tests/test_release_images_workflow.py",
|
||||||
"sha256": "92e1e03d71aaab8ef2f31eb6643d42b9e6189b9ccca86f1f0b55425d841512c2",
|
"sha256": "055e9adb93816464381e4de32b0c11e86d54555606a17041ba1ee52f36d73487",
|
||||||
"bytes": 2510
|
"bytes": 3063
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tests/test_stitch_connector.py",
|
"path": "tests/test_stitch_connector.py",
|
||||||
|
|||||||
+145
-46
@@ -1900,9 +1900,12 @@ class GiteaConnector:
|
|||||||
"status": str(item.get("status", ""))[:32],
|
"status": str(item.get("status", ""))[:32],
|
||||||
"conclusion": str(item.get("conclusion", ""))[:32],
|
"conclusion": str(item.get("conclusion", ""))[:32],
|
||||||
"event": str(item.get("event", ""))[:64],
|
"event": str(item.get("event", ""))[:64],
|
||||||
"headBranch": str(item.get("head_branch", ""))[:300],
|
"headBranch": str("" if item.get("head_branch") is None else item["head_branch"])[:300],
|
||||||
"headSha": str(item.get("head_sha", ""))[:64],
|
"headSha": str(item.get("head_sha", ""))[:64],
|
||||||
"workflowId": str(item.get("workflow_id", ""))[:2_048],
|
"workflowId": str(item.get("workflow_id") or "")[:2_048],
|
||||||
|
"workflowPath": str(item.get("path") or "")[:2_048],
|
||||||
|
"runNumber": max(int(item.get("run_number", item.get("id", 0))), 0),
|
||||||
|
"runAttempt": max(int(item.get("run_attempt", 0)), 0),
|
||||||
"displayTitle": safe_snippet(str(item.get("display_title", "")), max_bytes=500),
|
"displayTitle": safe_snippet(str(item.get("display_title", "")), max_bytes=500),
|
||||||
"createdAt": str(item.get("created_at", ""))[:64],
|
"createdAt": str(item.get("created_at", ""))[:64],
|
||||||
"updatedAt": str(item.get("updated_at", ""))[:64],
|
"updatedAt": str(item.get("updated_at", ""))[:64],
|
||||||
@@ -1932,14 +1935,33 @@ class GiteaConnector:
|
|||||||
identities = cls._workflow_identity_values(workflow)
|
identities = cls._workflow_identity_values(workflow)
|
||||||
if not identities:
|
if not identities:
|
||||||
return []
|
return []
|
||||||
if not any(str(item.get("workflow_id", "")).strip() for item in runs):
|
output: list[dict[str, Any]] = []
|
||||||
return runs if allow_legacy_without_workflow_id else []
|
for item in runs:
|
||||||
return [
|
raw_path = item.get("path")
|
||||||
item
|
workflow_id = str(item.get("workflow_id") or "").strip().casefold()
|
||||||
for item in runs
|
if raw_path is not None:
|
||||||
if str(item.get("workflow_id", "")).strip().casefold() in identities
|
if not isinstance(raw_path, str) or not raw_path or len(raw_path) > 512:
|
||||||
or str(item.get("workflow_id", "")).strip().casefold().rsplit("/", 1)[-1] in identities
|
continue
|
||||||
]
|
parts = raw_path.split("@")
|
||||||
|
if len(parts) > 2 or parts[0].casefold() not in identities:
|
||||||
|
continue
|
||||||
|
if len(parts) == 2 and not re.fullmatch(
|
||||||
|
r"refs/(?:pull/[1-9][0-9]*/head|heads/[A-Za-z0-9][A-Za-z0-9._/-]*)",
|
||||||
|
parts[1],
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
elif not workflow_id:
|
||||||
|
if (
|
||||||
|
allow_legacy_without_workflow_id
|
||||||
|
and isinstance(item.get("head_branch"), str)
|
||||||
|
and bool(item["head_branch"])
|
||||||
|
):
|
||||||
|
output.append(item)
|
||||||
|
continue
|
||||||
|
if workflow_id and workflow_id not in identities:
|
||||||
|
continue
|
||||||
|
output.append(item)
|
||||||
|
return output
|
||||||
|
|
||||||
def _find_reusable_validation_run(
|
def _find_reusable_validation_run(
|
||||||
self,
|
self,
|
||||||
@@ -1994,8 +2016,27 @@ class GiteaConnector:
|
|||||||
job_id = max(int(job.get("id", 0)), 0)
|
job_id = max(int(job.get("id", 0)), 0)
|
||||||
logs = ""
|
logs = ""
|
||||||
logs_truncated = False
|
logs_truncated = False
|
||||||
|
logs_unavailable = False
|
||||||
if include_logs and job_id:
|
if include_logs and job_id:
|
||||||
|
if str(job.get("status", "")).casefold() in {
|
||||||
|
"requested",
|
||||||
|
"queued",
|
||||||
|
"waiting",
|
||||||
|
"pending",
|
||||||
|
}:
|
||||||
|
# Gitea returns 404 before a job has an effective task/log.
|
||||||
|
logs = "[job not started; logs unavailable]"
|
||||||
|
logs_unavailable = True
|
||||||
|
elif job.get("conclusion") == "skipped" and not job.get("started_at"):
|
||||||
|
logs_unavailable = True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
logs, logs_truncated = self.client.action_job_logs(repository, job_id)
|
logs, logs_truncated = self.client.action_job_logs(repository, job_id)
|
||||||
|
except GiteaError as exc:
|
||||||
|
if exc.code != "GITEA_NOT_FOUND":
|
||||||
|
raise
|
||||||
|
logs_unavailable = True
|
||||||
|
else:
|
||||||
filtered_logs = safe_content(logs)
|
filtered_logs = safe_content(logs)
|
||||||
logs = filtered_logs or "[log excluded: secret-like content detected]"
|
logs = filtered_logs or "[log excluded: secret-like content detected]"
|
||||||
projected_jobs.append(
|
projected_jobs.append(
|
||||||
@@ -2008,6 +2049,7 @@ class GiteaConnector:
|
|||||||
"completedAt": str(job.get("completed_at", ""))[:64],
|
"completedAt": str(job.get("completed_at", ""))[:64],
|
||||||
"logs": logs,
|
"logs": logs,
|
||||||
"logsTruncated": logs_truncated,
|
"logsTruncated": logs_truncated,
|
||||||
|
"logsUnavailable": logs_unavailable,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
any_truncated = any_truncated or logs_truncated
|
any_truncated = any_truncated or logs_truncated
|
||||||
@@ -2182,12 +2224,41 @@ class GiteaConnector:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
workflow = _select_validation_workflow(self.client.action_workflows(repository))
|
workflow = _select_validation_workflow(self.client.action_workflows(repository))
|
||||||
|
commit_sha = str((branch_payload.get("commit") or {}).get("id", ""))[:64]
|
||||||
runs = self._runs_for_workflow(
|
runs = self._runs_for_workflow(
|
||||||
self.client.action_runs(repository, branch=safe_branch, page=1, limit=100), workflow
|
self.client.exact_head_action_runs(
|
||||||
|
repository,
|
||||||
|
branch=safe_branch,
|
||||||
|
head_sha=commit_sha,
|
||||||
|
pull_request_number=int(pull["number"]) if pull is not None else None,
|
||||||
|
),
|
||||||
|
workflow,
|
||||||
)
|
)
|
||||||
|
if not runs:
|
||||||
|
# Preserve honest stale branch history, never synthetic or current
|
||||||
|
# contradictory PR evidence, when the exact-head inventory is empty.
|
||||||
|
history = self.client.action_runs(repository, branch=safe_branch, page=1, limit=100)
|
||||||
|
history = [
|
||||||
|
item
|
||||||
|
for item in history
|
||||||
|
if (
|
||||||
|
type(item.get("id")) is int
|
||||||
|
and item["id"] > 0
|
||||||
|
and item.get("head_branch") == safe_branch
|
||||||
|
and isinstance(item.get("head_sha"), str)
|
||||||
|
and re.fullmatch(r"(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})", item["head_sha"])
|
||||||
|
and item["head_sha"] != commit_sha
|
||||||
|
and self.client.matches_exact_head_action_run(
|
||||||
|
item,
|
||||||
|
branch=safe_branch,
|
||||||
|
head_sha=item["head_sha"],
|
||||||
|
pull_request_number=int(pull["number"]) if pull is not None else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
runs = self._runs_for_workflow(history, workflow)
|
||||||
projected_runs = [self._action_run_projection(item) for item in runs]
|
projected_runs = [self._action_run_projection(item) for item in runs]
|
||||||
latest_run = projected_runs[0] if projected_runs else None
|
latest_run = projected_runs[0] if projected_runs else None
|
||||||
commit_sha = str((branch_payload.get("commit") or {}).get("id", ""))[:64]
|
|
||||||
merge_eligibility: dict[str, Any] = {
|
merge_eligibility: dict[str, Any] = {
|
||||||
"eligible": False,
|
"eligible": False,
|
||||||
"headSha": commit_sha,
|
"headSha": commit_sha,
|
||||||
@@ -2443,9 +2514,13 @@ class GiteaConnector:
|
|||||||
published_names = {str(item["name"]) for item in descriptors}
|
published_names = {str(item["name"]) for item in descriptors}
|
||||||
handler_names = set(self._tool_handlers())
|
handler_names = set(self._tool_handlers())
|
||||||
runtime = runtime_snapshot()
|
runtime = runtime_snapshot()
|
||||||
|
upstream = (self.write_client or self.client).upstream_compatibility()
|
||||||
|
merge_supported = upstream["compatibility"] == "SUPPORTED"
|
||||||
return {
|
return {
|
||||||
"server": "itworx-gitea",
|
"server": "itworx-gitea",
|
||||||
"version": self.version,
|
"version": self.version,
|
||||||
|
"connectorVersion": self.version,
|
||||||
|
"upstreamGitea": upstream,
|
||||||
"buildSha": os.environ.get("GITEA_CONNECTOR_BUILD_SHA", "unknown")[:64],
|
"buildSha": os.environ.get("GITEA_CONNECTOR_BUILD_SHA", "unknown")[:64],
|
||||||
"capabilityFingerprint": hashlib.sha256(encoded).hexdigest(),
|
"capabilityFingerprint": hashlib.sha256(encoded).hexdigest(),
|
||||||
"toolCount": len(publications),
|
"toolCount": len(publications),
|
||||||
@@ -2470,7 +2545,14 @@ class GiteaConnector:
|
|||||||
"catalog": "READY" if published_names == handler_names else "DEGRADED",
|
"catalog": "READY" if published_names == handler_names else "DEGRADED",
|
||||||
"dispatcher": "READY",
|
"dispatcher": "READY",
|
||||||
"worker": "READY" if runtime["circuitState"] == "CLOSED" else "DEGRADED",
|
"worker": "READY" if runtime["circuitState"] == "CLOSED" else "DEGRADED",
|
||||||
"upstream_gitea": "UNVERIFIED",
|
"upstream_gitea": (
|
||||||
|
"READY"
|
||||||
|
if merge_supported
|
||||||
|
else "UNAVAILABLE"
|
||||||
|
if upstream["compatibility"] == "UNAVAILABLE"
|
||||||
|
else "UNVERIFIED"
|
||||||
|
),
|
||||||
|
"merge": "READY" if merge_supported and self.write_client else "UNAVAILABLE",
|
||||||
"search": "READY",
|
"search": "READY",
|
||||||
"write": "READY" if self.write_client is not None else "DISABLED",
|
"write": "READY" if self.write_client is not None else "DISABLED",
|
||||||
"actions": "READY" if self.write_client is not None else "READ_ONLY",
|
"actions": "READY" if self.write_client is not None else "READ_ONLY",
|
||||||
@@ -2506,6 +2588,8 @@ class GiteaConnector:
|
|||||||
"capabilityFingerprint": capability["capabilityFingerprint"],
|
"capabilityFingerprint": capability["capabilityFingerprint"],
|
||||||
"toolCount": capability["toolCount"],
|
"toolCount": capability["toolCount"],
|
||||||
"credentialMode": "mounted-secret",
|
"credentialMode": "mounted-secret",
|
||||||
|
"connectorVersion": capability["connectorVersion"],
|
||||||
|
"upstreamGitea": capability["upstreamGitea"],
|
||||||
},
|
},
|
||||||
source="gitea:connection",
|
source="gitea:connection",
|
||||||
)
|
)
|
||||||
@@ -2533,6 +2617,13 @@ class GiteaConnector:
|
|||||||
"status": "PASS" if connection["connected"] else "FAIL",
|
"status": "PASS" if connection["connected"] else "FAIL",
|
||||||
"repositoryCount": connection["repositoryCount"],
|
"repositoryCount": connection["repositoryCount"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "upstream-merge-compatibility",
|
||||||
|
"status": "PASS"
|
||||||
|
if capability["upstreamGitea"]["compatibility"] == "SUPPORTED"
|
||||||
|
else "FAIL",
|
||||||
|
"diagnostics": capability["upstreamGitea"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "write-runtime",
|
"name": "write-runtime",
|
||||||
"status": "PASS" if self.write_client is not None else "DISABLED",
|
"status": "PASS" if self.write_client is not None else "DISABLED",
|
||||||
@@ -3930,32 +4021,24 @@ class GiteaConnector:
|
|||||||
):
|
):
|
||||||
raise GiteaError("GITEA_PROTECTION_DRIFT", "canonical PR check is not required")
|
raise GiteaError("GITEA_PROTECTION_DRIFT", "canonical PR check is not required")
|
||||||
|
|
||||||
|
workflow = _select_validation_workflow(self.client.action_workflows(repository))
|
||||||
|
safe_branch = _validate_ref(str(head.get("ref", "")))
|
||||||
|
|
||||||
def exact_runs() -> list[dict[str, Any]]:
|
def exact_runs() -> list[dict[str, Any]]:
|
||||||
runs: list[dict[str, Any]] = []
|
runs = self._runs_for_workflow(
|
||||||
for page in range(1, 6):
|
self.client.exact_head_action_runs(
|
||||||
batch = self.client.repository_action_runs(repository, page=page, limit=100)
|
repository,
|
||||||
runs.extend(batch)
|
branch=safe_branch,
|
||||||
if len(batch) < 100:
|
head_sha=expected_head_sha,
|
||||||
break
|
pull_request_number=number,
|
||||||
return sorted(
|
),
|
||||||
(
|
workflow,
|
||||||
|
)
|
||||||
|
return [
|
||||||
self._action_run_projection(item)
|
self._action_run_projection(item)
|
||||||
for item in runs
|
for item in runs
|
||||||
if str(item.get("event", "")) == "pull_request"
|
if item.get("event") == "pull_request"
|
||||||
and str(item.get("head_sha", "")) == expected_head_sha
|
]
|
||||||
and (
|
|
||||||
not isinstance(item.get("pull_requests"), list)
|
|
||||||
or not item["pull_requests"]
|
|
||||||
or any(
|
|
||||||
int(pr.get("number", 0)) == number
|
|
||||||
for pr in item["pull_requests"]
|
|
||||||
if isinstance(pr, dict)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
key=lambda item: int(item.get("id", 0)),
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
runs = exact_runs()
|
runs = exact_runs()
|
||||||
statuses = self.client.commit_statuses(repository, expected_head_sha, 1, 100)
|
statuses = self.client.commit_statuses(repository, expected_head_sha, 1, 100)
|
||||||
@@ -4337,10 +4420,16 @@ class GiteaConnector:
|
|||||||
"GITEA_PR_VALIDATION_REQUIRED",
|
"GITEA_PR_VALIDATION_REQUIRED",
|
||||||
"the required status has no verifiable canonical validation run",
|
"the required status has no verifiable canonical validation run",
|
||||||
)
|
)
|
||||||
run = self._action_run_projection(self.client.action_run(repository, int(run_match[1])))
|
raw_run = self.client.action_run(repository, int(run_match[1]))
|
||||||
|
run = self._action_run_projection(raw_run)
|
||||||
workflow = _select_validation_workflow(self.client.action_workflows(repository))
|
workflow = _select_validation_workflow(self.client.action_workflows(repository))
|
||||||
workflow_id = str(run.get("workflowId", "")).strip().casefold()
|
canonical_run = bool(self._runs_for_workflow([raw_run], workflow))
|
||||||
identities = self._workflow_identity_values(workflow or {})
|
canonical_run = canonical_run and self.client.matches_exact_head_action_run(
|
||||||
|
raw_run,
|
||||||
|
branch=str(head.get("ref", "")),
|
||||||
|
head_sha=expected_head_sha,
|
||||||
|
pull_request_number=number,
|
||||||
|
)
|
||||||
jobs = self.client.action_run_jobs(repository, int(run_match[1]))
|
jobs = self.client.action_run_jobs(repository, int(run_match[1]))
|
||||||
full_job = next((job for job in jobs if job.get("name") == "full"), None)
|
full_job = next((job for job in jobs if job.get("name") == "full"), None)
|
||||||
if (
|
if (
|
||||||
@@ -4349,11 +4438,7 @@ class GiteaConnector:
|
|||||||
or str(run.get("event", "")) != "pull_request"
|
or str(run.get("event", "")) != "pull_request"
|
||||||
or str(run.get("status", "")) != "completed"
|
or str(run.get("status", "")) != "completed"
|
||||||
or str(run.get("conclusion", "")) != "success"
|
or str(run.get("conclusion", "")) != "success"
|
||||||
or (
|
or not canonical_run
|
||||||
workflow_id
|
|
||||||
and workflow_id not in identities
|
|
||||||
and workflow_id.rsplit("/", 1)[-1] not in identities
|
|
||||||
)
|
|
||||||
or not isinstance(full_job, dict)
|
or not isinstance(full_job, dict)
|
||||||
or full_job.get("status") != "completed"
|
or full_job.get("status") != "completed"
|
||||||
or full_job.get("conclusion") != "success"
|
or full_job.get("conclusion") != "success"
|
||||||
@@ -4614,13 +4699,18 @@ class GiteaConnector:
|
|||||||
key=lambda item: str(item["name"]),
|
key=lambda item: str(item["name"]),
|
||||||
)
|
)
|
||||||
runtime = runtime_snapshot()
|
runtime = runtime_snapshot()
|
||||||
|
upstream = (self.write_client or self.client).upstream_compatibility()
|
||||||
snapshot = {
|
snapshot = {
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
"component": "gitea-connector",
|
"component": "gitea-connector",
|
||||||
"readOnly": self.write_client is None,
|
"readOnly": self.write_client is None,
|
||||||
"writeMode": "pr-first" if self.write_client is not None else "disabled",
|
"writeMode": "pr-first" if self.write_client is not None else "disabled",
|
||||||
"atomicChangesets": self.changeset_workspace is not None,
|
"atomicChangesets": self.changeset_workspace is not None,
|
||||||
"upstream": "reachable",
|
"upstream": "unavailable"
|
||||||
|
if upstream["compatibility"] == "UNAVAILABLE"
|
||||||
|
else "reachable",
|
||||||
|
"connectorVersion": self.version,
|
||||||
|
"upstreamGitea": upstream,
|
||||||
"toolCount": len(publications),
|
"toolCount": len(publications),
|
||||||
"capabilityFingerprint": hashlib.sha256(
|
"capabilityFingerprint": hashlib.sha256(
|
||||||
json.dumps(descriptors, sort_keys=True, separators=(",", ":")).encode()
|
json.dumps(descriptors, sort_keys=True, separators=(",", ":")).encode()
|
||||||
@@ -4632,7 +4722,16 @@ class GiteaConnector:
|
|||||||
"dispatcher": "READY",
|
"dispatcher": "READY",
|
||||||
"worker": ("READY" if runtime["circuitState"] == "CLOSED" else "DEGRADED"),
|
"worker": ("READY" if runtime["circuitState"] == "CLOSED" else "DEGRADED"),
|
||||||
"session": "RESUMABLE",
|
"session": "RESUMABLE",
|
||||||
"upstream_gitea": "READY",
|
"upstream_gitea": (
|
||||||
|
"READY"
|
||||||
|
if upstream["compatibility"] == "SUPPORTED"
|
||||||
|
else "UNAVAILABLE"
|
||||||
|
if upstream["compatibility"] == "UNAVAILABLE"
|
||||||
|
else "UNVERIFIED"
|
||||||
|
),
|
||||||
|
"merge": "READY"
|
||||||
|
if upstream["compatibility"] == "SUPPORTED" and self.write_client
|
||||||
|
else "UNAVAILABLE",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
with self._readiness_lock:
|
with self._readiness_lock:
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
from typing import Any, Protocol, cast
|
from typing import Any, Protocol, cast
|
||||||
@@ -331,6 +333,13 @@ class GiteaClient:
|
|||||||
"""Small bounded Gitea API client with same-origin redirect enforcement."""
|
"""Small bounded Gitea API client with same-origin redirect enforcement."""
|
||||||
|
|
||||||
_path_part = re.compile(r"^[^/\\?#\x00-\x1f\x7f]+$")
|
_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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -347,7 +356,8 @@ class GiteaClient:
|
|||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
transport=transport,
|
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:
|
def close(self) -> None:
|
||||||
self._client.close()
|
self._client.close()
|
||||||
@@ -813,27 +823,100 @@ class GiteaClient:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _merge_strategy_field(self) -> str:
|
def upstream_compatibility(self, *, force_refresh: bool = False) -> dict[str, Any]:
|
||||||
"""Resolve the versioned Gitea merge field before the single write attempt."""
|
"""Read the upstream version, independent of this connector's build version.
|
||||||
|
|
||||||
if self._merge_strategy_key is not None:
|
Diagnostics cache for 30 seconds; each merge negotiates again immediately
|
||||||
return self._merge_strategy_key
|
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"))
|
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
|
version = payload.get("version") if isinstance(payload, dict) else None
|
||||||
legacy_match = re.fullmatch(r"1\.(14|25|26|27)(?:\.\d+)?(?:[-+].*)?", str(version or ""))
|
# Do not reflect arbitrary upstream data (or secret-like content).
|
||||||
modern_match = re.fullmatch(r"(28)(?:\.\d+){1,2}(?:[-+].*)?", str(version or ""))
|
if isinstance(version, str) and len(version) <= 128:
|
||||||
if legacy_match is None and modern_match is None:
|
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(
|
raise GiteaError(
|
||||||
"GITEA_VERSION_UNSUPPORTED",
|
diagnostics["errorCode"] or "GITEA_VERSION_UNSUPPORTED",
|
||||||
(
|
(
|
||||||
"Gitea merge compatibility requires an identified "
|
"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 = (
|
return str(diagnostics["mergeStrategyField"])
|
||||||
"Do" if legacy_match is not None and legacy_match.group(1) in {"14", "25"} else "do"
|
|
||||||
)
|
|
||||||
return self._merge_strategy_key
|
|
||||||
|
|
||||||
def update_pull_request_branch(
|
def update_pull_request_branch(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Compatibility hardening for Gitea validation discovery and request latency.
|
"""Compatibility hardening for Gitea validation discovery and request latency.
|
||||||
|
|
||||||
Some Gitea versions expose pull-request check results as commit statuses while
|
Native Gitea PR runs can omit their head branch. This client reads actual
|
||||||
returning an empty branch-filtered Actions run wrapper. The base client treats
|
exact-head runs without a branch filter and checks available PR identity.
|
||||||
collections strictly; this subclass preserves that strictness while translating
|
Older status-only adapters retain an explicitly labeled legacy fallback;
|
||||||
a fully-green exact-head status set into one synthetic completed validation run.
|
exact-head native reads never synthesize a workflow run or merge proof.
|
||||||
|
|
||||||
Large repositories can require longer than the historical 15-second client
|
Large repositories can require longer than the historical 15-second client
|
||||||
budget to commit files, apply changesets, or materialize a server-side squash
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -40,6 +42,174 @@ class GiteaClient(BaseGiteaClient):
|
|||||||
)
|
)
|
||||||
super().__init__(resilient_settings, token_provider, transport=transport)
|
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(
|
def action_runs(
|
||||||
self,
|
self,
|
||||||
repository: RegisteredRepository,
|
repository: RegisteredRepository,
|
||||||
@@ -78,6 +248,11 @@ class GiteaClient(BaseGiteaClient):
|
|||||||
):
|
):
|
||||||
return []
|
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)
|
statuses = self.commit_statuses(repository, head_sha, 1, 100)
|
||||||
if not statuses:
|
if not statuses:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -699,6 +699,69 @@ class GiteaConnectorTests(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
connector_instance.client.close()
|
connector_instance.client.close()
|
||||||
|
|
||||||
|
def test_validation_logs_do_not_request_unstarted_jobs(self) -> None:
|
||||||
|
connector_instance, fixture = connector()
|
||||||
|
fixture._action_run.update(status="in_progress", conclusion="")
|
||||||
|
jobs = [
|
||||||
|
{"id": 1, "name": "backend", "status": "in_progress", "conclusion": ""},
|
||||||
|
*[
|
||||||
|
{"id": index, "name": status, "status": status, "conclusion": ""}
|
||||||
|
for index, status in enumerate(("requested", "queued", "waiting", "pending"), 2)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
def available_logs(_repository: RegisteredRepository, job_id: int) -> tuple[str, bool]:
|
||||||
|
if job_id != 1:
|
||||||
|
raise GiteaError("GITEA_NOT_FOUND", "Gitea resource was not found")
|
||||||
|
return "checks running", False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with (
|
||||||
|
patch.object(connector_instance.client, "action_run_jobs", return_value=jobs),
|
||||||
|
patch.object(
|
||||||
|
connector_instance.client, "action_job_logs", side_effect=available_logs
|
||||||
|
) as log_read,
|
||||||
|
):
|
||||||
|
result = connector_instance.validation_get(
|
||||||
|
context(), "hub-repository", 42, include_logs=True
|
||||||
|
)
|
||||||
|
projected = result["data"]["run"]["jobs"]
|
||||||
|
self.assertEqual(log_read.call_count, 1)
|
||||||
|
self.assertEqual(log_read.call_args.args[1], 1)
|
||||||
|
self.assertEqual(projected[0]["logs"], "checks running")
|
||||||
|
self.assertFalse(projected[0]["logsUnavailable"])
|
||||||
|
self.assertTrue(all(job["logsUnavailable"] for job in projected[1:]))
|
||||||
|
self.assertTrue(
|
||||||
|
all(job["logs"] == "[job not started; logs unavailable]" for job in projected[1:])
|
||||||
|
)
|
||||||
|
self.assertEqual(result["data"]["run"]["validationState"], "running")
|
||||||
|
self.assertNotIn("fixture-token", json.dumps(result))
|
||||||
|
finally:
|
||||||
|
connector_instance.client.close()
|
||||||
|
|
||||||
|
def test_validation_logs_preserve_started_job_errors(self) -> None:
|
||||||
|
connector_instance, _fixture = connector()
|
||||||
|
job = {"id": 1, "name": "backend", "status": "in_progress", "conclusion": ""}
|
||||||
|
try:
|
||||||
|
for code in ("GITEA_ACCESS_DENIED", "GITEA_UNAVAILABLE"):
|
||||||
|
with (
|
||||||
|
self.subTest(code=code),
|
||||||
|
patch.object(connector_instance.client, "action_run_jobs", return_value=[job]),
|
||||||
|
patch.object(
|
||||||
|
connector_instance.client,
|
||||||
|
"action_job_logs",
|
||||||
|
side_effect=GiteaError(code, "bounded upstream error"),
|
||||||
|
) as log_read,
|
||||||
|
self.assertRaises(GiteaError) as raised,
|
||||||
|
):
|
||||||
|
connector_instance.validation_get(
|
||||||
|
context(), "hub-repository", 42, include_logs=True
|
||||||
|
)
|
||||||
|
self.assertEqual(raised.exception.code, code)
|
||||||
|
self.assertEqual(log_read.call_count, 1)
|
||||||
|
finally:
|
||||||
|
connector_instance.client.close()
|
||||||
|
|
||||||
def test_repository_context_stays_below_connector_response_budget(self) -> None:
|
def test_repository_context_stays_below_connector_response_budget(self) -> None:
|
||||||
connector_instance, fixture = connector()
|
connector_instance, fixture = connector()
|
||||||
fixture._file = "R" * 100_000
|
fixture._file = "R" * 100_000
|
||||||
@@ -2028,7 +2091,7 @@ class GiteaConnectorTests(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
current = context()
|
current = context()
|
||||||
capabilities = connector_instance.capabilities_get(current)["data"]
|
capabilities = connector_instance.capabilities_get(current)["data"]
|
||||||
self.assertEqual(fixture.recorded_paths, [])
|
self.assertEqual(fixture.recorded_paths, ["/api/v1/version"])
|
||||||
self.assertEqual(capabilities["version"], "1.14.0")
|
self.assertEqual(capabilities["version"], "1.14.0")
|
||||||
self.assertEqual(capabilities["changeBranchPrefixes"], [])
|
self.assertEqual(capabilities["changeBranchPrefixes"], [])
|
||||||
self.assertEqual(capabilities["workBranchPolicy"], "any-valid-non-default-branch")
|
self.assertEqual(capabilities["workBranchPolicy"], "any-valid-non-default-branch")
|
||||||
@@ -2054,7 +2117,13 @@ class GiteaConnectorTests(unittest.TestCase):
|
|||||||
self.assertEqual(selftest["mutationsAttempted"], 0)
|
self.assertEqual(selftest["mutationsAttempted"], 0)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{item["name"] for item in selftest["checks"]},
|
{item["name"] for item in selftest["checks"]},
|
||||||
{"handler-parity", "gitea-connection", "write-runtime", "managed-actions"},
|
{
|
||||||
|
"handler-parity",
|
||||||
|
"gitea-connection",
|
||||||
|
"write-runtime",
|
||||||
|
"managed-actions",
|
||||||
|
"upstream-merge-compatibility",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
repository_health = connector_instance.repository_health(current, "hub-repository")[
|
repository_health = connector_instance.repository_health(current, "hub-repository")[
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from packages.connector_kit import GiteaError, PageRequest
|
from packages.connector_kit import GiteaError, PageRequest
|
||||||
@@ -66,27 +64,14 @@ def test_merge_uses_status_linked_run_when_actions_wrapper_is_empty() -> None:
|
|||||||
def test_merge_accepts_exact_head_managed_run_when_gitea_omits_workflow_id() -> None:
|
def test_merge_accepts_exact_head_managed_run_when_gitea_omits_workflow_id() -> None:
|
||||||
connector_instance, fixture = connector()
|
connector_instance, fixture = connector()
|
||||||
_add_validated_head_branch(fixture)
|
_add_validated_head_branch(fixture)
|
||||||
original_action_runs = connector_instance.client.action_runs
|
fixture._action_run.pop("workflow_id")
|
||||||
|
|
||||||
def action_runs_without_workflow_id(*args, **kwargs):
|
|
||||||
return [
|
|
||||||
{key: value for key, value in run.items() if key != "workflow_id"}
|
|
||||||
for run in original_action_runs(*args, **kwargs)
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with patch.object(
|
review = connector_instance.change_review(context(), "hub-repository", "codex/workflows")[
|
||||||
connector_instance.client,
|
"data"
|
||||||
"action_runs",
|
]
|
||||||
side_effect=action_runs_without_workflow_id,
|
|
||||||
):
|
|
||||||
review = connector_instance.change_review(
|
|
||||||
context(), "hub-repository", "codex/workflows"
|
|
||||||
)["data"]
|
|
||||||
assert review["readyToMerge"] is True
|
assert review["readyToMerge"] is True
|
||||||
assert review["validationRuns"][0]["id"] > 0
|
assert review["validationRuns"][0]["id"] > 0
|
||||||
assert review["validationRuns"][0]["workflowId"] == ""
|
assert review["validationRuns"][0]["workflowId"] == ""
|
||||||
|
|
||||||
merged = connector_instance.pull_request_merge(
|
merged = connector_instance.pull_request_merge(
|
||||||
context(), "hub-repository", 7, "e" * 40, "squash"
|
context(), "hub-repository", 7, "e" * 40, "squash"
|
||||||
)["data"]
|
)["data"]
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
"""Exact-head native Gitea Actions discovery without synthetic PR proof."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from packages.connector_kit import GiteaError
|
||||||
|
from tests.test_gitea_connector import connector, context
|
||||||
|
|
||||||
|
HEAD = "e" * 40
|
||||||
|
BRANCH = "codex/workflows"
|
||||||
|
|
||||||
|
|
||||||
|
def native_run(**changes):
|
||||||
|
return {
|
||||||
|
"id": 42,
|
||||||
|
"run_number": 17,
|
||||||
|
"run_attempt": 1,
|
||||||
|
"name": None,
|
||||||
|
"workflow_id": None,
|
||||||
|
"display_title": "ci: validate actual commit message",
|
||||||
|
"path": "managed-validation.yml@refs/pull/7/head",
|
||||||
|
"event": "pull_request",
|
||||||
|
"head_branch": "",
|
||||||
|
"head_sha": HEAD,
|
||||||
|
"status": "completed",
|
||||||
|
"conclusion": "success",
|
||||||
|
**changes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup_native(runs=None):
|
||||||
|
instance, fixture = connector()
|
||||||
|
fixture._branches.append({"name": BRANCH, "commit": {"id": HEAD}, "protected": False})
|
||||||
|
fixture._action_run = native_run()
|
||||||
|
native = runs if runs is not None else [fixture._action_run]
|
||||||
|
original = instance.client._request
|
||||||
|
requests = []
|
||||||
|
|
||||||
|
def request(path, *args, **kwargs):
|
||||||
|
if path.endswith("/actions/runs"):
|
||||||
|
query = args[0] if args else kwargs.get("params", {})
|
||||||
|
requests.append(dict(query))
|
||||||
|
if query.get("branch"):
|
||||||
|
return {"workflow_runs": [], "total_count": 0}
|
||||||
|
assert query.get("head_sha") == HEAD
|
||||||
|
return {"workflow_runs": copy.deepcopy(native), "total_count": len(native)}
|
||||||
|
return original(path, *args, **kwargs)
|
||||||
|
|
||||||
|
instance.client._request = request
|
||||||
|
return instance, fixture, requests
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_status_discovers_original_native_pr_run_with_null_workflow_id():
|
||||||
|
instance, _fixture, requests = setup_native()
|
||||||
|
try:
|
||||||
|
status = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert status["validationRuns"][0]["id"] == 42
|
||||||
|
assert status["validationRuns"][0]["workflowId"] == ""
|
||||||
|
assert status["validationRuns"][0]["workflowPath"] == native_run()["path"]
|
||||||
|
assert status["validationState"] == "succeeded"
|
||||||
|
assert status["readyToMerge"] is True
|
||||||
|
assert "run-validation" not in status["nextActions"]
|
||||||
|
assert requests and all("branch" not in item for item in requests)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_ensure_reuses_bound_native_canonical_pr_run():
|
||||||
|
instance, fixture, requests = setup_native()
|
||||||
|
try:
|
||||||
|
result = instance.pull_request_validation_ensure(context(), "hub-repository", 7, HEAD)[
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
assert result["state"] == "success"
|
||||||
|
assert result["runId"] == 42
|
||||||
|
assert result["reused"] is True
|
||||||
|
assert requests and all(item.get("head_sha") == HEAD for item in requests)
|
||||||
|
assert "POST" not in fixture.recorded_methods
|
||||||
|
assert "PATCH" not in fixture.recorded_methods
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"changes",
|
||||||
|
[
|
||||||
|
{"path": "managed-validation.yml", "pull_requests": []},
|
||||||
|
{"path": "security.yml@refs/pull/7/head"},
|
||||||
|
{"path": "managed-validation.yml@refs/pull/8/head"},
|
||||||
|
{"pull_requests": [{"number": 8}]},
|
||||||
|
{"workflow_id": "security.yml", "status": "queued", "conclusion": ""},
|
||||||
|
{"path": "security.yml@refs/pull/7/head", "conclusion": "failure"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validation_ensure_does_not_reuse_or_rerun_unbound_native_run(changes):
|
||||||
|
instance, fixture, _requests = setup_native([native_run(**changes)])
|
||||||
|
fixture._action_run.update(changes)
|
||||||
|
try:
|
||||||
|
with (
|
||||||
|
patch.object(instance._writer(), "rerun_action_run") as rerun,
|
||||||
|
patch.object(
|
||||||
|
instance._writer(),
|
||||||
|
"close_pull_request",
|
||||||
|
side_effect=GiteaError("GITEA_ACCESS_DENIED", "PR event trigger unavailable"),
|
||||||
|
) as close,
|
||||||
|
pytest.raises(GiteaError) as error,
|
||||||
|
):
|
||||||
|
instance.pull_request_validation_ensure(context(), "hub-repository", 7, HEAD)
|
||||||
|
assert error.value.code == "GITEA_ACCESS_DENIED"
|
||||||
|
close.assert_called_once()
|
||||||
|
rerun.assert_not_called()
|
||||||
|
assert "POST" not in fixture.recorded_methods
|
||||||
|
assert "PATCH" not in fixture.recorded_methods
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"changes",
|
||||||
|
[
|
||||||
|
{"head_sha": "d" * 40},
|
||||||
|
{"head_branch": "other"},
|
||||||
|
{"path": "managed-validation.yml@refs/pull/8/head"},
|
||||||
|
{"pull_requests": [{"number": 8}]},
|
||||||
|
{"pull_requests": [{"number": 7, "head": {"ref": "other", "sha": HEAD}}]},
|
||||||
|
{"pull_requests": [{"number": 7, "head": {"ref": BRANCH, "sha": "d" * 40}}]},
|
||||||
|
{"pull_requests": "malformed"},
|
||||||
|
{"pull_requests": {}},
|
||||||
|
{"pull_requests": [{"number": 7, "head": {"ref": [], "sha": HEAD}}]},
|
||||||
|
{"path": "managed-validation.yml", "pull_requests": []},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_native_exact_head_read_rejects_contradictory_or_unbound_pr_metadata(changes):
|
||||||
|
instance, fixture, _requests = setup_native([native_run(**changes)])
|
||||||
|
fixture._action_run.update(changes)
|
||||||
|
try:
|
||||||
|
repository = instance._repo(context(), "hub-repository")
|
||||||
|
assert (
|
||||||
|
instance.client.exact_head_action_runs(
|
||||||
|
repository,
|
||||||
|
branch=BRANCH,
|
||||||
|
head_sha=HEAD,
|
||||||
|
pull_request_number=7,
|
||||||
|
)
|
||||||
|
== []
|
||||||
|
)
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.pull_request_merge(context(), "hub-repository", 7, HEAD, "squash")
|
||||||
|
assert error.value.code == "GITEA_PR_VALIDATION_REQUIRED"
|
||||||
|
assert not any(body.get("Do") == "squash" for body in fixture.recorded_bodies)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path",
|
||||||
|
[
|
||||||
|
"security.yml@refs/pull/7/head",
|
||||||
|
"untrusted/managed-validation.yml@refs/pull/7/head",
|
||||||
|
"managed-validation.yml@refs/pull/7/head@extra",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_native_workflow_title_cannot_substitute_for_canonical_path(path):
|
||||||
|
instance, fixture, _requests = setup_native([native_run(path=path)])
|
||||||
|
fixture._action_run["path"] = path
|
||||||
|
try:
|
||||||
|
status = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert status["validationRuns"] == []
|
||||||
|
assert status["readyToMerge"] is False
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.pull_request_merge(context(), "hub-repository", 7, HEAD, "squash")
|
||||||
|
assert error.value.code == "GITEA_PR_VALIDATION_REQUIRED"
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_newer_native_failed_attempt_controls_validation_projection():
|
||||||
|
runs = [native_run(), native_run(id=44, run_attempt=2, conclusion="failure")]
|
||||||
|
instance, fixture, _requests = setup_native(runs)
|
||||||
|
fixture._statuses[0]["status"] = "failure"
|
||||||
|
try:
|
||||||
|
status = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert status["validationRuns"][0]["id"] == 44
|
||||||
|
assert status["validationRuns"][0]["runAttempt"] == 2
|
||||||
|
assert status["readyToMerge"] is False
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("failure", ["duplicate", "empty", "changed", "overflow"])
|
||||||
|
def test_native_inventory_rejects_incomplete_or_nonadvancing_pages(failure):
|
||||||
|
instance, _fixture, _requests = setup_native()
|
||||||
|
original = instance.client._request
|
||||||
|
|
||||||
|
def request(path, *args, **kwargs):
|
||||||
|
if path.endswith("/actions/runs"):
|
||||||
|
query = args[0]
|
||||||
|
if query["page"] == 1:
|
||||||
|
return {"workflow_runs": [native_run()], "total_count": 2}
|
||||||
|
if failure == "duplicate":
|
||||||
|
return {"workflow_runs": [native_run()], "total_count": 2}
|
||||||
|
if failure == "empty":
|
||||||
|
return {"workflow_runs": [], "total_count": 2}
|
||||||
|
if failure == "changed":
|
||||||
|
return {"workflow_runs": [native_run(id=43)], "total_count": 3}
|
||||||
|
return {"workflow_runs": [native_run(id=43), native_run(id=44)], "total_count": 2}
|
||||||
|
return original(path, *args, **kwargs)
|
||||||
|
|
||||||
|
instance.client._request = request
|
||||||
|
try:
|
||||||
|
repository = instance._repo(context(), "hub-repository")
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.client.exact_head_action_runs(
|
||||||
|
repository,
|
||||||
|
branch=BRANCH,
|
||||||
|
head_sha=HEAD,
|
||||||
|
pull_request_number=7,
|
||||||
|
)
|
||||||
|
assert error.value.code in {"GITEA_ACTION_RUNS_INVALID", "GITEA_ACTION_RUNS_TRUNCATED"}
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_get_preserves_skipped_job_metadata_without_missing_logs():
|
||||||
|
instance, _fixture = connector()
|
||||||
|
jobs = [
|
||||||
|
{"id": 43, "name": "preflight", "status": "completed", "conclusion": "failure"},
|
||||||
|
{"id": 44, "name": "build", "status": "completed", "conclusion": "skipped"},
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
with (
|
||||||
|
patch.object(instance.client, "action_run_jobs", return_value=jobs),
|
||||||
|
patch.object(
|
||||||
|
instance.client,
|
||||||
|
"action_job_logs",
|
||||||
|
return_value=("preflight failed", False),
|
||||||
|
) as logs,
|
||||||
|
):
|
||||||
|
result = instance.validation_get(context(), "hub-repository", 42, True)["data"]["run"]
|
||||||
|
assert len(result["jobs"]) == 2
|
||||||
|
assert result["jobs"][1]["conclusion"] == "skipped"
|
||||||
|
assert result["jobs"][1]["logsUnavailable"] is True
|
||||||
|
assert logs.call_count == 1
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_get_marks_expected_missing_logs_without_suppressing_other_errors():
|
||||||
|
instance, _fixture = connector()
|
||||||
|
try:
|
||||||
|
with patch.object(
|
||||||
|
instance.client,
|
||||||
|
"action_job_logs",
|
||||||
|
side_effect=GiteaError(
|
||||||
|
"GITEA_NOT_FOUND",
|
||||||
|
"logs unavailable",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = instance.validation_get(context(), "hub-repository", 42, True)["data"]["run"]
|
||||||
|
assert result["jobs"][0]["logsUnavailable"] is True
|
||||||
|
assert result["jobs"][0]["status"] == "completed"
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
instance.client,
|
||||||
|
"action_job_logs",
|
||||||
|
side_effect=GiteaError(
|
||||||
|
"GITEA_ACCESS_DENIED",
|
||||||
|
"access denied",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.raises(GiteaError) as error,
|
||||||
|
):
|
||||||
|
instance.validation_get(context(), "hub-repository", 42, True)
|
||||||
|
assert error.value.code == "GITEA_ACCESS_DENIED"
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_branch_read_prefers_actual_native_run_before_status_fallback():
|
||||||
|
instance, _fixture, _requests = setup_native()
|
||||||
|
try:
|
||||||
|
repository = instance._repo(context(), "hub-repository")
|
||||||
|
runs = instance.client.action_runs(repository, branch=BRANCH, page=1, limit=25)
|
||||||
|
assert len(runs) == 1 and runs[0]["id"] == 42
|
||||||
|
assert "validation_evidence" not in runs[0]
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_read_walks_all_actual_pages_and_returns_newest_attempt_first():
|
||||||
|
instance, _fixture, _requests = setup_native()
|
||||||
|
original = instance.client._request
|
||||||
|
seen = []
|
||||||
|
|
||||||
|
def request(path, *args, **kwargs):
|
||||||
|
if path.endswith("/actions/runs"):
|
||||||
|
page = args[0]["page"]
|
||||||
|
seen.append(page)
|
||||||
|
assert kwargs["max_attempts"] == 1 and 0 < kwargs["timeout_seconds"] <= 10
|
||||||
|
return {"workflow_runs": [native_run(id=41 + page, run_attempt=page)], "total_count": 2}
|
||||||
|
return original(path, *args, **kwargs)
|
||||||
|
|
||||||
|
instance.client._request = request
|
||||||
|
try:
|
||||||
|
repository = instance._repo(context(), "hub-repository")
|
||||||
|
runs = instance.client.exact_head_action_runs(
|
||||||
|
repository, branch=BRANCH, head_sha=HEAD, pull_request_number=7
|
||||||
|
)
|
||||||
|
assert seen == [1, 2] and [run["id"] for run in runs] == [43, 42]
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("changes", [{"id": True}, {"run_attempt": 0}, {"run_number": None}])
|
||||||
|
def test_native_inventory_rejects_malformed_run_ordering(changes):
|
||||||
|
instance, _fixture, _requests = setup_native([native_run(**changes)])
|
||||||
|
try:
|
||||||
|
repository = instance._repo(context(), "hub-repository")
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.client.exact_head_action_runs(
|
||||||
|
repository, branch=BRANCH, head_sha=HEAD, pull_request_number=7
|
||||||
|
)
|
||||||
|
assert error.value.code == "GITEA_ACTION_RUNS_INVALID"
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("length", [39, 41, 63, 65])
|
||||||
|
def test_native_read_rejects_non_git_sha_lengths(length):
|
||||||
|
instance, fixture, _requests = setup_native()
|
||||||
|
try:
|
||||||
|
repository = instance._repo(context(), "hub-repository")
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.client.exact_head_action_runs(repository, branch=BRANCH, head_sha="e" * length)
|
||||||
|
assert error.value.code == "GITEA_TARGET_INVALID"
|
||||||
|
assert not fixture.recorded_bodies
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_exact_branch_ref_remains_bound_to_original_run():
|
||||||
|
instance, fixture = connector()
|
||||||
|
try:
|
||||||
|
fixture._branches.append({"name": BRANCH, "commit": {"id": HEAD}, "protected": False})
|
||||||
|
dispatched = instance.validation_run(context(), "hub-repository", BRANCH, "full")["data"]
|
||||||
|
assert fixture._action_run["head_branch"] == f"refs/heads/{BRANCH}"
|
||||||
|
result = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert result["validationRuns"][0]["id"] == dispatched["id"] == 42
|
||||||
|
bad = dict(fixture._action_run, head_branch="refs/heads/other")
|
||||||
|
assert not instance.client.matches_exact_head_action_run(bad, branch=BRANCH, head_sha=HEAD)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_blank_native_pr_number_without_workflow_identity_cannot_authorize_merge():
|
||||||
|
run = native_run(path=None, workflow_id=None, pull_requests=[{"number": 7}])
|
||||||
|
instance, fixture, _requests = setup_native([run])
|
||||||
|
fixture._action_run = dict(run)
|
||||||
|
try:
|
||||||
|
result = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert result["validationRuns"] == []
|
||||||
|
assert result["readyToMerge"] is False
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.pull_request_merge(context(), "hub-repository", 7, HEAD, "squash")
|
||||||
|
assert error.value.code == "GITEA_PR_VALIDATION_REQUIRED"
|
||||||
|
assert not any(body.get("Do") == "squash" for body in fixture.recorded_bodies)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_missing_workflow_identity_requires_exact_expected_branch_for_merge():
|
||||||
|
instance, fixture = connector()
|
||||||
|
fixture._action_run.pop("workflow_id", None)
|
||||||
|
fixture._action_run["head_branch"] = "other"
|
||||||
|
try:
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.pull_request_merge(context(), "hub-repository", 7, HEAD, "squash")
|
||||||
|
assert error.value.code == "GITEA_PR_VALIDATION_REQUIRED"
|
||||||
|
assert not any(body.get("Do") == "squash" for body in fixture.recorded_bodies)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_actual_run_22404_null_head_branch_retains_pr236_original_proof():
|
||||||
|
instance, fixture = connector()
|
||||||
|
actual_head = "c19a8620de6a24241b63aae4dbe52c0e5505aa62"
|
||||||
|
run = native_run(
|
||||||
|
id=22404,
|
||||||
|
head_sha=actual_head,
|
||||||
|
head_branch=None,
|
||||||
|
run_number=1177,
|
||||||
|
path="managed-validation.yml@refs/pull/236/head",
|
||||||
|
)
|
||||||
|
fixture._action_run = run
|
||||||
|
fixture._pulls[0]["number"] = 236
|
||||||
|
fixture._pulls[0]["head"]["sha"] = actual_head
|
||||||
|
fixture._branches.append({"name": BRANCH, "commit": {"id": actual_head}, "protected": False})
|
||||||
|
fixture._statuses[0]["target_url"] = "https://gitea.example/acme/hub/actions/runs/22404"
|
||||||
|
original_request = instance.client._request
|
||||||
|
# The general fake transport serves only PR7; preserve actual PR236 payload.
|
||||||
|
instance.client._request = lambda path, *args, **kwargs: original_request(
|
||||||
|
path.replace("/pulls/236", "/pulls/7"), *args, **kwargs
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert result["validationRuns"][0]["id"] == 22404
|
||||||
|
assert result["validationRuns"][0]["headBranch"] == ""
|
||||||
|
assert result["validationRuns"][0]["workflowPath"] == run["path"]
|
||||||
|
assert result["readyToMerge"] is True
|
||||||
|
merged = instance.pull_request_merge(
|
||||||
|
context(), "hub-repository", 236, actual_head, "squash"
|
||||||
|
)["data"]
|
||||||
|
assert merged["validationRunId"] == 22404
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("branch_metadata", [None, False, 0, [], {}])
|
||||||
|
def test_native_nonstring_branch_requires_null_pr_shape_and_workflow_identity(branch_metadata):
|
||||||
|
run = native_run(head_branch=branch_metadata, path=None, pull_requests=[{"number": 7}])
|
||||||
|
instance, fixture, _requests = setup_native([run])
|
||||||
|
fixture._action_run = dict(run)
|
||||||
|
try:
|
||||||
|
result = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert result["validationRuns"] == []
|
||||||
|
assert result["readyToMerge"] is False
|
||||||
|
with pytest.raises(GiteaError):
|
||||||
|
instance.pull_request_merge(context(), "hub-repository", 7, HEAD, "squash")
|
||||||
|
assert not any(body.get("Do") == "squash" for body in fixture.recorded_bodies)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_null_native_branch_does_not_hide_failed_required_workflow_context():
|
||||||
|
instance, fixture, _requests = setup_native([native_run(head_branch=None)])
|
||||||
|
fixture._action_run["head_branch"] = None
|
||||||
|
fixture._branch_protections["acme/hub"][0]["status_check_contexts"].append(
|
||||||
|
"Security / scan (pull_request)"
|
||||||
|
)
|
||||||
|
fixture._statuses.append({"context": "Security / scan (pull_request)", "status": "failure"})
|
||||||
|
try:
|
||||||
|
result = instance.change_status(context(), "hub-repository", BRANCH)["data"]
|
||||||
|
assert result["validationRuns"][0]["id"] == 42
|
||||||
|
assert result["readyToMerge"] is False
|
||||||
|
with pytest.raises(GiteaError) as error:
|
||||||
|
instance.pull_request_merge(context(), "hub-repository", 7, HEAD, "squash")
|
||||||
|
assert error.value.code == "GITEA_REQUIRED_CHECK_BLOCKED"
|
||||||
|
assert not any(body.get("Do") == "squash" for body in fixture.recorded_bodies)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_null_branch_is_not_absent_branch_for_push_or_dispatch():
|
||||||
|
instance, fixture = connector()
|
||||||
|
try:
|
||||||
|
for event in ("push", "workflow_dispatch"):
|
||||||
|
assert not instance.client.matches_exact_head_action_run(
|
||||||
|
native_run(head_branch=None, event=event), branch=BRANCH, head_sha=HEAD
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
"""Version negotiation canary: read-only diagnostics and fail-closed merge gates."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from connectors.gitea import FakeGiteaServer
|
||||||
|
from packages.connector_kit import GiteaError
|
||||||
|
from tests.test_gitea_connector import connector, context
|
||||||
|
from tests.test_gitea_merge_version_compat import _client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("version", "normalized", "field"),
|
||||||
|
[
|
||||||
|
("28.0.0", "28.0.0", "do"),
|
||||||
|
(" v28.0.0+gitea.1 ", "28.0.0", "do"),
|
||||||
|
("v1.25.5+vendor.7", "1.25.5", "Do"),
|
||||||
|
("1.26.4", "1.26.4", "do"),
|
||||||
|
("1.27.2", "1.27.2", "do"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_upstream_discovery_normalizes_verified_release(version, normalized, field) -> None:
|
||||||
|
client, fixture, _ = _client(version)
|
||||||
|
try:
|
||||||
|
diagnostics = client.upstream_compatibility()
|
||||||
|
assert diagnostics["upstreamVersion"] == version.strip().split("+", 1)[0]
|
||||||
|
assert diagnostics["normalizedVersion"] == normalized
|
||||||
|
assert diagnostics["compatibility"] == "SUPPORTED"
|
||||||
|
assert diagnostics["mergeStrategyField"] == field
|
||||||
|
assert diagnostics["source"] == "/api/v1/version"
|
||||||
|
assert fixture.recorded_methods == ["GET"]
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"metadata",
|
||||||
|
[
|
||||||
|
"AKIA" + "A" * 16,
|
||||||
|
"glpat-" + "x" * 24,
|
||||||
|
"eyJ" + "a" * 15 + "." + "b" * 15 + "." + "c" * 15,
|
||||||
|
"fixture-token",
|
||||||
|
"a1" * 20,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize("separator", ["+", "-"])
|
||||||
|
def test_version_diagnostics_exclude_secret_shaped_metadata(metadata, separator):
|
||||||
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0" + separator + metadata))
|
||||||
|
try:
|
||||||
|
data = instance.capabilities_get(context())["data"]
|
||||||
|
diagnostics = data["upstreamGitea"]
|
||||||
|
assert metadata not in json.dumps(data)
|
||||||
|
assert diagnostics["upstreamVersion"] == "28.0.0"
|
||||||
|
assert diagnostics["normalizedVersion"] == "28.0.0"
|
||||||
|
assert diagnostics["compatibility"] == ("SUPPORTED" if separator == "+" else "UNSUPPORTED")
|
||||||
|
assert "POST" not in fixture.recorded_methods
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("version", "verdict"),
|
||||||
|
[
|
||||||
|
("1.24.9", "UNSUPPORTED"),
|
||||||
|
("1.28.0", "UNSUPPORTED"),
|
||||||
|
("2.0.0", "UNSUPPORTED"),
|
||||||
|
("29.0.0", "UNSUPPORTED"),
|
||||||
|
("28.0.0-rc.1", "UNSUPPORTED"),
|
||||||
|
("", "UNKNOWN"),
|
||||||
|
("garbage", "UNKNOWN"),
|
||||||
|
("28.0.0+", "UNKNOWN"),
|
||||||
|
("28.0.0-", "UNKNOWN"),
|
||||||
|
("28.00.0", "UNKNOWN"),
|
||||||
|
("28.0.0\nforged", "UNKNOWN"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_unknown_or_untested_upstream_fails_before_write(version, verdict) -> None:
|
||||||
|
client, fixture, repository = _client(version)
|
||||||
|
try:
|
||||||
|
diagnostics = client.upstream_compatibility()
|
||||||
|
assert diagnostics["compatibility"] == verdict
|
||||||
|
assert diagnostics["mergeStrategyField"] is None
|
||||||
|
with pytest.raises(GiteaError) as raised:
|
||||||
|
client.merge_pull_request(repository, 7, method="squash", head_commit_id="e" * 40)
|
||||||
|
assert raised.value.code == "GITEA_VERSION_UNSUPPORTED"
|
||||||
|
assert "POST" not in fixture.recorded_methods
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("payload", [{}, {"version": None}, {"version": 28}, []])
|
||||||
|
def test_missing_or_non_string_upstream_version_cannot_fall_back_to_connector(payload) -> None:
|
||||||
|
instance, fixture = connector()
|
||||||
|
try:
|
||||||
|
with patch.object(instance.client, "_request", return_value=payload):
|
||||||
|
data = instance.capabilities_get(context())["data"]
|
||||||
|
assert data["connectorVersion"] == "1.14.0"
|
||||||
|
assert data["upstreamGitea"]["compatibility"] == "UNKNOWN"
|
||||||
|
assert data["upstreamGitea"]["upstreamVersion"] is None
|
||||||
|
assert data["subsystems"]["upstream_gitea"] == "UNVERIFIED"
|
||||||
|
assert data["subsystems"]["merge"] == "UNAVAILABLE"
|
||||||
|
assert fixture.recorded_methods == []
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_connector_and_server_versions_are_distinct_in_read_only_canary() -> None:
|
||||||
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0"))
|
||||||
|
try:
|
||||||
|
data = instance.capabilities_get(context())["data"]
|
||||||
|
assert data["version"] == data["connectorVersion"] == "1.14.0"
|
||||||
|
assert data["upstreamGitea"]["upstreamVersion"] == "28.0.0"
|
||||||
|
assert data["upstreamGitea"]["compatibility"] == "SUPPORTED"
|
||||||
|
assert data["subsystems"]["upstream_gitea"] == "READY"
|
||||||
|
assert data["subsystems"]["merge"] == "READY"
|
||||||
|
canary = instance.selftest_run(context())["data"]
|
||||||
|
assert canary["status"] == "PASS"
|
||||||
|
assert any(item["name"] == "upstream-merge-compatibility" for item in canary["checks"])
|
||||||
|
assert "POST" not in fixture.recorded_methods
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_diagnostics_cache_expires_and_rechecks_after_dependency_recovery() -> None:
|
||||||
|
client, fixture, _ = _client("28.0.0")
|
||||||
|
try:
|
||||||
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=100):
|
||||||
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
||||||
|
fixture.gitea_version = "29.0.0"
|
||||||
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=120):
|
||||||
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
||||||
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=131):
|
||||||
|
assert client.upstream_compatibility()["compatibility"] == "UNSUPPORTED"
|
||||||
|
fixture.gitea_version = "28.0.0"
|
||||||
|
with patch("packages.connector_kit.gitea.time.monotonic", return_value=162):
|
||||||
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
||||||
|
assert fixture.recorded_paths.count("/api/v1/version") == 3
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_rechecks_server_even_with_cached_successful_diagnostics() -> None:
|
||||||
|
client, fixture, repository = _client("28.0.0")
|
||||||
|
try:
|
||||||
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
||||||
|
fixture.gitea_version = "29.0.0"
|
||||||
|
with pytest.raises(GiteaError) as raised:
|
||||||
|
client.merge_pull_request(repository, 7, method="squash", head_commit_id="e" * 40)
|
||||||
|
assert raised.value.code == "GITEA_VERSION_UNSUPPORTED"
|
||||||
|
assert "POST" not in fixture.recorded_methods
|
||||||
|
assert fixture.recorded_paths.count("/api/v1/version") == 2
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_outage_invalidates_cached_success_and_recovers() -> None:
|
||||||
|
client, fixture, repository = _client("28.0.0")
|
||||||
|
try:
|
||||||
|
assert client.upstream_compatibility()["compatibility"] == "SUPPORTED"
|
||||||
|
with patch.object(
|
||||||
|
client,
|
||||||
|
"_request",
|
||||||
|
side_effect=GiteaError("GITEA_UNAVAILABLE", "upstream request unavailable"),
|
||||||
|
):
|
||||||
|
with pytest.raises(GiteaError) as raised:
|
||||||
|
client.merge_pull_request(repository, 7, method="squash", head_commit_id="e" * 40)
|
||||||
|
assert raised.value.code == "GITEA_UNAVAILABLE"
|
||||||
|
assert client.upstream_compatibility()["compatibility"] == "UNAVAILABLE"
|
||||||
|
assert "POST" not in fixture.recorded_methods
|
||||||
|
assert client.upstream_compatibility(force_refresh=True)["compatibility"] == "SUPPORTED"
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_readiness_reports_failed_version_probe_without_disabling_read_service() -> None:
|
||||||
|
instance, _ = connector()
|
||||||
|
original_request = instance.client._request
|
||||||
|
|
||||||
|
def request(path, *args, **kwargs):
|
||||||
|
if path == "version":
|
||||||
|
raise GiteaError("GITEA_UNAVAILABLE", "upstream request unavailable")
|
||||||
|
return original_request(path, *args, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch.object(instance.client, "_request", side_effect=request):
|
||||||
|
data = instance.readiness()
|
||||||
|
assert data["status"] == "ready"
|
||||||
|
assert data["upstream"] == "unavailable"
|
||||||
|
assert data["subsystems"]["upstream_gitea"] == "UNAVAILABLE"
|
||||||
|
assert data["subsystems"]["merge"] == "UNAVAILABLE"
|
||||||
|
assert data["upstreamGitea"]["errorCode"] == "GITEA_UNAVAILABLE"
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mutation", ["wrong-head", "wrong-event", "failed"])
|
||||||
|
def test_current_version_still_requires_exact_successful_pr_validation(mutation) -> None:
|
||||||
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0"))
|
||||||
|
if mutation == "wrong-head":
|
||||||
|
fixture._action_run["head_sha"] = "d" * 40
|
||||||
|
elif mutation == "wrong-event":
|
||||||
|
fixture._action_run["event"] = "workflow_dispatch"
|
||||||
|
else:
|
||||||
|
fixture._action_run["conclusion"] = "failure"
|
||||||
|
try:
|
||||||
|
with pytest.raises(GiteaError):
|
||||||
|
instance.pull_request_merge(context(), "hub-repository", 7, "e" * 40, "squash")
|
||||||
|
assert "/api/v1/repos/acme/hub/pulls/7/merge" not in fixture.recorded_paths
|
||||||
|
assert fixture._pulls[0].get("merged") is not True
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_release_allows_only_sha_locked_non_forced_merge() -> None:
|
||||||
|
instance, fixture = connector(FakeGiteaServer(gitea_version="28.0.0"))
|
||||||
|
try:
|
||||||
|
result = instance.pull_request_merge(context(), "hub-repository", 7, "e" * 40, "squash")[
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
assert result["validationRunId"] == 42
|
||||||
|
body = next(body for body in fixture.recorded_bodies if body.get("do") == "squash")
|
||||||
|
assert body["head_commit_id"] == "e" * 40
|
||||||
|
assert body["force_merge"] is False
|
||||||
|
assert body["merge_when_checks_succeed"] is False
|
||||||
|
finally:
|
||||||
|
instance.client.close()
|
||||||
@@ -9,8 +9,9 @@ def test_managed_validation_uses_resolvable_setup_dotnet_v4_pin() -> None:
|
|||||||
assert "actions/setup-dotnet@67a3573c9a9863f9c594539f4ab511d57bb3ce9" not in workflow
|
assert "actions/setup-dotnet@67a3573c9a9863f9c594539f4ab511d57bb3ce9" not in workflow
|
||||||
|
|
||||||
|
|
||||||
def test_managed_validation_uses_internal_gitea_for_checkout() -> None:
|
def test_managed_validation_uses_canonical_https_gitea_for_checkout() -> None:
|
||||||
with open(".gitea/workflows/managed-validation.yml", encoding="utf-8") as workflow_file:
|
with open(".gitea/workflows/managed-validation.yml", encoding="utf-8") as workflow_file:
|
||||||
workflow = workflow_file.read()
|
workflow = workflow_file.read()
|
||||||
|
|
||||||
assert "github-server-url: http://gitea-runner-internal:3000" in workflow
|
assert workflow.count("github-server-url: https://gitea.example.com") == 2
|
||||||
|
assert "http://gitea-runner-internal:3000" not in workflow
|
||||||
@@ -16,6 +16,22 @@ def release_job() -> dict:
|
|||||||
return workflow["jobs"]["release-images"]
|
return workflow["jobs"]["release-images"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_checkouts_use_the_canonical_authenticated_https_origin() -> None:
|
||||||
|
workflow = yaml.safe_load(
|
||||||
|
(ROOT / ".gitea/workflows/managed-validation.yml").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
checkouts = [
|
||||||
|
step
|
||||||
|
for job in workflow["jobs"].values()
|
||||||
|
for step in job["steps"]
|
||||||
|
if step.get("uses") == CHECKOUT
|
||||||
|
]
|
||||||
|
assert len(checkouts) == 2
|
||||||
|
for checkout in checkouts:
|
||||||
|
assert checkout["with"]["github-server-url"] == "https://gitea.example.com"
|
||||||
|
assert "token" not in checkout["with"]
|
||||||
|
|
||||||
|
|
||||||
def test_release_build_requires_successful_validation_and_exact_main_push() -> None:
|
def test_release_build_requires_successful_validation_and_exact_main_push() -> None:
|
||||||
job = release_job()
|
job = release_job()
|
||||||
assert job["name"] == "release-images"
|
assert job["name"] == "release-images"
|
||||||
|
|||||||
Reference in new issue
Block a user