feat: initial commit — Billo Release Agent (LangGraph)
LangGraph-based release automation agent with: - PR discovery (webhook + polling) - AI code review via Claude Code CLI (subscription-based) - Auto-create Jira tickets for PRs without ticket ID - Jira ticket lifecycle management (code review -> staging -> done) - CI/CD pipeline trigger, polling, and approval gates - Slack interactive messages with approval buttons - Per-repo semantic versioning - PostgreSQL persistence (threads, staging, releases) - FastAPI API (webhooks, approvals, status, manual triggers) - Docker Compose deployment 1069 tests, 95%+ coverage.
This commit is contained in:
153
src/release_agent/api/status.py
Normal file
153
src/release_agent/api/status.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""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."""
|
||||
from release_agent.api.webhooks import _run_graph
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
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,
|
||||
)
|
||||
)
|
||||
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}",
|
||||
)
|
||||
Reference in New Issue
Block a user