Publish curated ITWorx_MCP_Hub source from 9ec6419178e8ddc76a698e84bde72d5ff4472411
This commit is contained in:
1 parent
5ff5c807d3
commit
f26354e16c
20 files changed
+1639
-84
No files matched your search
+482
-42
@@ -13,9 +13,11 @@ import time
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
|
||||
import yaml
|
||||
|
||||
from packages.connector_kit import (
|
||||
ConnectorContext,
|
||||
GiteaClient,
|
||||
@@ -72,6 +74,208 @@ _REPOSITORY_CONTEXT_DOCUMENT_LIMIT = 4
|
||||
_REPOSITORY_CONTEXT_DOCUMENT_BYTES = 4_096
|
||||
_REPOSITORY_CONTEXT_DOCUMENT_TOTAL_BYTES = 8_192
|
||||
|
||||
_PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY = "jens/projectbrain"
|
||||
_PROJECTBRAIN_QUALITY_VALIDATION_CONTEXT = "Pull request / quality (pull_request)"
|
||||
_PROJECTBRAIN_REQUIRED_VALIDATION_CONTEXTS = (
|
||||
MANAGED_VALIDATION_CONTEXT,
|
||||
_PROJECTBRAIN_QUALITY_VALIDATION_CONTEXT,
|
||||
)
|
||||
_PROJECTBRAIN_REQUIRED_PROTECTION_LOCKS = {
|
||||
"enable_push": False,
|
||||
"enable_push_whitelist": False,
|
||||
"enable_force_push": False,
|
||||
"enable_force_push_allowlist": False,
|
||||
"enable_bypass_allowlist": False,
|
||||
"enable_status_check": True,
|
||||
"block_admin_merge_override": True,
|
||||
}
|
||||
# Original source commit for the ProjectBrain-specific profile lineage.
|
||||
_PROJECTBRAIN_VALIDATION_PROFILE_SOURCE_SHA = "57157b1b24d3679018b54970c934fc35d1735df7"
|
||||
# SHA-256 of JSON-dumped jobs.full (sort_keys, compact separators), parsed with
|
||||
# BaseLoader. It pins the reviewed ProjectBrain workflow copy below.
|
||||
# Frozen workflow source and copied fixture raw SHA-256:
|
||||
# 2f78d37ff51f38f0a3b0c9e222eb2dbf83d5108aee3615eb53f2c01283f37f34.
|
||||
# Refresh only after review.
|
||||
_PROJECTBRAIN_VALIDATION_FULL_JOB_DIGEST = (
|
||||
"a3fefb8865f0bd0d4a8c10af79eeb5e22367c5e82e9333a2fe04bdaf97d0193b"
|
||||
)
|
||||
_PROJECTBRAIN_VALIDATION_MAX_YAML_CHARS = 200_000
|
||||
_PROJECTBRAIN_VALIDATION_MAX_YAML_TOKENS = 10_000
|
||||
_PROJECTBRAIN_VALIDATION_MAX_YAML_DEPTH = 64
|
||||
_PROJECTBRAIN_VALIDATION_RUN_NAME = "Managed validation / ${{ inputs.profile || 'full' }}"
|
||||
_PROJECTBRAIN_VALIDATION_DISPATCH_OPTIONS = [
|
||||
"test",
|
||||
"lint",
|
||||
"typecheck",
|
||||
"build",
|
||||
"security",
|
||||
"full",
|
||||
]
|
||||
_PROJECTBRAIN_VALIDATION_TOP_LEVEL_KEYS = frozenset(
|
||||
{"name", "run-name", "on", "permissions", "concurrency", "jobs"}
|
||||
)
|
||||
_PROJECTBRAIN_VALIDATION_CONCURRENCY = {
|
||||
"group": "managed-validation-${{ gitea.repository }}-${{ gitea.ref }}",
|
||||
"cancel-in-progress": "true",
|
||||
}
|
||||
|
||||
|
||||
class _UniqueWorkflowLoader(yaml.BaseLoader):
|
||||
"""Parse workflow YAML without YAML 1.1's special treatment of the `on` key."""
|
||||
|
||||
|
||||
def _construct_unique_workflow_mapping(loader: Any, node: Any) -> dict[str, Any]:
|
||||
mapping: dict[str, Any] = {}
|
||||
for key_node, value_node in node.value:
|
||||
key = loader.construct_object(key_node)
|
||||
if not isinstance(key, str) or key in mapping:
|
||||
raise yaml.constructor.ConstructorError(
|
||||
"while constructing a workflow mapping",
|
||||
node.start_mark,
|
||||
"workflow mapping keys must be unique strings",
|
||||
key_node.start_mark,
|
||||
)
|
||||
mapping[key] = loader.construct_object(value_node)
|
||||
return mapping
|
||||
|
||||
|
||||
_UniqueWorkflowLoader.add_constructor(
|
||||
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
||||
_construct_unique_workflow_mapping,
|
||||
)
|
||||
|
||||
|
||||
def _workflow_mapping(value: Any) -> dict[str, Any] | None:
|
||||
return value if isinstance(value, dict) else None
|
||||
|
||||
|
||||
def _workflow_yaml_is_bounded_and_alias_free(workflow_content: str) -> bool:
|
||||
if len(workflow_content) > _PROJECTBRAIN_VALIDATION_MAX_YAML_CHARS:
|
||||
return False
|
||||
container_starts = (
|
||||
yaml.tokens.BlockMappingStartToken,
|
||||
yaml.tokens.BlockSequenceStartToken,
|
||||
yaml.tokens.FlowMappingStartToken,
|
||||
yaml.tokens.FlowSequenceStartToken,
|
||||
)
|
||||
container_ends = (
|
||||
yaml.tokens.BlockEndToken,
|
||||
yaml.tokens.FlowMappingEndToken,
|
||||
yaml.tokens.FlowSequenceEndToken,
|
||||
)
|
||||
depth = 0
|
||||
try:
|
||||
for token_count, token in enumerate(
|
||||
yaml.scan(workflow_content, Loader=_UniqueWorkflowLoader), start=1
|
||||
):
|
||||
if token_count > _PROJECTBRAIN_VALIDATION_MAX_YAML_TOKENS:
|
||||
return False
|
||||
if isinstance(token, yaml.tokens.AnchorToken | yaml.tokens.AliasToken):
|
||||
return False
|
||||
if isinstance(token, container_starts):
|
||||
depth += 1
|
||||
if depth > _PROJECTBRAIN_VALIDATION_MAX_YAML_DEPTH:
|
||||
return False
|
||||
elif isinstance(token, container_ends):
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
return False
|
||||
except (yaml.YAMLError, RecursionError, TypeError, ValueError):
|
||||
return False
|
||||
return depth == 0
|
||||
|
||||
|
||||
def _projectbrain_managed_validation_is_strong(workflow_content: str) -> bool:
|
||||
if not _workflow_yaml_is_bounded_and_alias_free(workflow_content):
|
||||
return False
|
||||
try:
|
||||
# BaseLoader constructs only strings/lists/maps; it never creates Python objects.
|
||||
workflow = yaml.load(workflow_content, Loader=_UniqueWorkflowLoader) # noqa: S506
|
||||
except (yaml.YAMLError, RecursionError, TypeError, ValueError):
|
||||
return False
|
||||
document = _workflow_mapping(workflow)
|
||||
if document is None or document.get("name") != "Managed validation":
|
||||
return False
|
||||
if (
|
||||
not set(document).issubset(_PROJECTBRAIN_VALIDATION_TOP_LEVEL_KEYS)
|
||||
or document.get("run-name") != _PROJECTBRAIN_VALIDATION_RUN_NAME
|
||||
or document.get("permissions") != {"contents": "read"}
|
||||
or document.get("concurrency") != _PROJECTBRAIN_VALIDATION_CONCURRENCY
|
||||
):
|
||||
return False
|
||||
|
||||
triggers = _workflow_mapping(document.get("on"))
|
||||
if (
|
||||
triggers is None
|
||||
or set(triggers) != {"push", "pull_request", "workflow_dispatch"}
|
||||
):
|
||||
return False
|
||||
dispatch = _workflow_mapping(triggers.get("workflow_dispatch"))
|
||||
dispatch_inputs = _workflow_mapping(dispatch.get("inputs")) if dispatch is not None else None
|
||||
profile_input = (
|
||||
_workflow_mapping(dispatch_inputs.get("profile"))
|
||||
if dispatch_inputs is not None
|
||||
else None
|
||||
)
|
||||
if (
|
||||
dispatch_inputs is None
|
||||
or set(dispatch) != {"inputs"}
|
||||
or set(dispatch_inputs) != {"profile"}
|
||||
or profile_input is None
|
||||
or set(profile_input) != {"description", "required", "default", "type", "options"}
|
||||
or not isinstance(profile_input.get("description"), str)
|
||||
or profile_input.get("required") != "true"
|
||||
or profile_input.get("default") != "full"
|
||||
or profile_input.get("type") != "choice"
|
||||
or profile_input.get("options") != _PROJECTBRAIN_VALIDATION_DISPATCH_OPTIONS
|
||||
):
|
||||
return False
|
||||
push = _workflow_mapping(triggers.get("push"))
|
||||
if (
|
||||
push is None
|
||||
or set(push) != {"branches"}
|
||||
or push.get("branches") != ["main"]
|
||||
):
|
||||
return False
|
||||
pull_request = triggers.get("pull_request")
|
||||
if pull_request not in (None, ""):
|
||||
pull_request_mapping = _workflow_mapping(pull_request)
|
||||
if pull_request_mapping is None:
|
||||
return False
|
||||
if set(pull_request_mapping) - {"branches"}:
|
||||
return False
|
||||
branches = pull_request_mapping.get("branches")
|
||||
if branches is not None and branches != ["main"]:
|
||||
return False
|
||||
|
||||
jobs = _workflow_mapping(document.get("jobs"))
|
||||
if jobs is None or set(jobs) != {"full"}:
|
||||
return False
|
||||
full_job = _workflow_mapping(jobs.get("full"))
|
||||
if full_job is None:
|
||||
return False
|
||||
try:
|
||||
full_job_digest = hashlib.sha256(
|
||||
json.dumps(full_job, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
).hexdigest()
|
||||
except (RecursionError, TypeError, ValueError):
|
||||
return False
|
||||
return full_job_digest == _PROJECTBRAIN_VALIDATION_FULL_JOB_DIGEST
|
||||
|
||||
|
||||
def _managed_validation_profile(
|
||||
repository_qualified_name: str, workflow_content: str | None
|
||||
) -> str | None:
|
||||
"""Accept only the canonical workflow, except for ProjectBrain's stronger bounded profile."""
|
||||
if workflow_content is None:
|
||||
return None
|
||||
if repository_qualified_name.casefold() == _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY:
|
||||
if _projectbrain_managed_validation_is_strong(workflow_content):
|
||||
return "projectbrain"
|
||||
return None
|
||||
digest = hashlib.sha256(workflow_content.encode("utf-8")).hexdigest()
|
||||
return "canonical" if digest == MANAGED_VALIDATION_DIGEST else None
|
||||
|
||||
|
||||
def _select_validation_workflow(workflows: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||||
"""Prefer the bounded managed contract over legacy validation workflows."""
|
||||
@@ -177,6 +381,19 @@ def _branch_protection_projection(payload: dict[str, Any] | None) -> dict[str, A
|
||||
return {key: payload.get(key) for key in desired_keys}
|
||||
|
||||
|
||||
def _branch_protection_effective_projection(
|
||||
payload: dict[str, Any] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
if payload is None:
|
||||
return None
|
||||
volatile_metadata = {"id", "created_at", "updated_at"}
|
||||
return {
|
||||
key: value
|
||||
for key, value in payload.items()
|
||||
if key not in volatile_metadata and key != "status_check_contexts"
|
||||
}
|
||||
|
||||
|
||||
def _validate_ref(value: str, *, change_branch: bool = False) -> str:
|
||||
pattern = _SAFE_CHANGE_BRANCH if change_branch else _SAFE_REF
|
||||
if (
|
||||
@@ -800,15 +1017,20 @@ class GiteaConnector:
|
||||
None,
|
||||
)
|
||||
desired = _desired_branch_protection(default_branch or "main")
|
||||
if current is not None:
|
||||
existing_contexts = current.get("status_check_contexts")
|
||||
if isinstance(existing_contexts, list):
|
||||
desired["status_check_contexts"] = sorted(
|
||||
{
|
||||
MANAGED_VALIDATION_CONTEXT,
|
||||
*(str(item) for item in existing_contexts if str(item).strip()),
|
||||
}
|
||||
)
|
||||
required_contexts = (
|
||||
_PROJECTBRAIN_REQUIRED_VALIDATION_CONTEXTS
|
||||
if repository.qualified_name.casefold() == _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY
|
||||
else (MANAGED_VALIDATION_CONTEXT,)
|
||||
)
|
||||
existing_contexts = current.get("status_check_contexts") if current is not None else None
|
||||
preserved_contexts = (
|
||||
{str(item) for item in existing_contexts if str(item).strip()}
|
||||
if isinstance(existing_contexts, list)
|
||||
else set()
|
||||
)
|
||||
desired["status_check_contexts"] = sorted(
|
||||
set(required_contexts).union(preserved_contexts)
|
||||
)
|
||||
current_projection = _branch_protection_projection(current)
|
||||
protection_diff = {
|
||||
key: {
|
||||
@@ -839,13 +1061,51 @@ class GiteaConnector:
|
||||
if workflow_content is not None
|
||||
else None
|
||||
)
|
||||
workflow_action = (
|
||||
"create"
|
||||
if workflow_content is None
|
||||
else "none"
|
||||
if workflow_digest == MANAGED_VALIDATION_DIGEST
|
||||
else "update"
|
||||
workflow_profile = _managed_validation_profile(
|
||||
repository.qualified_name, workflow_content
|
||||
)
|
||||
is_projectbrain = (
|
||||
repository.qualified_name.casefold()
|
||||
== _PROJECTBRAIN_MANAGED_VALIDATION_REPOSITORY
|
||||
)
|
||||
if is_projectbrain and workflow_profile is None:
|
||||
blockers.append("managed-validation-projectbrain-profile-invalid")
|
||||
# Fail closed: governance must never replace ProjectBrain's
|
||||
# repository-specific database/E2E gate with the generic baseline.
|
||||
workflow_action = "none"
|
||||
desired_workflow_digest = None
|
||||
else:
|
||||
workflow_action = (
|
||||
"create"
|
||||
if workflow_content is None
|
||||
else "none"
|
||||
if workflow_profile is not None
|
||||
else "update"
|
||||
)
|
||||
desired_workflow_digest = (
|
||||
workflow_digest
|
||||
if workflow_profile == "projectbrain"
|
||||
else MANAGED_VALIDATION_DIGEST
|
||||
)
|
||||
if is_projectbrain and current_projection is not None:
|
||||
if any(
|
||||
current_projection.get(key) != expected
|
||||
for key, expected in _PROJECTBRAIN_REQUIRED_PROTECTION_LOCKS.items()
|
||||
):
|
||||
blockers.append("managed-validation-projectbrain-protection-safety-locks-invalid")
|
||||
if workflow_profile == "projectbrain":
|
||||
desired = dict(current_projection)
|
||||
desired["status_check_contexts"] = sorted(
|
||||
set(required_contexts).union(preserved_contexts)
|
||||
)
|
||||
protection_diff = {
|
||||
key: {
|
||||
"current": None if current_projection is None else current_projection.get(key),
|
||||
"desired": value,
|
||||
}
|
||||
for key, value in desired.items()
|
||||
if current_projection is None or current_projection.get(key) != value
|
||||
}
|
||||
protection_action = "create" if current is None else "update" if protection_diff else "none"
|
||||
plan_payload = {
|
||||
"repositoryRef": repository_ref,
|
||||
@@ -853,7 +1113,9 @@ class GiteaConnector:
|
||||
"workflow": {
|
||||
"path": MANAGED_VALIDATION_PATH,
|
||||
"currentDigest": workflow_digest,
|
||||
"desiredDigest": MANAGED_VALIDATION_DIGEST,
|
||||
"desiredDigest": desired_workflow_digest,
|
||||
"profile": workflow_profile
|
||||
or ("projectbrain-required" if is_projectbrain else "canonical"),
|
||||
"action": workflow_action,
|
||||
},
|
||||
"branchProtection": {
|
||||
@@ -864,17 +1126,50 @@ class GiteaConnector:
|
||||
},
|
||||
"blockers": blockers,
|
||||
}
|
||||
effective_protection = _branch_protection_effective_projection(current)
|
||||
plan_hash_source: dict[str, Any] = plan_payload
|
||||
if is_projectbrain:
|
||||
plan_hash_source = {
|
||||
"plan": plan_payload,
|
||||
"effectiveProtection": effective_protection,
|
||||
}
|
||||
plan_hash = hashlib.sha256(
|
||||
json.dumps(plan_payload, sort_keys=True, separators=(",", ":")).encode()
|
||||
json.dumps(plan_hash_source, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()
|
||||
protection_only_available = (
|
||||
is_projectbrain
|
||||
and current is not None
|
||||
and current.get("enable_status_check") is True
|
||||
and default_branch == "main"
|
||||
and isinstance(desired.get("status_check_contexts"), list)
|
||||
and set(blockers) == {"managed-validation-projectbrain-profile-invalid"}
|
||||
)
|
||||
protection_only_plan_hash = (
|
||||
hashlib.sha256(
|
||||
json.dumps(
|
||||
{
|
||||
"applyScope": "protection_only",
|
||||
"plan": plan_payload,
|
||||
"effectiveProtection": effective_protection,
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
).hexdigest()
|
||||
if protection_only_available
|
||||
else None
|
||||
)
|
||||
return {
|
||||
**plan_payload,
|
||||
"planHash": plan_hash,
|
||||
"protectionOnlyPlanHash": protection_only_plan_hash,
|
||||
"protectionOnlyAvailable": protection_only_available,
|
||||
"compliant": not blockers and workflow_action == "none" and protection_action == "none",
|
||||
"_repository": repository,
|
||||
"_workflowContent": workflow_content,
|
||||
"_workflowSha": workflow_sha,
|
||||
"_currentProtection": current,
|
||||
"_protectionOnlyAvailable": protection_only_available,
|
||||
}
|
||||
|
||||
@governed_tool("gitea.branchprotection.get")
|
||||
@@ -946,17 +1241,103 @@ class GiteaConnector:
|
||||
|
||||
@governed_tool("gitea.branchprotection.ensure")
|
||||
def branch_protection_ensure(
|
||||
self, context: ConnectorContext, repository_ref: str, expected_plan_hash: str
|
||||
self,
|
||||
context: ConnectorContext,
|
||||
repository_ref: str,
|
||||
expected_plan_hash: str,
|
||||
apply_scope: Literal["all", "protection_only"] = "all",
|
||||
) -> dict[str, Any]:
|
||||
if apply_scope not in {"all", "protection_only"}:
|
||||
raise GiteaError("GITEA_INPUT_INVALID", "governance apply scope is not allowlisted")
|
||||
plan = self._governance_plan_data(context, repository_ref)
|
||||
expected_hash = (
|
||||
plan["planHash"]
|
||||
if apply_scope == "all"
|
||||
else plan["protectionOnlyPlanHash"]
|
||||
)
|
||||
if (
|
||||
not re.fullmatch(r"[0-9a-f]{64}", expected_plan_hash)
|
||||
or expected_plan_hash != plan["planHash"]
|
||||
not isinstance(expected_hash, str)
|
||||
or not re.fullmatch(r"[0-9a-f]{64}", expected_plan_hash)
|
||||
or expected_plan_hash != expected_hash
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_PLAN_STALE",
|
||||
"governance state changed; preview the repository again before applying",
|
||||
)
|
||||
|
||||
if apply_scope == "protection_only":
|
||||
allowed_blockers = {"managed-validation-projectbrain-profile-invalid"}
|
||||
if (
|
||||
not plan["_protectionOnlyAvailable"]
|
||||
or any(blocker not in allowed_blockers for blocker in plan["blockers"])
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_BLOCKED",
|
||||
"ProjectBrain protection-only apply requires an existing "
|
||||
"enabled status-check rule",
|
||||
)
|
||||
current_protection = plan["_currentProtection"]
|
||||
if current_protection is None:
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_BLOCKED",
|
||||
"ProjectBrain protection-only apply requires an existing "
|
||||
"enabled status-check rule",
|
||||
)
|
||||
current_contexts = current_protection.get("status_check_contexts")
|
||||
expected_contexts = list(plan["branchProtection"]["desired"]["status_check_contexts"])
|
||||
protection_changed = current_contexts != expected_contexts
|
||||
if not protection_changed:
|
||||
return self._envelope(
|
||||
context,
|
||||
{
|
||||
**self._public_governance_plan(plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": False,
|
||||
"verified": True,
|
||||
"temporaryPushAllowlistUsed": False,
|
||||
},
|
||||
source=f"gitea:{repository_ref}",
|
||||
)
|
||||
self._writer().update_branch_protection(
|
||||
plan["_repository"],
|
||||
str(plan["defaultBranch"]),
|
||||
{"status_check_contexts": expected_contexts},
|
||||
)
|
||||
verified_plan = self._governance_plan_data(context, repository_ref)
|
||||
verified_protection = verified_plan["branchProtection"]["current"]
|
||||
expected_effective_protection = _branch_protection_effective_projection(
|
||||
current_protection
|
||||
)
|
||||
actual_effective_protection = _branch_protection_effective_projection(
|
||||
verified_plan["_currentProtection"]
|
||||
)
|
||||
actual_contexts = set(
|
||||
verified_protection.get("status_check_contexts") or []
|
||||
) if verified_protection is not None else set()
|
||||
if (
|
||||
verified_protection is None
|
||||
or verified_protection.get("enable_status_check") is not True
|
||||
or actual_contexts != set(expected_contexts)
|
||||
or actual_effective_protection != expected_effective_protection
|
||||
or verified_plan["workflow"]["currentDigest"]
|
||||
!= plan["workflow"]["currentDigest"]
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_VERIFY_FAILED",
|
||||
"Gitea did not retain the requested ProjectBrain protection-only contract",
|
||||
)
|
||||
return self._envelope(
|
||||
context,
|
||||
{
|
||||
**self._public_governance_plan(verified_plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": True,
|
||||
"verified": True,
|
||||
"temporaryPushAllowlistUsed": False,
|
||||
},
|
||||
source=f"gitea:{repository_ref}",
|
||||
)
|
||||
|
||||
if plan["blockers"]:
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_BLOCKED",
|
||||
@@ -965,7 +1346,12 @@ class GiteaConnector:
|
||||
if plan["compliant"]:
|
||||
return self._envelope(
|
||||
context,
|
||||
{**self._public_governance_plan(plan), "changed": False, "verified": True},
|
||||
{
|
||||
**self._public_governance_plan(plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": False,
|
||||
"verified": True,
|
||||
},
|
||||
source=f"gitea:{repository_ref}",
|
||||
)
|
||||
|
||||
@@ -992,11 +1378,16 @@ class GiteaConnector:
|
||||
if protection_action == "create":
|
||||
writer.create_branch_protection(repository, desired)
|
||||
elif protection_action == "update":
|
||||
edit_payload = {
|
||||
key: value
|
||||
for key, value in desired.items()
|
||||
if key not in {"rule_name", "branch_name"}
|
||||
}
|
||||
if plan["workflow"]["profile"] == "projectbrain":
|
||||
edit_payload = {
|
||||
"status_check_contexts": desired["status_check_contexts"]
|
||||
}
|
||||
else:
|
||||
edit_payload = {
|
||||
key: value
|
||||
for key, value in desired.items()
|
||||
if key not in {"rule_name", "branch_name"}
|
||||
}
|
||||
writer.update_branch_protection(repository, default_branch, edit_payload)
|
||||
except GiteaError:
|
||||
if workflow_changed:
|
||||
@@ -1027,7 +1418,36 @@ class GiteaConnector:
|
||||
raise
|
||||
|
||||
verified_plan = self._governance_plan_data(context, repository_ref)
|
||||
if not verified_plan["compliant"]:
|
||||
is_projectbrain_profile = plan["workflow"]["profile"] == "projectbrain"
|
||||
verified_contexts = set(
|
||||
(verified_plan["branchProtection"]["current"] or {}).get(
|
||||
"status_check_contexts"
|
||||
)
|
||||
or []
|
||||
)
|
||||
expected_contexts = set(
|
||||
plan["branchProtection"]["desired"].get("status_check_contexts") or []
|
||||
)
|
||||
projectbrain_protection_changed = is_projectbrain_profile and (
|
||||
verified_contexts != expected_contexts
|
||||
or (
|
||||
plan["_currentProtection"] is not None
|
||||
and _branch_protection_effective_projection(
|
||||
verified_plan["_currentProtection"]
|
||||
)
|
||||
!= _branch_protection_effective_projection(plan["_currentProtection"])
|
||||
)
|
||||
)
|
||||
projectbrain_workflow_changed = (
|
||||
is_projectbrain_profile
|
||||
and verified_plan["workflow"]["currentDigest"]
|
||||
!= plan["workflow"]["currentDigest"]
|
||||
)
|
||||
if (
|
||||
not verified_plan["compliant"]
|
||||
or projectbrain_protection_changed
|
||||
or projectbrain_workflow_changed
|
||||
):
|
||||
raise GiteaError(
|
||||
"GITEA_GOVERNANCE_VERIFY_FAILED",
|
||||
"Gitea did not retain the requested governance contract",
|
||||
@@ -1036,6 +1456,7 @@ class GiteaConnector:
|
||||
context,
|
||||
{
|
||||
**self._public_governance_plan(verified_plan),
|
||||
"applyScope": apply_scope,
|
||||
"changed": True,
|
||||
"verified": True,
|
||||
"temporaryPushAllowlistUsed": temporary_push_allowlist_used,
|
||||
@@ -1100,8 +1521,12 @@ class GiteaConnector:
|
||||
for item in plans:
|
||||
repository_ref = str(item.get("repositoryRef", ""))
|
||||
try:
|
||||
apply_scope = str(item.get("applyScope", "all"))
|
||||
result = self.branch_protection_ensure(
|
||||
context, repository_ref, str(item.get("planHash", ""))
|
||||
context,
|
||||
repository_ref,
|
||||
str(item.get("planHash", "")),
|
||||
apply_scope=apply_scope, # type: ignore[arg-type]
|
||||
)["data"]
|
||||
items.append(
|
||||
{
|
||||
@@ -1109,6 +1534,7 @@ class GiteaConnector:
|
||||
"applied": True,
|
||||
"changed": bool(result.get("changed")),
|
||||
"verified": bool(result.get("verified")),
|
||||
"applyScope": str(result.get("applyScope", "all")),
|
||||
}
|
||||
)
|
||||
except GiteaError as exc:
|
||||
@@ -4519,13 +4945,13 @@ GITEA_READ_TOOLS = tuple(
|
||||
"gitea.change.status",
|
||||
"gitea.change.review",
|
||||
"gitea.workspace.status",
|
||||
"gitea.branchprotection.preview",
|
||||
"gitea.portfolio.governance.preview",
|
||||
"gitea.portfolio.governance.verify",
|
||||
}
|
||||
else (
|
||||
"1.8.0"
|
||||
if name.startswith("gitea.branchprotection.")
|
||||
or name.startswith("gitea.portfolio.governance.")
|
||||
else "1.5.0"
|
||||
),
|
||||
else "1.8.0"
|
||||
if name == "gitea.branchprotection.get"
|
||||
else "1.5.0",
|
||||
connector_type="gitea",
|
||||
risk_class="R1",
|
||||
input_schema=_tool_input_schema(name),
|
||||
@@ -4620,6 +5046,11 @@ def _write_tool_input_schema(name: str) -> dict[str, Any]:
|
||||
}
|
||||
if name == "gitea.branchprotection.ensure":
|
||||
properties["expectedPlanHash"] = {"type": "string", "pattern": "^[0-9a-f]{64}$"}
|
||||
properties["applyScope"] = {
|
||||
"type": "string",
|
||||
"enum": ["all", "protection_only"],
|
||||
"default": "all",
|
||||
}
|
||||
required.append("expectedPlanHash")
|
||||
elif name == "gitea.portfolio.governance.apply":
|
||||
return {
|
||||
@@ -4639,7 +5070,19 @@ def _write_tool_input_schema(name: str) -> dict[str, Any]:
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9-]{0,127}$",
|
||||
},
|
||||
"planHash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
||||
"planHash": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$",
|
||||
"description": (
|
||||
"Use planHash for all scope or protectionOnlyPlanHash "
|
||||
"for protection_only scope."
|
||||
),
|
||||
},
|
||||
"applyScope": {
|
||||
"type": "string",
|
||||
"enum": ["all", "protection_only"],
|
||||
"default": "all",
|
||||
},
|
||||
},
|
||||
"required": ["repositoryRef", "planHash"],
|
||||
},
|
||||
@@ -4827,13 +5270,10 @@ GITEA_PR_WRITE_TOOLS = tuple(
|
||||
"gitea.pullrequest.update",
|
||||
"gitea.changeset.apply",
|
||||
}
|
||||
else "1.9.0"
|
||||
if name in {"gitea.branchprotection.ensure", "gitea.portfolio.governance.apply"}
|
||||
else "1.8.0"
|
||||
if name
|
||||
in {
|
||||
"gitea.repository.create",
|
||||
"gitea.branchprotection.ensure",
|
||||
"gitea.portfolio.governance.apply",
|
||||
}
|
||||
if name == "gitea.repository.create"
|
||||
else "1.5.0"
|
||||
),
|
||||
connector_type="gitea",
|
||||
|
||||
Reference in new issue
Block a user