"""Status, releases, staging, and manual trigger endpoints. GET /status — health check GET /releases/{repo} — list release versions for a repo GET /staging — current staging release for a repo POST /manual/pr/{pr_id} — manually trigger PR processing POST /manual/release — manually trigger a release run """ import asyncio import uuid from datetime import datetime, timezone from fastapi import APIRouter, Depends, Request, status from fastapi.responses import JSONResponse from release_agent.api.dependencies import ( get_db_pool, get_graphs, get_staging_store, get_tool_clients, require_operator_token, ) from release_agent.api.models import ( HealthResponse, ManualReleaseRequest, ManualTriggerResponse, ReleaseVersionListResponse, StagingResponse, ) router = APIRouter() _VERSION = "0.1.0" # --------------------------------------------------------------------------- # GET /status # --------------------------------------------------------------------------- @router.get("/status") async def get_status(request: Request) -> HealthResponse: """Return the health status of the agent service.""" started_at: datetime = request.app.state.started_at uptime = (datetime.now(tz=timezone.utc) - started_at).total_seconds() return HealthResponse( status="ok", version=_VERSION, uptime_seconds=uptime, ) # --------------------------------------------------------------------------- # GET /releases/{repo} # --------------------------------------------------------------------------- @router.get("/releases/{repo}") async def list_release_versions( repo: str, staging_store=Depends(get_staging_store), ) -> ReleaseVersionListResponse: """List all known release versions for the given repository.""" versions = await staging_store.list_versions(repo) return ReleaseVersionListResponse(repo=repo, versions=versions) # --------------------------------------------------------------------------- # GET /staging # --------------------------------------------------------------------------- @router.get("/staging") async def get_staging( repo: str, staging_store=Depends(get_staging_store), ) -> StagingResponse: """Return the current staging release for the given repository.""" staging_obj = await staging_store.load(repo) staging_dict = staging_obj.model_dump(mode="json") if staging_obj is not None else None return StagingResponse(repo=repo, staging=staging_dict) # --------------------------------------------------------------------------- # POST /manual/pr/{pr_id} # --------------------------------------------------------------------------- @router.post("/manual/pr/{pr_id}", status_code=status.HTTP_202_ACCEPTED) async def manual_pr_trigger( pr_id: str, request: Request, graphs=Depends(get_graphs), tool_clients=Depends(get_tool_clients), db_pool=Depends(get_db_pool), _auth: None = Depends(require_operator_token), ) -> ManualTriggerResponse: """Manually trigger PR processing for the given PR ID. Fetches PR details from AzDo first to build a proper initial state including the synthesized webhook payload. """ from release_agent.api.webhooks import _run_graph settings = request.app.state.settings thread_id = str(uuid.uuid4()) # Fetch PR info to build webhook-compatible initial state try: pr_info = await tool_clients.azdo.get_pr(int(pr_id)) initial_state = { "webhook_payload": { "event_type": "git.pullrequest.updated", "subscription_id": f"manual-{pr_id}", "resource": { "pull_request_id": int(pr_id), "title": pr_info.pr_title, "status": pr_info.pr_status, "source_ref_name": pr_info.branch, "target_ref_name": "refs/heads/develop", "closed_date": None, "repository": { "id": f"{pr_info.repo_name}-id", "name": pr_info.repo_name, "web_url": f"https://dev.azure.com/billodev/Billo%20App%20Platform/_git/{pr_info.repo_name}", }, }, }, "pr_id": pr_id, "repo_name": pr_info.repo_name, } except Exception: # Fallback: minimal state, let parse_webhook handle errors initial_state = {"pr_id": pr_id} task = asyncio.create_task( _run_graph( graph=graphs["pr_completed"], initial_state=initial_state, thread_id=thread_id, tool_clients=tool_clients, db_pool=db_pool, repos_base_dir=settings.repos_base_dir, graph_name="pr_completed", default_jira_project=settings.default_jira_project, ) ) request.app.state.background_tasks.add(task) task.add_done_callback(request.app.state.background_tasks.discard) return ManualTriggerResponse( thread_id=thread_id, message=f"PR {pr_id} processing scheduled as thread {thread_id}", ) # --------------------------------------------------------------------------- # POST /manual/release # --------------------------------------------------------------------------- @router.post("/manual/release", status_code=status.HTTP_202_ACCEPTED) async def manual_release_trigger( body: ManualReleaseRequest, request: Request, graphs=Depends(get_graphs), tool_clients=Depends(get_tool_clients), db_pool=Depends(get_db_pool), _auth: None = Depends(require_operator_token), ) -> ManualTriggerResponse: """Manually trigger a release run for the given repository.""" from release_agent.api.webhooks import _run_graph thread_id = str(uuid.uuid4()) initial_state = {"repo_name": body.repo} task = asyncio.create_task( _run_graph( graph=graphs["release"], initial_state=initial_state, thread_id=thread_id, tool_clients=tool_clients, db_pool=db_pool, ) ) request.app.state.background_tasks.add(task) task.add_done_callback(request.app.state.background_tasks.discard) return ManualTriggerResponse( thread_id=thread_id, message=f"Release for repo '{body.repo}' scheduled as thread {thread_id}", )