use crate::error::{Result, StateError};
use crate::git::{CommitInfo, TagInfo, PushInfo};
use crate::publish::PublishResult;
use crate::version::{VersionBump, UpdateResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub const STATE_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseState {
pub format_version: u32,
pub release_id: String,
pub target_version: semver::Version,
pub version_bump: VersionBump,
pub started_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub current_phase: ReleasePhase,
pub checkpoints: Vec<ReleaseCheckpoint>,
pub version_state: Option<VersionState>,
pub git_state: Option<GitState>,
pub publish_state: Option<PublishState>,
pub errors: Vec<ReleaseError>,
pub config: ReleaseConfig,
pub original_versions: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReleasePhase {
Validation,
VersionUpdate,
GitOperations,
Publishing,
Cleanup,
Completed,
Failed,
RollingBack,
RolledBack,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseCheckpoint {
pub name: String,
pub phase: ReleasePhase,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub data: Option<serde_json::Value>,
pub rollback_capable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionState {
pub previous_version: semver::Version,
pub new_version: semver::Version,
pub update_result: Option<VersionUpdateInfo>,
pub modified_files: Vec<PathBuf>,
pub backup_files: Vec<FileBackup>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitState {
pub previous_head: Option<String>,
pub release_commit: Option<GitCommitInfo>,
pub release_tag: Option<GitTagInfo>,
pub push_info: Option<GitPushInfo>,
pub pushed_to_remote: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishState {
pub published_packages: HashMap<String, PublishPackageInfo>,
pub failed_packages: HashMap<String, String>,
pub current_tier: usize,
pub total_tiers: usize,
pub publishing_started_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseError {
pub message: String,
pub phase: ReleasePhase,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub recoverable: bool,
pub context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseConfig {
pub dry_run_first: bool,
pub push_to_remote: bool,
pub inter_package_delay_ms: u64,
pub registry: Option<String>,
pub allow_dirty: bool,
pub additional_options: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionUpdateInfo {
pub packages_updated: usize,
pub dependencies_updated: usize,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitCommitInfo {
pub hash: String,
pub short_hash: String,
pub message: String,
pub author_name: String,
pub author_email: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitTagInfo {
pub name: String,
pub message: Option<String>,
pub target_commit: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub is_annotated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitPushInfo {
pub remote_name: String,
pub commits_pushed: usize,
pub tags_pushed: usize,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishPackageInfo {
pub package_name: String,
pub version: semver::Version,
pub duration_ms: u64,
pub retry_attempts: usize,
pub warnings: Vec<String>,
pub published_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileBackup {
pub file_path: PathBuf,
pub backup_content: String,
pub backup_timestamp: chrono::DateTime<chrono::Utc>,
}
impl ReleaseState {
pub fn new(
target_version: semver::Version,
version_bump: VersionBump,
config: ReleaseConfig,
) -> Self {
let now = chrono::Utc::now();
let release_id = format!("release-{}-{}", target_version, now.timestamp());
Self {
format_version: STATE_FORMAT_VERSION,
release_id,
target_version,
version_bump,
started_at: now,
updated_at: now,
current_phase: ReleasePhase::Validation,
checkpoints: Vec::new(),
version_state: None,
git_state: None,
publish_state: None,
errors: Vec::new(),
config,
original_versions: None,
}
}
pub fn add_checkpoint(
&mut self,
name: String,
phase: ReleasePhase,
data: Option<serde_json::Value>,
rollback_capable: bool,
) {
let checkpoint = ReleaseCheckpoint {
name,
phase,
timestamp: chrono::Utc::now(),
data,
rollback_capable,
};
self.checkpoints.push(checkpoint);
self.updated_at = chrono::Utc::now();
}
pub fn set_phase(&mut self, phase: ReleasePhase) {
self.current_phase = phase;
self.updated_at = chrono::Utc::now();
}
pub fn add_error(
&mut self,
message: String,
phase: ReleasePhase,
recoverable: bool,
context: Option<String>,
) {
let error = ReleaseError {
message,
phase,
timestamp: chrono::Utc::now(),
recoverable,
context,
};
self.errors.push(error);
self.updated_at = chrono::Utc::now();
}
pub fn set_version_state(&mut self, update_result: &UpdateResult) {
self.version_state = Some(VersionState {
previous_version: update_result.previous_version.clone(),
new_version: update_result.new_version.clone(),
update_result: Some(VersionUpdateInfo {
packages_updated: update_result.packages_updated,
dependencies_updated: update_result.dependencies_updated,
duration_ms: 0, }),
modified_files: update_result.modified_files.clone(),
backup_files: Vec::new(), });
self.updated_at = chrono::Utc::now();
}
pub fn set_original_versions(&mut self, versions: HashMap<String, String>) {
self.original_versions = Some(versions);
self.updated_at = chrono::Utc::now();
}
pub fn set_git_state(&mut self, commit: Option<&CommitInfo>, tag: Option<&TagInfo>) {
if self.git_state.is_none() {
self.git_state = Some(GitState {
previous_head: None,
release_commit: None,
release_tag: None,
push_info: None,
pushed_to_remote: false,
});
}
if let Some(git_state) = &mut self.git_state {
if let Some(commit) = commit {
git_state.release_commit = Some(GitCommitInfo {
hash: commit.hash.clone(),
short_hash: commit.short_hash.clone(),
message: commit.message.clone(),
author_name: commit.author_name.clone(),
author_email: commit.author_email.clone(),
timestamp: commit.timestamp,
});
}
if let Some(tag) = tag {
git_state.release_tag = Some(GitTagInfo {
name: tag.name.clone(),
message: tag.message.clone(),
target_commit: tag.target_commit.clone(),
timestamp: tag.timestamp,
is_annotated: tag.is_annotated,
});
}
}
self.updated_at = chrono::Utc::now();
}
pub fn set_git_push_state(&mut self, push_info: &PushInfo) {
if let Some(git_state) = &mut self.git_state {
git_state.push_info = Some(GitPushInfo {
remote_name: push_info.remote_name.clone(),
commits_pushed: push_info.commits_pushed,
tags_pushed: push_info.tags_pushed,
warnings: push_info.warnings.clone(),
});
git_state.pushed_to_remote = true;
}
self.updated_at = chrono::Utc::now();
}
pub fn init_publish_state(&mut self, total_tiers: usize) {
self.publish_state = Some(PublishState {
published_packages: HashMap::new(),
failed_packages: HashMap::new(),
current_tier: 0,
total_tiers,
publishing_started_at: Some(chrono::Utc::now()),
});
self.updated_at = chrono::Utc::now();
}
pub fn add_published_package(&mut self, publish_result: &PublishResult) {
if let Some(publish_state) = &mut self.publish_state {
let package_info = PublishPackageInfo {
package_name: publish_result.package_name.clone(),
version: publish_result.version.clone(),
duration_ms: publish_result.duration.as_millis() as u64,
retry_attempts: publish_result.retry_attempts,
warnings: publish_result.warnings.clone(),
published_at: chrono::Utc::now(),
};
publish_state.published_packages.insert(
publish_result.package_name.clone(),
package_info,
);
}
self.updated_at = chrono::Utc::now();
}
pub fn add_failed_package(&mut self, package_name: String, error: String) {
if let Some(publish_state) = &mut self.publish_state {
publish_state.failed_packages.insert(package_name, error);
}
self.updated_at = chrono::Utc::now();
}
pub fn set_current_tier(&mut self, tier: usize) {
if let Some(publish_state) = &mut self.publish_state {
publish_state.current_tier = tier;
}
self.updated_at = chrono::Utc::now();
}
pub fn is_resumable(&self) -> bool {
matches!(
self.current_phase,
ReleasePhase::Validation
| ReleasePhase::VersionUpdate
| ReleasePhase::GitOperations
| ReleasePhase::Publishing
) && !self.has_critical_errors()
}
pub fn has_critical_errors(&self) -> bool {
self.errors.iter().any(|e| !e.recoverable)
}
pub fn get_rollback_checkpoints(&self) -> Vec<&ReleaseCheckpoint> {
self.checkpoints
.iter()
.filter(|cp| cp.rollback_capable)
.rev()
.collect()
}
pub fn progress_percentage(&self) -> f64 {
match self.current_phase {
ReleasePhase::Validation => 10.0,
ReleasePhase::VersionUpdate => 30.0,
ReleasePhase::GitOperations => 50.0,
ReleasePhase::Publishing => {
if let Some(publish_state) = &self.publish_state {
if publish_state.total_tiers > 0 {
let tier_progress = (publish_state.current_tier as f64 / publish_state.total_tiers as f64) * 40.0;
50.0 + tier_progress
} else {
70.0
}
} else {
70.0
}
}
ReleasePhase::Cleanup => 95.0,
ReleasePhase::Completed => 100.0,
ReleasePhase::Failed | ReleasePhase::RollingBack | ReleasePhase::RolledBack => {
0.0
}
}
}
pub fn elapsed_time(&self) -> chrono::Duration {
self.updated_at - self.started_at
}
pub fn validate(&self) -> Result<()> {
if self.format_version != STATE_FORMAT_VERSION {
return Err(StateError::VersionMismatch {
expected: STATE_FORMAT_VERSION.to_string(),
found: self.format_version.to_string(),
}.into());
}
match self.current_phase {
ReleasePhase::VersionUpdate => {
if self.version_state.is_none() {
return Err(StateError::Corrupted {
reason: "Version update phase but no version state".to_string(),
}.into());
}
}
ReleasePhase::GitOperations => {
if self.git_state.is_none() {
return Err(StateError::Corrupted {
reason: "Git operations phase but no git state".to_string(),
}.into());
}
}
ReleasePhase::Publishing => {
if self.publish_state.is_none() {
return Err(StateError::Corrupted {
reason: "Publishing phase but no publish state".to_string(),
}.into());
}
}
_ => {}
}
Ok(())
}
pub fn summary(&self) -> String {
let elapsed = self.elapsed_time();
let progress = self.progress_percentage();
format!(
"Release v{} ({:?}) - {:.1}% complete - {} elapsed",
self.target_version,
self.current_phase,
progress,
format_duration(elapsed)
)
}
}
fn format_duration(duration: chrono::Duration) -> String {
let total_seconds = duration.num_seconds();
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
if hours > 0 {
format!("{}h {}m {}s", hours, minutes, seconds)
} else if minutes > 0 {
format!("{}m {}s", minutes, seconds)
} else {
format!("{}s", seconds)
}
}
impl Default for ReleaseConfig {
fn default() -> Self {
Self {
dry_run_first: true,
push_to_remote: true,
inter_package_delay_ms: 15000, registry: None,
allow_dirty: false,
additional_options: HashMap::new(),
}
}
}
impl std::fmt::Display for ReleasePhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReleasePhase::Validation => write!(f, "Validation"),
ReleasePhase::VersionUpdate => write!(f, "Version Update"),
ReleasePhase::GitOperations => write!(f, "Git Operations"),
ReleasePhase::Publishing => write!(f, "Publishing"),
ReleasePhase::Cleanup => write!(f, "Cleanup"),
ReleasePhase::Completed => write!(f, "Completed"),
ReleasePhase::Failed => write!(f, "Failed"),
ReleasePhase::RollingBack => write!(f, "Rolling Back"),
ReleasePhase::RolledBack => write!(f, "Rolled Back"),
}
}
}