Matthias Nott
2026-02-22 f80c96be55296d0f6184a9fdff8fbe0409a23a46
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import json
from datetime import datetime, timezone
from typing import AsyncGenerator, Literal
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from app.auth import verify_token
from app.ops_runner import _BACKUP_TIMEOUT, stream_ops_host
router = APIRouter()
# Only adjacent-environment promote paths are allowed (code flows up)
_VALID_PROMOTE_PAIRS = {("dev", "int"), ("int", "prod")}
def _sse_line(payload: dict) -> str:
    return f"data: {json.dumps(payload)}\n\n"
def _now() -> str:
    return datetime.now(timezone.utc).isoformat()
async def _promote_generator(
    project: str,
    from_env: str,
    to_env: str,
    dry_run: bool,
) -> AsyncGenerator[str, None]:
    """Stream promote output via SSE."""
    args = ["promote", project, from_env, to_env]
    if dry_run:
        args.append("--dry-run")
    args.append("--yes")
    label = f"Promoting {project}: {from_env} -> {to_env}"
    if dry_run:
        label += "  (dry run)"
    yield _sse_line({"line": label, "timestamp": _now()})
    success = True
    async for line in stream_ops_host(args, timeout=_BACKUP_TIMEOUT):
        yield _sse_line({"line": line, "timestamp": _now()})
        if line.startswith("[error]") or "failed" in line.lower():
            success = False
    yield _sse_line({"done": True, "success": success})
@router.get("/{project}/{from_env}/{to_env}", summary="Promote code with real-time output")
async def promote_code(
    project: str,
    from_env: str,
    to_env: str,
    dry_run: bool = Query(default=False, alias="dry_run"),
    _: str = Depends(verify_token),
) -> StreamingResponse:
    """Promote code forward (dev->int, int->prod) with SSE streaming."""
    if (from_env, to_env) not in _VALID_PROMOTE_PAIRS:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid promote path '{from_env} -> {to_env}'. Only adjacent pairs are allowed: dev->int, int->prod.",
        )
    return StreamingResponse(
        _promote_generator(project, from_env, to_env, dry_run),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
        },
    )