autonomy-unit-tests exact-head check passed: autonomy-unit-tests
autonomy-smoke-tests exact-head check passed: autonomy-smoke-tests
signer-validator-contract exact-head check passed: signer-validator-contract
secret-scan exact-head check passed: secret-scan
py-compile exact-head check passed: py-compile
git-diff-check exact-head check passed: git-diff-check
release-state-machine exact-head check passed: release-state-machine
reconciliation-contract exact-head check passed: reconciliation-contract
campaign-bootstrap-gate exact-head check passed: campaign-bootstrap-gate
126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
"""Non-Owner Gitea machine capability and branch reconciliation boundary."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Mapping, Protocol
|
|
|
|
|
|
class MachineBrokerError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class GiteaTransport(Protocol):
|
|
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str: ...
|
|
def create_or_update_pr(self, branch: str, target: str, title: str, body: str) -> Mapping[str, Any]: ...
|
|
def publish_status(self, branch: str, state: str, description: str) -> Mapping[str, Any]: ...
|
|
def protected_merge(self, pr_number: int) -> Mapping[str, Any]: ...
|
|
def read_branch(self, branch: str) -> str | None: ...
|
|
def whoami(self) -> Mapping[str, Any]: ...
|
|
def repository(self, owner: str, name: str) -> Mapping[str, Any]: ...
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class MachineCredentialProjection:
|
|
"""Opaque systemd credential reference; the credential value is never accepted."""
|
|
|
|
reference: str
|
|
account: str
|
|
owner_account: str = "aiman"
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: Mapping[str, Any]) -> "MachineCredentialProjection":
|
|
if set(value) != {"scheme", "name", "account"}:
|
|
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
|
|
if value["scheme"] != "systemd-credential" or value["name"] != "gitea-machine-token":
|
|
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
|
|
return cls(f"{value['scheme']}:{value['name']}", str(value["account"]))
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.reference != "systemd-credential:gitea-machine-token":
|
|
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
|
|
if not self.account or self.account == self.owner_account:
|
|
raise MachineBrokerError("OWNER_MACHINE_IDENTITY_REJECTED")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CampaignCapabilityReceipt:
|
|
campaign_id: str
|
|
task_id: str
|
|
repository: str
|
|
branch: str
|
|
expected_oid: str
|
|
capabilities: frozenset[str]
|
|
denied: frozenset[str]
|
|
|
|
def require(self, capability: str) -> None:
|
|
if capability in self.denied or capability not in self.capabilities:
|
|
raise MachineBrokerError("CAPABILITY_DENIED")
|
|
if self.branch in {"main", "master"}:
|
|
raise MachineBrokerError("PROTECTED_BRANCH_DENIED")
|
|
|
|
|
|
class GiteaMachineBroker:
|
|
def __init__(self, transport: GiteaTransport, credential: MachineCredentialProjection, receipt: CampaignCapabilityReceipt) -> None:
|
|
self.transport = transport
|
|
self.credential = credential
|
|
self.receipt = receipt
|
|
|
|
def verify_identity(self) -> Mapping[str, Any]:
|
|
identity = dict(self.transport.whoami())
|
|
login = identity.get("login") or identity.get("username")
|
|
if not isinstance(login, str) or not login or login == self.credential.owner_account or login != self.credential.account:
|
|
raise MachineBrokerError("GITEA_MACHINE_IDENTITY_INVALID")
|
|
owner, name = self.receipt.repository.split("/", 1)
|
|
repo = dict(self.transport.repository(owner, name))
|
|
if repo.get("full_name") != self.receipt.repository:
|
|
raise MachineBrokerError("GITEA_REPOSITORY_MISMATCH")
|
|
return {"login": login, "repository": self.receipt.repository, "owner_account": self.credential.owner_account}
|
|
|
|
def push_branch(self, new_oid: str) -> str:
|
|
self.receipt.require("exact_task_branch_push")
|
|
return self.transport.compare_and_swap_branch(self.receipt.branch, self.receipt.expected_oid, new_oid)
|
|
|
|
def create_pr(self, title: str, body: str) -> Mapping[str, Any]:
|
|
self.receipt.require("pr_create_update")
|
|
return self.transport.create_or_update_pr(self.receipt.branch, "main", title, body)
|
|
|
|
def status(self, state: str, description: str, context: str = "aios/durable-release") -> Mapping[str, Any]:
|
|
self.receipt.require("status_publish")
|
|
return self.transport.publish_status(self.receipt.branch, state, description, context)
|
|
|
|
def merge(self, pr_number: int) -> Mapping[str, Any]:
|
|
self.receipt.require("protected_merge")
|
|
return self.transport.protected_merge(pr_number)
|
|
|
|
def readback(self) -> str | None:
|
|
self.receipt.require("remote_readback")
|
|
return self.transport.read_branch(self.receipt.branch)
|
|
|
|
|
|
def reconcile_exact_branch(authority_oid: str, gitea_oid: str | None, expected_oid: str) -> str:
|
|
if authority_oid != expected_oid:
|
|
return "AUTHORITY_EXPECTATION_MISMATCH"
|
|
if gitea_oid is None:
|
|
return "CREATE_REQUIRED"
|
|
if gitea_oid == expected_oid:
|
|
return "ALREADY_IN_SYNC"
|
|
return "MANUAL_OWNER_REVIEW_REQUIRED"
|
|
|
|
|
|
def reconcile_fast_forward(
|
|
remote_oid: str | None,
|
|
expected_old_oid: str,
|
|
new_oid: str,
|
|
*,
|
|
old_is_ancestor_of_new: bool,
|
|
) -> str:
|
|
"""Return a bounded action for an active Job/lease branch scope."""
|
|
if remote_oid is None:
|
|
return "CREATE_REQUIRED"
|
|
if remote_oid == new_oid:
|
|
return "ALREADY_IN_SYNC"
|
|
if remote_oid == expected_old_oid and old_is_ancestor_of_new:
|
|
return "FAST_FORWARD_REQUIRED"
|
|
return "MANUAL_OWNER_REVIEW_REQUIRED"
|