1use std::path::PathBuf;
2
3use changeset_saga::SagaError;
4use thiserror::Error;
5
6#[derive(Debug)]
8pub struct CompensationFailure {
9 pub step: String,
11 pub description: String,
13 pub error: Box<OperationError>,
15}
16
17#[derive(Debug, Error)]
18pub enum OperationError {
19 #[error(transparent)]
20 Core(#[from] changeset_core::ChangesetError),
21
22 #[error(transparent)]
23 Git(#[from] changeset_git::GitError),
24
25 #[error(transparent)]
26 Project(#[from] changeset_project::ProjectError),
27
28 #[error(transparent)]
29 Parse(#[from] changeset_parse::FormatError),
30
31 #[error(transparent)]
32 Manifest(#[from] changeset_manifest::ManifestError),
33
34 #[error(transparent)]
35 Changelog(#[from] changeset_changelog::ChangelogError),
36
37 #[error("version calculation failed")]
38 VersionCalculation(#[from] changeset_version::VersionError),
39
40 #[error("failed to read changeset file '{path}'")]
41 ChangesetFileRead {
42 path: PathBuf,
43 #[source]
44 source: std::io::Error,
45 },
46
47 #[error("failed to parse changeset file '{path}'")]
48 ChangesetParse {
49 path: PathBuf,
50 #[source]
51 source: changeset_parse::FormatError,
52 },
53
54 #[error("failed to write changeset file")]
55 ChangesetFileWrite(#[source] std::io::Error),
56
57 #[error("failed to list changeset files in '{path}'")]
58 ChangesetList {
59 path: PathBuf,
60 #[source]
61 source: std::io::Error,
62 },
63
64 #[error("operation cancelled")]
65 Cancelled,
66
67 #[error("no packages found in project at '{0}'")]
68 EmptyProject(PathBuf),
69
70 #[error("unknown package '{name}' (available: {available})")]
71 UnknownPackage { name: String, available: String },
72
73 #[error("missing bump type for package '{package_name}'")]
74 MissingBumpType { package_name: String },
75
76 #[error("missing description")]
77 MissingDescription,
78
79 #[error("description cannot be empty")]
80 EmptyDescription,
81
82 #[error("no packages selected")]
83 NoPackagesSelected,
84
85 #[error("interaction required but provider returned None")]
86 InteractionRequired,
87
88 #[error("IO error")]
89 Io(#[from] std::io::Error),
90
91 #[error("packages with inherited versions require --convert flag: {}", packages.join(", "))]
92 InheritedVersionsRequireConvert { packages: Vec<String> },
93
94 #[error("comparison links enabled but no repository URL available")]
95 ComparisonLinksRequired,
96
97 #[error("working tree has uncommitted changes; commit or stash them, or use --no-commit")]
98 DirtyWorkingTree,
99
100 #[error("current version is stable; please specify a pre-release tag: --prerelease <tag>")]
101 PrereleaseTagRequired,
102
103 #[error("no changesets found; use --force to release without changesets")]
104 NoChangesetsWithoutForce,
105
106 #[error("invalid changeset path '{path}': {reason}")]
107 InvalidChangesetPath { path: PathBuf, reason: &'static str },
108
109 #[error("failed to read release state file '{path}'")]
110 ReleaseStateRead {
111 path: PathBuf,
112 #[source]
113 source: std::io::Error,
114 },
115
116 #[error("failed to write release state file '{path}'")]
117 ReleaseStateWrite {
118 path: PathBuf,
119 #[source]
120 source: std::io::Error,
121 },
122
123 #[error("failed to parse release state file '{path}'")]
124 ReleaseStateParse {
125 path: PathBuf,
126 #[source]
127 source: toml::de::Error,
128 },
129
130 #[error("failed to serialize release state for '{path}'")]
131 ReleaseStateSerialize {
132 path: PathBuf,
133 #[source]
134 source: toml::ser::Error,
135 },
136
137 #[error("release validation failed")]
138 ValidationFailed(#[from] crate::operations::ValidationErrors),
139
140 #[error("failed to parse version '{version}' during {context}")]
141 VersionParse { version: String, context: String },
142
143 #[error("failed to delete {} tag(s) during compensation: {}", failed_tags.len(), failed_tags.join(", "))]
144 TagDeletionFailed { failed_tags: Vec<String> },
145
146 #[error("release saga failed at step '{step}'")]
147 SagaFailed {
148 step: String,
149 #[source]
150 source: Box<OperationError>,
151 },
152
153 #[error(
154 "release saga failed at step '{step}' and {} compensation(s) also failed", compensation_failures.len()
155 )]
156 SagaCompensationFailed {
157 step: String,
158 source: Box<OperationError>,
159 compensation_failures: Vec<CompensationFailure>,
160 },
161}
162
163pub type Result<T> = std::result::Result<T, OperationError>;
164
165impl From<SagaError<OperationError>> for OperationError {
166 fn from(err: SagaError<OperationError>) -> Self {
167 match err {
168 SagaError::StepFailed { step, source } => Self::SagaFailed {
169 step,
170 source: Box::new(source),
171 },
172 SagaError::CompensationFailed {
173 failed_step,
174 step_error,
175 compensation_errors,
176 } => {
177 let compensation_failures = compensation_errors
178 .into_iter()
179 .map(|e| CompensationFailure {
180 step: e.step,
181 description: e.description,
182 error: Box::new(e.error),
183 })
184 .collect();
185 Self::SagaCompensationFailed {
186 step: failed_step,
187 source: Box::new(step_error),
188 compensation_failures,
189 }
190 }
191 _ => Self::SagaFailed {
192 step: "unknown".to_string(),
193 source: Box::new(Self::Cancelled),
194 },
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn empty_project_error_includes_path() {
205 let err = OperationError::EmptyProject(PathBuf::from("/my/project"));
206
207 let msg = err.to_string();
208
209 assert!(msg.contains("/my/project"));
210 }
211
212 #[test]
213 fn unknown_package_error_includes_name_and_available() {
214 let err = OperationError::UnknownPackage {
215 name: "missing".to_string(),
216 available: "foo, bar".to_string(),
217 };
218
219 let msg = err.to_string();
220
221 assert!(msg.contains("missing"));
222 assert!(msg.contains("foo, bar"));
223 }
224
225 #[test]
226 fn cancelled_error_message() {
227 let err = OperationError::Cancelled;
228
229 assert!(err.to_string().contains("cancelled"));
230 }
231}