- runbook_artifacts.py: adds list_run_artifacts() returning files with
names, paths, sizes, mtime_utc from release_artifacts/<run_id>/
- runbook_runs_router.py: adds GET /api/runbooks/runs/{run_id}/artifacts
- docs/runbook/team-onboarding-console.md: one-page team onboarding doc
covering access, rehearsal run steps, audit auth model (strict, no
localhost bypass), artifacts location, abort procedure
Made-with: Cursor
132 lines
4.4 KiB
Python
132 lines
4.4 KiB
Python
"""
|
|
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
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
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"])
|
|
|
|
|
|
@runbook_runs_router.post("")
|
|
async def create_run(
|
|
body: Dict[str, Any] = Body(...),
|
|
_auth: str = Depends(require_auth),
|
|
):
|
|
"""Create a new run from runbook_path. Returns run_id, status, current_step, steps_total."""
|
|
runbook_path = (body.get("runbook_path") or "").strip()
|
|
if not runbook_path or ".." in runbook_path:
|
|
raise HTTPException(status_code=400, detail="Invalid runbook_path")
|
|
try:
|
|
out = await runner.create_run(
|
|
runbook_path=runbook_path,
|
|
operator_id=(body.get("operator_id") or "").strip() or None,
|
|
node_id=(body.get("node_id") or "").strip() or None,
|
|
sofiia_url=(body.get("sofiia_url") or "").strip() or None,
|
|
data_json=body.get("data"),
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
return out
|
|
|
|
|
|
@runbook_runs_router.get("/{run_id}")
|
|
async def get_run(
|
|
run_id: str,
|
|
_auth: str = Depends(require_auth),
|
|
):
|
|
"""Get run and steps (light)."""
|
|
run = await runner.get_run(run_id)
|
|
if not run:
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
return run
|
|
|
|
|
|
@runbook_runs_router.post("/{run_id}/next")
|
|
async def next_step(
|
|
run_id: str,
|
|
body: Dict[str, Any] = Body(default=None),
|
|
_auth: str = Depends(require_auth),
|
|
):
|
|
"""Execute next step: if http_check run it and return result; if manual return instructions."""
|
|
operator_id = (body or {}).get("operator_id") or ""
|
|
out = await runner.next_step(run_id, operator_id=operator_id)
|
|
if out is None:
|
|
raise HTTPException(status_code=404, detail="Run not found or not active")
|
|
return out
|
|
|
|
|
|
@runbook_runs_router.post("/{run_id}/steps/{step_index}/complete")
|
|
async def complete_step(
|
|
run_id: str,
|
|
step_index: int,
|
|
body: Dict[str, Any] = Body(...),
|
|
_auth: str = Depends(require_auth),
|
|
):
|
|
"""Mark manual step complete. Body: status (ok|warn|fail|skipped), notes, data."""
|
|
status = (body.get("status") or "ok").strip()
|
|
notes = (body.get("notes") or "").strip()
|
|
data = body.get("data")
|
|
operator_id = (body.get("operator_id") or "").strip()
|
|
ok = await runner.complete_step(run_id, step_index, status=status, notes=notes, data=data, operator_id=operator_id)
|
|
if not ok:
|
|
raise HTTPException(status_code=404, detail="Run/step not found or step not current")
|
|
return {"ok": True, "run_id": run_id, "step_index": step_index, "next_step": step_index + 1}
|
|
|
|
|
|
@runbook_runs_router.get("/{run_id}/artifacts")
|
|
async def list_artifacts(
|
|
run_id: str,
|
|
_auth: str = Depends(require_auth),
|
|
):
|
|
"""List generated artifacts for a run (paths, sizes, mtimes)."""
|
|
return await artifacts.list_run_artifacts(run_id)
|
|
|
|
|
|
@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,
|
|
body: Dict[str, Any] = Body(default=None),
|
|
_auth: str = Depends(require_auth),
|
|
):
|
|
"""Abort run."""
|
|
operator_id = (body or {}).get("operator_id") or ""
|
|
ok = await runner.abort_run(run_id, operator_id=operator_id)
|
|
if not ok:
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
return {"ok": True, "run_id": run_id, "status": "aborted"}
|