feat: prove repository rename continuity
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
parent
037c8360e2
commit
9f0a104b56
11 changed files with 1186 additions and 96 deletions
|
|
@ -1,12 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.database import get_session
|
||||
from api.events import EventEnvelope, publish_event
|
||||
from api.models.repository_rename import RepositoryRenameOperation
|
||||
from api.schemas.repository_rename import (
|
||||
ForgeIdentityVerifyRequest,
|
||||
|
|
@ -44,10 +47,77 @@ operation_router = APIRouter(
|
|||
)
|
||||
|
||||
|
||||
def repository_rename_events(
|
||||
operation: RepositoryRenameOperation,
|
||||
*,
|
||||
outcome: str,
|
||||
no_op: bool = False,
|
||||
verification: dict[str, Any] | None = None,
|
||||
) -> list[tuple[str, EventEnvelope]]:
|
||||
"""Build credential-free lifecycle events from the durable journal."""
|
||||
telemetry = (operation.evidence or {}).get("telemetry") or {}
|
||||
phase_durations = telemetry.get("phase_durations_ms") or {}
|
||||
attributes = {
|
||||
"operation_id": str(operation.id),
|
||||
"repo_id": str(operation.repo_id),
|
||||
"forge_repository_id": operation.expected_forge_repository_id,
|
||||
"old_slug": operation.old_slug,
|
||||
"new_slug": operation.new_slug,
|
||||
"phase": operation.phase,
|
||||
"actor": operation.actor,
|
||||
"expected_source_commit": operation.expected_source_commit,
|
||||
"outcome": outcome,
|
||||
"no_op": no_op,
|
||||
"phase_duration_ms": int(phase_durations.get(operation.phase) or 0),
|
||||
"retries": int(telemetry.get("retries") or 0),
|
||||
"failures": int(telemetry.get("failures") or 0),
|
||||
"rollback_attempts": int(telemetry.get("rollback_attempts") or 0),
|
||||
"rollback_outcome": telemetry.get("rollback_outcome"),
|
||||
"verification_outcome": (
|
||||
"passed" if verification and verification.get("ok") else
|
||||
"failed" if verification else
|
||||
telemetry.get("verification_outcome", "pending")
|
||||
),
|
||||
"error_code": operation.error_code if outcome == "failed" else None,
|
||||
"evidence_ref": f"repository-renames/operations/{operation.id}",
|
||||
}
|
||||
subject = (
|
||||
"org.statehub.repo.rename.failed"
|
||||
if outcome == "failed"
|
||||
else "org.statehub.repo.rename.verified"
|
||||
if verification is not None
|
||||
else "org.statehub.repo.rename.rolled_back"
|
||||
if operation.phase == "rolled-back"
|
||||
else "org.statehub.repo.rename.phase"
|
||||
)
|
||||
events = [(subject, EventEnvelope.new(subject, attributes=attributes))]
|
||||
if operation.phase == "completed" and outcome == "succeeded":
|
||||
renamed_subject = "org.statehub.repo.renamed"
|
||||
events.append(
|
||||
(renamed_subject, EventEnvelope.new(renamed_subject, attributes=attributes))
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def _publish_rename_events(
|
||||
operation: RepositoryRenameOperation,
|
||||
*,
|
||||
outcome: str,
|
||||
no_op: bool = False,
|
||||
verification: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
for subject, envelope in repository_rename_events(
|
||||
operation, outcome=outcome, no_op=no_op, verification=verification
|
||||
):
|
||||
asyncio.create_task(publish_event(subject, envelope))
|
||||
|
||||
|
||||
def _raise(exc: RenameLifecycleError) -> None:
|
||||
raise HTTPException(
|
||||
status_code=exc.status_code,
|
||||
detail={"code": exc.code, "message": str(exc), "details": exc.details},
|
||||
detail=jsonable_encoder(
|
||||
{"code": exc.code, "message": str(exc), "details": exc.details}
|
||||
),
|
||||
) from exc
|
||||
|
||||
|
||||
|
|
@ -129,6 +199,7 @@ async def create_repository_rename_operation(
|
|||
operation, no_op = await create_operation(session, gateway, repo_id, body)
|
||||
except RenameLifecycleError as exc:
|
||||
_raise(exc)
|
||||
_publish_rename_events(operation, outcome="succeeded", no_op=no_op)
|
||||
return _operation_read(operation, no_op=no_op)
|
||||
|
||||
|
||||
|
|
@ -194,7 +265,11 @@ async def apply_repository_rename_phase(
|
|||
session, gateway, repo_id, operation_id, phase, body
|
||||
)
|
||||
except RenameLifecycleError as exc:
|
||||
failed = await session.get(RepositoryRenameOperation, operation_id)
|
||||
if failed is not None:
|
||||
_publish_rename_events(failed, outcome="failed")
|
||||
_raise(exc)
|
||||
_publish_rename_events(operation, outcome="succeeded", no_op=no_op)
|
||||
return _operation_read(operation, no_op=no_op)
|
||||
|
||||
|
||||
|
|
@ -210,9 +285,15 @@ async def verify_repository_rename_operation(
|
|||
) -> dict[str, Any]:
|
||||
try:
|
||||
operation = await load_operation(session, repo_id, operation_id)
|
||||
return await verify_operation(session, gateway, operation)
|
||||
verification = await verify_operation(session, gateway, operation)
|
||||
except RenameLifecycleError as exc:
|
||||
_raise(exc)
|
||||
_publish_rename_events(
|
||||
operation,
|
||||
outcome="succeeded" if verification["ok"] else "failed",
|
||||
verification=verification,
|
||||
)
|
||||
return verification
|
||||
|
||||
|
||||
@router.post(
|
||||
|
|
@ -236,7 +317,11 @@ async def preflight_repository_rename_rollback(
|
|||
confirmation=body.confirmation,
|
||||
)
|
||||
except RenameLifecycleError as exc:
|
||||
failed = await session.get(RepositoryRenameOperation, operation_id)
|
||||
if failed is not None:
|
||||
_publish_rename_events(failed, outcome="failed")
|
||||
_raise(exc)
|
||||
_publish_rename_events(operation, outcome="succeeded")
|
||||
return {
|
||||
"operation_id": operation.id,
|
||||
"repo_id": operation.repo_id,
|
||||
|
|
@ -266,5 +351,9 @@ async def rollback_repository_rename_operation(
|
|||
confirmation=body.confirmation,
|
||||
)
|
||||
except RenameLifecycleError as exc:
|
||||
failed = await session.get(RepositoryRenameOperation, operation_id)
|
||||
if failed is not None:
|
||||
_publish_rename_events(failed, outcome="failed")
|
||||
_raise(exc)
|
||||
_publish_rename_events(operation, outcome="succeeded", no_op=no_op)
|
||||
return _operation_read(operation, no_op=no_op)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue