use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, ReleaseError>;
#[derive(Error, Debug)]
pub enum ReleaseError {
#[error("Workspace error: {0}")]
Workspace(#[from] WorkspaceError),
#[error("Version error: {0}")]
Version(#[from] VersionError),
#[error("Git error: {0}")]
Git(#[from] GitError),
#[error("Publish error: {0}")]
Publish(#[from] PublishError),
#[error("State error: {0}")]
State(#[from] StateError),
#[error("CLI error: {0}")]
Cli(#[from] CliError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("TOML error: {0}")]
Toml(#[from] toml::de::Error),
#[error("TOML edit error: {0}")]
TomlEdit(#[from] toml_edit::TomlError),
}
#[derive(Error, Debug)]
pub enum WorkspaceError {
#[error("Could not find workspace root. Please run from within a Cargo workspace.")]
RootNotFound,
#[error("Invalid workspace structure: {reason}")]
InvalidStructure {
reason: String
},
#[error("Package '{name}' not found in workspace")]
PackageNotFound {
name: String
},
#[error("Circular dependency detected in packages: {packages:?}")]
CircularDependency {
packages: Vec<String>
},
#[error("Missing Cargo.toml file at {path}")]
MissingCargoToml {
path: PathBuf
},
#[error("Invalid package configuration for '{package}': {reason}")]
InvalidPackage {
package: String,
reason: String
},
}
#[derive(Error, Debug)]
pub enum VersionError {
#[error("Invalid version '{version}': {reason}")]
InvalidVersion {
version: String,
reason: String
},
#[error("Failed to parse version '{version}': {source}")]
ParseFailed {
version: String,
#[source]
source: semver::Error,
},
#[error("Internal dependency version mismatch for '{dependency}': expected {expected}, found {found}")]
DependencyMismatch {
dependency: String,
expected: String,
found: String,
},
#[error("Failed to update Cargo.toml at {path}: {reason}")]
TomlUpdateFailed {
path: PathBuf,
reason: String
},
#[error("Version bump '{bump}' not supported for version '{version}'")]
UnsupportedBump {
bump: String,
version: String
},
}
#[derive(Error, Debug)]
pub enum GitError {
#[error("Not a git repository. Please initialize git first.")]
NotRepository,
#[error("Git operation '{operation}' failed: {reason}")]
OperationFailed {
operation: String,
reason: String,
},
#[error("Working directory not clean. Please commit or stash changes before releasing.")]
DirtyWorkingDirectory,
#[error("Git authentication failed: {reason}")]
AuthenticationFailed {
reason: String
},
#[error("Git remote operation failed: {operation} - {reason}")]
RemoteOperationFailed {
operation: String,
reason: String
},
#[error("Git tag '{tag}' already exists. Use --force to overwrite or choose a different version.")]
TagExists {
tag: String
},
#[error("Git branch operation failed: {reason}")]
BranchOperationFailed {
reason: String
},
#[error("Git commit failed: {reason}")]
CommitFailed {
reason: String
},
#[error("Git push failed: {reason}")]
PushFailed {
reason: String
},
}
#[derive(Error, Debug)]
pub enum PublishError {
#[error("Package '{package}' version '{version}' already published to crates.io")]
AlreadyPublished {
package: String,
version: String
},
#[error("Cargo publish failed for '{package}': {reason}")]
PublishFailed {
package: String,
reason: String
},
#[error("Dry run validation failed for '{package}': {reason}")]
DryRunFailed {
package: String,
reason: String
},
#[error("Rate limit exceeded for crates.io. Please wait {retry_after_seconds} seconds before retrying.")]
RateLimitExceeded {
retry_after_seconds: u64
},
#[error("Network error during publishing: {reason}")]
NetworkError {
reason: String
},
#[error("Authentication error: Please ensure you're logged in with 'cargo login'")]
AuthenticationError,
#[error("Failed to yank package '{package}' version '{version}': {reason}")]
YankFailed {
package: String,
version: String,
reason: String,
},
}
#[derive(Error, Debug)]
pub enum StateError {
#[error("State file corrupted: {reason}")]
Corrupted {
reason: String
},
#[error("State file not found. No release in progress.")]
NotFound,
#[error("State file version mismatch: expected {expected}, found {found}")]
VersionMismatch {
expected: String,
found: String
},
#[error("Failed to save state: {reason}")]
SaveFailed {
reason: String
},
#[error("Failed to load state: {reason}")]
LoadFailed {
reason: String
},
}
#[derive(Error, Debug)]
pub enum CliError {
#[error("Invalid arguments: {reason}")]
InvalidArguments {
reason: String
},
#[error("Missing required argument: {argument}")]
MissingArgument {
argument: String
},
#[error("Conflicting arguments: {arguments:?}")]
ConflictingArguments {
arguments: Vec<String>
},
#[error("Command execution failed: {command} - {reason}")]
ExecutionFailed {
command: String,
reason: String
},
}
impl ReleaseError {
pub fn recovery_suggestions(&self) -> Vec<String> {
match self {
ReleaseError::Workspace(WorkspaceError::RootNotFound) => vec![
"Navigate to a directory containing a Cargo workspace".to_string(),
"Ensure you have a Cargo.toml file with [workspace] section".to_string(),
],
ReleaseError::Workspace(WorkspaceError::CircularDependency { packages }) => vec![
format!("Review dependencies between packages: {}", packages.join(", ")),
"Remove circular dependencies by restructuring package relationships".to_string(),
],
ReleaseError::Git(GitError::DirtyWorkingDirectory) => vec![
"Commit pending changes: git add . && git commit -m 'message'".to_string(),
"Stash changes temporarily: git stash".to_string(),
"Reset working directory: git reset --hard HEAD".to_string(),
],
ReleaseError::Git(GitError::AuthenticationFailed { .. }) => vec![
"Check SSH key configuration: ssh -T git@github.com".to_string(),
"Verify git remote URL: git remote -v".to_string(),
"Regenerate SSH keys if needed".to_string(),
],
ReleaseError::Publish(PublishError::AuthenticationError) => vec![
"Login to crates.io: cargo login".to_string(),
"Verify API token is valid and has publish permissions".to_string(),
],
ReleaseError::Publish(PublishError::RateLimitExceeded { retry_after_seconds }) => vec![
format!("Wait {} seconds before retrying", retry_after_seconds),
"Use --publish-interval to add delays between packages".to_string(),
],
_ => vec!["Check the error message above for specific details".to_string()],
}
}
pub fn is_recoverable(&self) -> bool {
match self {
ReleaseError::Workspace(WorkspaceError::RootNotFound) => false,
ReleaseError::Workspace(WorkspaceError::CircularDependency { .. }) => false,
ReleaseError::Git(GitError::NotRepository) => false,
ReleaseError::Version(VersionError::InvalidVersion { .. }) => false,
ReleaseError::Publish(PublishError::AlreadyPublished { .. }) => false,
_ => true,
}
}
}