You need to sign in before continuing.
deployment_query.py 4.09 KB
"""U-043: read-side controllers for `v_deployment_status` -- no operation_id
for these exists anywhere in 03-C/the MVCP JSON (this whole view is a PH5
addition with no 02-V spec to begin with, see development-plan.md's own
U-043 note), same "view's own filter/display needs directly justify a new
read endpoint" precedent as `c_task_list`/`c_artifact_list` before it.
`deployment` has no `project_id` column (01-M 1.15) -- there's exactly one
graphlemon self-target, not one per project, so these never take a
project_id filter the way task/artifact listing does.
"""

_STATUSES = (
    "staged", "candidate_starting", "health_check_running", "health_check_passed",
    "health_check_failed", "cutover_in_progress", "live", "monitoring",
    "anomaly_detected", "rollback_in_progress", "rolled_back", "rollback_failed",
    "blocked_pending_human", "monitoring_window_clean", "finalized",
)
_TERMINAL_STATUSES = {"rolled_back", "rollback_failed", "finalized", "blocked_pending_human"}


class DeploymentNotFoundError(Exception):
    """NOT_FOUND -- c_deployment_get on a nonexistent deployment_id."""


def _deployment_row_to_dict(r):
    return {
        "deployment_id": r[0], "git_commit_hash": r[1], "previous_commit_hash": r[2], "status": r[3],
        "candidate_port": r[4], "candidate_instance_dir": r[5],
        "started_at": r[6], "cutover_at": r[7], "monitoring_closed_at": r[8],
        "rollback_at": r[9], "rollback_reason": r[10], "previous_instance_dir": r[11],
        "is_active": r[3] not in _TERMINAL_STATUSES,
    }


_DEPLOYMENT_COLUMNS = (
    "deployment_id, git_commit_hash, previous_commit_hash, status, candidate_port, candidate_instance_dir, "
    "started_at, cutover_at, monitoring_closed_at, rollback_at, rollback_reason, previous_instance_dir"
)


def list_deployments(conn, limit=20):
    """Newest-first (`rowid DESC` -- `deployment` has no created_at column
    of its own, `started_at` is nullable until c_deploy_supervisor_stage
    actually sets it, same ordering convention U-016/U-037 already use
    elsewhere for "most recent" when a table lacks a dedicated timestamp)."""
    rows = conn.execute(
        "SELECT {} FROM deployment ORDER BY rowid DESC LIMIT ?".format(_DEPLOYMENT_COLUMNS), (limit,)
    ).fetchall()
    return [_deployment_row_to_dict(r) for r in rows]


def get_current_deployment(conn):
    """The single non-terminal deployment, if any (01-M 1.15's own
    `ux_deployment_active` constraint guarantees at most one) -- what
    `v_deployment_status` actually wants front and center: "当前是否有
    deployment 在进行中". Returns None when nothing is active (the common
    case -- most of the time nothing is deploying)."""
    row = conn.execute(
        "SELECT {} FROM deployment WHERE status NOT IN ({}) ORDER BY rowid DESC LIMIT 1".format(
            _DEPLOYMENT_COLUMNS, ", ".join("'{}'".format(s) for s in _TERMINAL_STATUSES)
        )
    ).fetchone()
    return _deployment_row_to_dict(row) if row else None


def get_deployment(conn, deployment_id):
    """Includes recent `deploy_health_signal` rows (newest first) -- "最近
    一次健康信号采样结果" per this Unit's own Done line, but the full recent
    history (not just the latest) is what actually lets a human tell "one
    blip" from "a real pattern" at a glance, same reasoning U-026's
    `v_graph_visualizer` tooltip already applies to per-node detail."""
    row = conn.execute(
        "SELECT {} FROM deployment WHERE deployment_id = ?".format(_DEPLOYMENT_COLUMNS), (deployment_id,)
    ).fetchone()
    if row is None:
        raise DeploymentNotFoundError(deployment_id)
    result = _deployment_row_to_dict(row)
    result["health_signals"] = _recent_signals(conn, deployment_id)
    return result


def _recent_signals(conn, deployment_id, limit=50):
    rows = conn.execute(
        "SELECT signal_id, sampled_at, signal_type, status, detail FROM deploy_health_signal "
        "WHERE deployment_id = ? ORDER BY rowid DESC LIMIT ?",
        (deployment_id, limit),
    ).fetchall()
    return [
        {"signal_id": r[0], "sampled_at": r[1], "signal_type": r[2], "status": r[3], "detail": r[4]}
        for r in rows
    ]