Pipeline/backend/app/services/forgejo_issue_edit.py

151 lines
4.6 KiB
Python
Raw Permalink Normal View History

2026-05-21 23:30:19 -05:00
"""Service for editing Forgejo issues and updating local cache."""
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import UUID
from sqlmodel import select
from app.core.logging import get_logger
from app.core.time import utcnow
from app.db import crud
from app.models.forgejo_connections import ForgejoConnection
from app.models.forgejo_issues import ForgejoIssue
from app.models.forgejo_repositories import ForgejoRepository
from app.services.forgejo_client import get_forgejo_client
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio.session import AsyncSession
logger = get_logger(__name__)
class EditIssueError(Exception):
"""Base exception for edit issue errors."""
class EditIssueNotFoundError(EditIssueError):
"""Raised when issue, repository, or connection is not found."""
class EditIssueRemoteError(EditIssueError):
"""Raised when the Forgejo API call fails."""
async def edit_cached_issue(
session: AsyncSession,
issue: ForgejoIssue,
*,
title: str | None = None,
body: str | None = None,
state: str | None = None,
actor_user_id: UUID | None = None,
actor_agent_id: UUID | None = None,
) -> dict[str, object]:
"""Edit a Forgejo issue remotely and sync changes to the local cache.
Raises:
EditIssueNotFoundError: If repository or connection not found.
EditIssueRemoteError: If the Forgejo API call fails.
"""
repository = await crud.get_by_id(session, ForgejoRepository, issue.repository_id)
if repository is None:
raise EditIssueNotFoundError("Repository not found")
connection = await crud.get_by_id(session, ForgejoConnection, repository.connection_id)
if connection is None:
raise EditIssueNotFoundError("Connection not found")
try:
async with get_forgejo_client(connection) as client:
updated = await client.edit_issue(
owner=repository.owner,
repo=repository.repo,
issue_number=issue.forgejo_issue_number,
title=title,
body=body,
state=state,
)
except Exception as exc:
raise EditIssueRemoteError(f"Failed to edit issue on Forgejo: {exc}") from exc
now = utcnow()
if title is not None:
issue.title = title
if body is not None:
issue.body = body
issue.body_preview = body[:1000] if body else None
if state is not None:
issue.state = state
if state == "closed" and issue.forgejo_closed_at is None:
issue.forgejo_closed_at = now
issue.forgejo_updated_at = now
issue.last_synced_at = now
if isinstance(issue.forgejo_payload, dict):
payload = dict(issue.forgejo_payload)
if title is not None:
payload["title"] = title
if body is not None:
payload["body"] = body
if state is not None:
payload["state"] = state
payload["updated_at"] = now.isoformat()
issue.forgejo_payload = payload
session.add(issue)
await session.flush()
logger.info(
"forgejo.issue.edited",
extra={
"issue_id": str(issue.id),
"forgejo_issue_number": issue.forgejo_issue_number,
"repository_id": str(repository.id),
"fields_changed": [
f for f, v in [("title", title), ("body", body), ("state", state)] if v is not None
],
"actor_user_id": str(actor_user_id) if actor_user_id else None,
"actor_agent_id": str(actor_agent_id) if actor_agent_id else None,
},
)
_ = updated
return {
"success": True,
"issue_id": str(issue.id),
"forgejo_issue_number": issue.forgejo_issue_number,
"title": issue.title,
"body": issue.body,
"state": issue.state,
"forgejo_updated_at": issue.forgejo_updated_at.isoformat(),
}
async def edit_issue_by_id(
session: AsyncSession,
issue_id: UUID,
*,
title: str | None = None,
body: str | None = None,
state: str | None = None,
actor_user_id: UUID | None = None,
actor_agent_id: UUID | None = None,
) -> dict[str, object]:
"""Edit a Forgejo issue by its local ID."""
issue = (await session.exec(select(ForgejoIssue).where(ForgejoIssue.id == issue_id))).first()
if issue is None:
raise EditIssueNotFoundError("Issue not found")
return await edit_cached_issue(
session=session,
issue=issue,
title=title,
body=body,
state=state,
actor_user_id=actor_user_id,
actor_agent_id=actor_agent_id,
)