feat(sofiia-console): add auto-evidence and post-review generation from runbook runs

- adds runbook_artifacts.py: server-side render of release_evidence.md and
  post_review.md from DB step results (no shell); saves to
  SOFIIA_DATA_DIR/release_artifacts/<run_id>/
- evidence: auto-fills preflight/smoke/script outcomes, step table, timestamps
- post_review: auto-fills metadata, smoke results, incidents from step statuses;
  leaves [TODO] markers for manual observation sections
- adds POST /api/runbooks/runs/{run_id}/evidence and /post_review endpoints
- updates runbook_runs.evidence_path in DB after render
- adds 11 tests covering file creation, key sections, TODO markers, 404s, API

Made-with: Cursor
This commit is contained in:
Apple
2026-03-03 05:07:52 -08:00
parent 0603184524
commit 8879da1e7f
3 changed files with 665 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
"""
Runbook runs API — create run, get run, next step, complete step, abort (PR2).
Evidence + post-review generation (PR4).
All under require_auth.
"""
from __future__ import annotations
@@ -10,6 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, Body
from .auth import require_auth
from . import runbook_runner as runner
from . import runbook_artifacts as artifacts
runbook_runs_router = APIRouter(prefix="/api/runbooks/runs", tags=["runbook-runs"])
@@ -80,6 +82,32 @@ async def complete_step(
return {"ok": True, "run_id": run_id, "step_index": step_index, "next_step": step_index + 1}
@runbook_runs_router.post("/{run_id}/evidence")
async def generate_evidence(
run_id: str,
_auth: str = Depends(require_auth),
):
"""Generate release evidence markdown from run step results. Returns {evidence_path, bytes, created_at}."""
try:
out = await artifacts.render_release_evidence(run_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
return out
@runbook_runs_router.post("/{run_id}/post_review")
async def generate_post_review(
run_id: str,
_auth: str = Depends(require_auth),
):
"""Generate post-release review markdown. Returns {path, bytes, created_at}."""
try:
out = await artifacts.render_post_review(run_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
return out
@runbook_runs_router.post("/{run_id}/abort")
async def abort_run(
run_id: str,