//! Rollout monitor — checks crash rate, advances or halts rollout. use crate::{config::RolloutConfig, PublishError, PublishResult}; /// Current rollout state. #[derive(Debug, Clone)] pub struct RolloutState { pub track: String, pub current_percent: u8, pub crash_rate: f64, pub is_halted: bool, pub hours_elapsed: u64, } impl RolloutState { pub fn new(track: impl Into, initial_percent: u8) -> Self { Self { track: track.into(), current_percent: initial_percent, crash_rate: 0.0, is_halted: false, hours_elapsed: 0, } } } /// Monitors a staged rollout and decides whether to advance or halt. pub struct RolloutMonitor { pub config: RolloutConfig, } impl RolloutMonitor { pub fn new(config: RolloutConfig) -> Self { Self { config } } /// Evaluate whether to advance, halt, or maintain the current rollout. pub fn evaluate(&self, state: &RolloutState) -> RolloutDecision { if state.is_halted { return RolloutDecision::Halted { reason: "rollout was previously halted".into(), }; } // Check crash rate if state.crash_rate > self.config.max_crash_rate { return RolloutDecision::Halt { reason: format!( "crash rate {:.2}% exceeds threshold {:.2}%", state.crash_rate * 100.0, self.config.max_crash_rate * 100.0 ), }; } // Check if we should advance if self.config.auto_advance && state.hours_elapsed >= self.config.advance_after_hours && state.current_percent < 100 { let next_percent = advance_percent(state.current_percent); return RolloutDecision::Advance { to_percent: next_percent }; } RolloutDecision::Maintain } /// Apply a rollout decision — returns the new rollout state. pub fn apply( &self, mut state: RolloutState, decision: &RolloutDecision, ) -> PublishResult { match decision { RolloutDecision::Advance { to_percent } => { state.current_percent = *to_percent; state.hours_elapsed = 0; } RolloutDecision::Halt { reason } => { state.is_halted = true; let _ = reason; } RolloutDecision::Halted { .. } => { return Err(PublishError::Config( "cannot apply decision to halted rollout".into(), )); } RolloutDecision::Maintain => {} } Ok(state) } /// Run a complete rollout cycle: fetch metrics, evaluate, apply. /// /// In production: fetch crash rate from Firebase Crashlytics or App Store /// Connect Analytics API. pub fn tick(&self, mut state: RolloutState) -> PublishResult<(RolloutState, RolloutDecision)> { // TODO: fetch real crash rate from analytics API // let crash_rate = analytics_client.crash_rate(&state.track, &config.build).await?; // state.crash_rate = crash_rate; state.hours_elapsed += 1; let decision = self.evaluate(&state); let new_state = self.apply(state, &decision)?; Ok((new_state, decision)) } } /// The outcome of a rollout evaluation. #[derive(Debug, Clone)] pub enum RolloutDecision { /// Advance to a higher rollout percentage. Advance { to_percent: u8 }, /// Halt the rollout due to high crash rate. Halt { reason: String }, /// Rollout was already halted. Halted { reason: String }, /// No change needed. Maintain, } /// Determine the next rollout percentage. /// Uses the standard staged-rollout progression: 10 → 25 → 50 → 100. fn advance_percent(current: u8) -> u8 { match current { 0..=9 => 10, 10..=24 => 25, 25..=49 => 50, _ => 100, } }