- Fix RunnableConfig type annotations (dict -> RunnableConfig) for LangGraph compat - Fix AzDo PR URL parsing (_links.web.href fallback + remoteUrl construction) - Fix AzDo diff endpoint (use iterations/changes instead of non-existent diffs API) - Fix _format_diff to read changeEntries field (not changes) - Fix URL encoding for project names with spaces (Billo App Platform) - Fix subprocess.run for Windows (replace asyncio.create_subprocess_exec with thread pool) - Fix SlackClient to handle empty webhook URL gracefully - Fix notify_request_changes to catch all exceptions (not just ReleaseAgentError) - Fix JSON parsing to strip whitespace before json.loads - Add CLAUDE_CMD config field for cross-platform CLI path - Add run.py for Windows SelectorEventLoop workaround - Add db port mapping in docker-compose for local dev - Add comprehensive README sections: WSL setup, known issues, TODO list
189 lines
6.3 KiB
Python
189 lines
6.3 KiB
Python
"""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}",
|
|
)
|