mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 15:03:16 +00:00
* feat: gate workflow pushes with approval Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: preserve proxy refresh test compatibility Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: bind workflow approvals to pushed ref Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
119 lines
3.8 KiB
Python
119 lines
3.8 KiB
Python
"""Workflow-file push approval state."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from langgraph_sdk import get_client
|
|
|
|
WORKFLOW_PUSH_APPROVALS_KEY = "workflow_push_approvals"
|
|
WORKFLOW_APPROVAL_PENDING = "pending"
|
|
WORKFLOW_APPROVAL_APPROVED = "approved"
|
|
WORKFLOW_APPROVAL_REJECTED = "rejected"
|
|
_MAX_APPROVAL_RECORDS = 20
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(UTC).isoformat()
|
|
|
|
|
|
def _approvals_from_metadata(metadata: Mapping[str, Any] | None) -> dict[str, dict[str, Any]]:
|
|
raw = metadata.get(WORKFLOW_PUSH_APPROVALS_KEY) if metadata else None
|
|
if not isinstance(raw, dict):
|
|
return {}
|
|
approvals: dict[str, dict[str, Any]] = {}
|
|
for fingerprint, value in raw.items():
|
|
if isinstance(fingerprint, str) and fingerprint and isinstance(value, dict):
|
|
record = dict(value)
|
|
record.setdefault("fingerprint", fingerprint)
|
|
approvals[fingerprint] = record
|
|
return approvals
|
|
|
|
|
|
async def get_workflow_push_approvals(thread_id: str) -> dict[str, dict[str, Any]]:
|
|
client = get_client()
|
|
thread = await client.threads.get(thread_id)
|
|
metadata = thread.get("metadata") if isinstance(thread, dict) else None
|
|
return _approvals_from_metadata(metadata if isinstance(metadata, dict) else None)
|
|
|
|
|
|
async def workflow_push_approved(thread_id: str, fingerprint: str) -> bool:
|
|
approvals = await get_workflow_push_approvals(thread_id)
|
|
return approvals.get(fingerprint, {}).get("status") == WORKFLOW_APPROVAL_APPROVED
|
|
|
|
|
|
async def ensure_workflow_push_pending(
|
|
thread_id: str,
|
|
*,
|
|
fingerprint: str,
|
|
repo: str,
|
|
branch: str,
|
|
base_sha: str,
|
|
head_sha: str,
|
|
files: list[str],
|
|
) -> tuple[dict[str, Any], bool]:
|
|
"""Store a pending approval unless a terminal record already exists."""
|
|
approvals = await get_workflow_push_approvals(thread_id)
|
|
existing = approvals.get(fingerprint)
|
|
if existing and existing.get("status") in {
|
|
WORKFLOW_APPROVAL_PENDING,
|
|
WORKFLOW_APPROVAL_APPROVED,
|
|
WORKFLOW_APPROVAL_REJECTED,
|
|
}:
|
|
return existing, False
|
|
|
|
record = {
|
|
"fingerprint": fingerprint,
|
|
"status": WORKFLOW_APPROVAL_PENDING,
|
|
"repo": repo,
|
|
"branch": branch,
|
|
"base_sha": base_sha,
|
|
"head_sha": head_sha,
|
|
"files": files,
|
|
"requested_at": _now(),
|
|
"notified": False,
|
|
}
|
|
approvals[fingerprint] = record
|
|
await _save_approvals(thread_id, approvals)
|
|
return record, True
|
|
|
|
|
|
async def mark_workflow_push_notified(thread_id: str, fingerprint: str) -> None:
|
|
approvals = await get_workflow_push_approvals(thread_id)
|
|
record = approvals.get(fingerprint)
|
|
if not record:
|
|
return
|
|
record["notified"] = True
|
|
record["notified_at"] = _now()
|
|
approvals[fingerprint] = record
|
|
await _save_approvals(thread_id, approvals)
|
|
|
|
|
|
async def decide_workflow_push_approval(
|
|
thread_id: str,
|
|
fingerprint: str,
|
|
*,
|
|
approved: bool,
|
|
actor: str,
|
|
) -> dict[str, Any] | None:
|
|
approvals = await get_workflow_push_approvals(thread_id)
|
|
record = approvals.get(fingerprint)
|
|
if not record:
|
|
return None
|
|
record["status"] = WORKFLOW_APPROVAL_APPROVED if approved else WORKFLOW_APPROVAL_REJECTED
|
|
record["decided_at"] = _now()
|
|
record["decided_by"] = actor
|
|
approvals[fingerprint] = record
|
|
await _save_approvals(thread_id, approvals)
|
|
return record
|
|
|
|
|
|
async def _save_approvals(thread_id: str, approvals: dict[str, dict[str, Any]]) -> None:
|
|
ordered = sorted(approvals.values(), key=lambda r: str(r.get("requested_at", "")))
|
|
trimmed = ordered[-_MAX_APPROVAL_RECORDS:]
|
|
await get_client().threads.update(
|
|
thread_id=thread_id,
|
|
metadata={WORKFLOW_PUSH_APPROVALS_KEY: {str(r["fingerprint"]): r for r in trimmed}},
|
|
)
|