use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{DeployError, Result};
use crate::types::DeployPlan;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
pub id: String,
pub status: String,
pub path: String,
pub timestamp: DateTime<Utc>,
pub env: String,
pub target: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeployHistoryIndex {
pub entries: Vec<HistoryEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployHistoryRecord {
pub id: String,
pub timestamp: DateTime<Utc>,
pub env: String,
pub target: String,
pub target_kind: String,
pub status: String,
pub services: Vec<String>,
pub git_sha: Option<String>,
pub project_version: Option<String>,
pub digests: std::collections::BTreeMap<String, String>,
pub kubernetes_context: Option<String>,
pub namespace: Option<String>,
pub summary: String,
pub error: Option<String>,
pub plan: DeployPlan,
}
pub struct DeployHistoryStore {
pub dir: PathBuf,
}
impl DeployHistoryStore {
pub fn new(dir: impl Into<PathBuf>) -> Self {
Self { dir: dir.into() }
}
pub fn index_path(&self) -> PathBuf {
self.dir.join("index.json")
}
pub fn load_index(&self) -> Result<DeployHistoryIndex> {
let path = self.index_path();
if !path.exists() {
return Ok(DeployHistoryIndex::default());
}
let raw = std::fs::read_to_string(&path).map_err(|e| DeployError::Io(e.to_string()))?;
serde_json::from_str(&raw).map_err(|e| DeployError::Io(e.to_string()))
}
pub fn write_record(&self, record: &DeployHistoryRecord) -> Result<PathBuf> {
std::fs::create_dir_all(&self.dir).map_err(|e| DeployError::Io(e.to_string()))?;
let file_name = format!("{}.json", record.id);
let path = self.dir.join(&file_name);
let body =
serde_json::to_string_pretty(record).map_err(|e| DeployError::Io(e.to_string()))?;
std::fs::write(&path, body).map_err(|e| DeployError::Io(e.to_string()))?;
let mut index = self.load_index()?;
index.entries.retain(|e| e.id != record.id);
index.entries.insert(
0,
HistoryEntry {
id: record.id.clone(),
status: record.status.clone(),
path: file_name,
timestamp: record.timestamp,
env: record.env.clone(),
target: record.target.clone(),
},
);
index.entries.truncate(200);
let index_body =
serde_json::to_string_pretty(&index).map_err(|e| DeployError::Io(e.to_string()))?;
std::fs::write(self.index_path(), index_body)
.map_err(|e| DeployError::Io(e.to_string()))?;
Ok(path)
}
pub fn latest_status(&self, target: &str, env: &str) -> Result<Option<HistoryEntry>> {
let index = self.load_index()?;
Ok(index
.entries
.into_iter()
.find(|e| (target == "all" || e.target == target) && (env.is_empty() || e.env == env)))
}
pub fn list(&self, target: &str, env: &str, limit: usize) -> Result<Vec<HistoryEntry>> {
let index = self.load_index()?;
Ok(index
.entries
.into_iter()
.filter(|e| (target == "all" || e.target == target) && (env.is_empty() || e.env == env))
.take(limit)
.collect())
}
}
pub fn make_record_id(env: &str, target: &str, ts: DateTime<Utc>) -> String {
let safe_target: String = target
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
format!("{}-{}-{}", ts.format("%Y%m%dT%H%M%SZ"), env, safe_target)
}
pub fn record_from_plan(
plan: &DeployPlan,
ok: bool,
summary: String,
error: Option<String>,
kubernetes_context: Option<String>,
) -> DeployHistoryRecord {
let ts = Utc::now();
let id = make_record_id(&plan.env, &plan.target.label(), ts);
let mut digests = std::collections::BTreeMap::new();
for svc in &plan.services {
if let Some(d) = &svc.digest {
digests.insert(svc.name.clone(), d.clone());
}
}
DeployHistoryRecord {
id,
timestamp: ts,
env: plan.env.clone(),
target: plan.target.label(),
target_kind: plan.target.kind_str().into(),
status: if ok { "success".into() } else { "failed".into() },
services: plan.order.clone(),
git_sha: plan.git_sha.clone(),
project_version: Some(plan.project_version.clone()),
digests,
kubernetes_context,
namespace: plan
.services
.first()
.and_then(|s| s.deploy.namespace.clone()),
summary,
error,
plan: plan.clone(),
}
}
#[allow(dead_code)]
pub fn history_dir(root: &Path, rel: &Path) -> PathBuf {
root.join(rel)
}