apiforge 0.1.0

Production-grade API release automation CLI. From merged code to healthy pods in production — one command.
Documentation
use std::collections::HashMap;

use chrono::Utc;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseRecord {
    pub id: String,
    pub version: String,
    pub bump_type: String,
    pub timestamp: String,
    pub status: ReleaseStatus,
    pub steps: Vec<StepRecord>,
    pub duration_ms: u64,
    pub dry_run: bool,
    pub metadata: HashMap<String, String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ReleaseStatus {
    Success,
    Failed,
    RolledBack,
}

impl std::fmt::Display for ReleaseStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ReleaseStatus::Success => write!(f, "success"),
            ReleaseStatus::Failed => write!(f, "failed"),
            ReleaseStatus::RolledBack => write!(f, "rolled_back"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepRecord {
    pub name: String,
    pub status: StepStatus,
    pub duration_ms: u64,
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum StepStatus {
    Success,
    Failed,
    Skipped,
}

pub struct AuditStore {
    db: sled::Db,
}

impl AuditStore {
    pub fn open(path: &std::path::Path) -> crate::error::Result<Self> {
        let db = sled::open(path).map_err(|e| {
            crate::error::ApiForgError::Audit(format!("Failed to open audit DB: {}", e))
        })?;
        Ok(Self { db })
    }

    pub fn record(&self, record: &ReleaseRecord) -> crate::error::Result<()> {
        let key = format!("{}_{}", record.timestamp, record.id);
        let value = serde_json::to_vec(record).map_err(|e| {
            crate::error::ApiForgError::Audit(format!("Failed to serialize record: {}", e))
        })?;
        self.db.insert(key.as_bytes(), value).map_err(|e| {
            crate::error::ApiForgError::Audit(format!("Failed to write record: {}", e))
        })?;
        Ok(())
    }

    pub fn list(&self, limit: usize) -> crate::error::Result<Vec<ReleaseRecord>> {
        let mut records: Vec<ReleaseRecord> = Vec::new();
        for entry in self.db.iter().rev() {
            if records.len() >= limit {
                break;
            }
            let (_key, value) = entry.map_err(|e| {
                crate::error::ApiForgError::Audit(format!("Failed to read record: {}", e))
            })?;
            let record: ReleaseRecord = serde_json::from_slice(&value).map_err(|e| {
                crate::error::ApiForgError::Audit(format!("Failed to deserialize record: {}", e))
            })?;
            records.push(record);
        }
        Ok(records)
    }

    pub fn new_record(version: &str, bump_type: &str, dry_run: bool) -> ReleaseRecord {
        ReleaseRecord {
            id: Uuid::new_v4().to_string(),
            version: version.to_string(),
            bump_type: bump_type.to_string(),
            timestamp: Utc::now().to_rfc3339(),
            status: ReleaseStatus::Success,
            steps: Vec::new(),
            duration_ms: 0,
            dry_run,
            metadata: HashMap::new(),
        }
    }
}