Skip to main content

mockforge_intelligence/handlers/drift_budget/
gitops.rs

1//! GitOps incident selection and downstream pipeline event metadata.
2
3use super::DriftBudgetStateProvider;
4use super::DriftGitOps;
5use crate::handlers::structural_incidents::StructuralIncidents;
6use axum::{extract::State, http::StatusCode, response::Json};
7use mockforge_foundation::incidents_types::{
8    DriftIncident, IncidentQuery, IncidentStatus, IncidentType,
9};
10use serde::Deserialize;
11
12/// Request to generate GitOps PR from incidents
13#[derive(Debug, Deserialize)]
14pub struct GeneratePRRequest {
15    /// Optional list of specific incident IDs
16    pub incident_ids: Option<Vec<String>>,
17    /// Optional workspace ID filter
18    pub workspace_id: Option<String>,
19    /// Optional status filter (e.g., "open")
20    pub status: Option<String>,
21}
22
23fn emit_threshold_event<S: DriftBudgetStateProvider>(
24    state: &S,
25    incidents: &[DriftIncident],
26    workspace_override: Option<&str>,
27) {
28    let exceeded = incidents
29        .iter()
30        .filter(|incident| matches!(incident.incident_type, IncidentType::ThresholdExceeded))
31        .count();
32    if exceeded == 0 {
33        return;
34    }
35    let workspace = workspace_override
36        .and_then(|id| uuid::Uuid::parse_str(id).ok())
37        .or_else(|| {
38            incidents
39                .first()
40                .and_then(|incident| incident.workspace_id.as_deref())
41                .and_then(|id| uuid::Uuid::parse_str(id).ok())
42        })
43        .unwrap_or_else(uuid::Uuid::new_v4);
44    let endpoint = incidents
45        .first()
46        .map(|incident| format!("{} {}", incident.method, incident.endpoint))
47        .unwrap_or_else(|| "unknown".to_string());
48    state.publish_drift_threshold(workspace, endpoint, exceeded as i32, incidents.len() as i32);
49}
50
51/// Generate GitOps PR from drift incidents
52///
53/// POST /api/v1/drift/gitops/generate-pr
54pub async fn generate_gitops_pr<S: DriftBudgetStateProvider>(
55    State(state): State<S>,
56    Json(request): Json<GeneratePRRequest>,
57) -> Result<Json<serde_json::Value>, StatusCode> {
58    let handler = state.gitops_handler().ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
59
60    // Get incidents to include in PR
61    let mut query = IncidentQuery::default();
62
63    if let Some(incident_ids) = &request.incident_ids {
64        // Filter by specific incident IDs
65        // Note: IncidentQuery doesn't support ID filtering yet, so we'll get all and filter
66        let all_incidents = state.incident_manager().query_incidents(query).await;
67        let incidents: Vec<_> =
68            all_incidents.into_iter().filter(|inc| incident_ids.contains(&inc.id)).collect();
69
70        match handler.generate_pr_from_incidents(&incidents).await {
71            Ok(Some(pr_result)) => {
72                emit_threshold_event(&state, &incidents, None);
73
74                Ok(Json(serde_json::json!({
75                    "success": true,
76                    "pr": pr_result,
77                })))
78            }
79            Ok(None) => Ok(Json(serde_json::json!({
80                "success": false,
81                "message": "No PR generated (no file changes or incidents)",
82            }))),
83            Err(_e) => Err(StatusCode::INTERNAL_SERVER_ERROR),
84        }
85    } else {
86        // Filter by workspace and/or status
87        let workspace_id_str = request.workspace_id.clone();
88        query.workspace_id = request.workspace_id;
89        if let Some(status_str) = &request.status {
90            query.status = match status_str.as_str() {
91                "open" => Some(IncidentStatus::Open),
92                "acknowledged" => Some(IncidentStatus::Acknowledged),
93                _ => None,
94            };
95        }
96
97        let incidents = state.incident_manager().query_incidents(query).await;
98
99        match handler.generate_pr_from_incidents(&incidents).await {
100            Ok(Some(pr_result)) => {
101                emit_threshold_event(&state, &incidents, workspace_id_str.as_deref());
102
103                Ok(Json(serde_json::json!({
104                    "success": true,
105                    "pr": pr_result,
106                    "incidents_included": incidents.len(),
107                })))
108            }
109            Ok(None) => Ok(Json(serde_json::json!({
110                "success": false,
111                "message": "No PR generated (no file changes or incidents)",
112            }))),
113            Err(_e) => Err(StatusCode::INTERNAL_SERVER_ERROR),
114        }
115    }
116}