Skip to main content

cuenv_release/
error.rs

1//! Error types for release management operations.
2
3use miette::Diagnostic;
4use std::path::PathBuf;
5use thiserror::Error;
6
7/// Result type alias for release operations.
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Errors that can occur during release management operations.
11#[derive(Error, Debug, Diagnostic)]
12pub enum Error {
13    /// Failed to read or write a changeset file.
14    #[error("Changeset I/O error: {message}")]
15    #[diagnostic(
16        code(cuenv::release::changeset_io),
17        help("Check that the .cuenv/changesets directory exists and is writable")
18    )]
19    ChangesetIo {
20        /// The error message
21        message: String,
22        /// The path that caused the error
23        path: Option<PathBuf>,
24        /// The underlying source error
25        #[source]
26        source: Option<std::io::Error>,
27    },
28
29    /// Failed to parse a changeset file.
30    #[error("Invalid changeset format: {message}")]
31    #[diagnostic(
32        code(cuenv::release::changeset_parse),
33        help("Ensure the changeset file is valid Markdown with proper frontmatter")
34    )]
35    ChangesetParse {
36        /// The error message
37        message: String,
38        /// The path to the invalid file
39        path: Option<PathBuf>,
40    },
41
42    /// Failed to parse or validate a version string.
43    #[error("Invalid version: {version}")]
44    #[diagnostic(
45        code(cuenv::release::invalid_version),
46        help("Version must follow semantic versioning (e.g., 1.0.0, 2.1.0-beta.1)")
47    )]
48    InvalidVersion {
49        /// The invalid version string
50        version: String,
51    },
52
53    /// Package not found in the workspace.
54    #[error("Package not found: {name}")]
55    #[diagnostic(
56        code(cuenv::release::package_not_found),
57        help("Ensure the package exists in the workspace and is properly configured")
58    )]
59    PackageNotFound {
60        /// The package name that wasn't found
61        name: String,
62    },
63
64    /// No changesets found for release.
65    #[error("No changesets found")]
66    #[diagnostic(
67        code(cuenv::release::no_changesets),
68        help("Create changesets with 'cuenv changeset add' before running release version")
69    )]
70    NoChangesets,
71
72    /// Configuration error.
73    #[error("Release configuration error: {message}")]
74    #[diagnostic(code(cuenv::release::config), help("{help}"))]
75    Config {
76        /// The error message
77        message: String,
78        /// Help text for the user
79        help: String,
80    },
81
82    /// Manifest file error (Cargo.toml, package.json, etc.).
83    #[error("Manifest error: {message}")]
84    #[diagnostic(
85        code(cuenv::release::manifest),
86        help("Check that the manifest file exists and is properly formatted")
87    )]
88    Manifest {
89        /// The error message
90        message: String,
91        /// The manifest file path
92        path: Option<PathBuf>,
93    },
94
95    /// Git operation error.
96    #[error("Git error: {message}")]
97    #[diagnostic(
98        code(cuenv::release::git),
99        help("Ensure you are in a git repository and have the necessary permissions")
100    )]
101    Git {
102        /// The error message
103        message: String,
104    },
105
106    /// Publish error.
107    #[error("Publish failed: {message}")]
108    #[diagnostic(code(cuenv::release::publish))]
109    Publish {
110        /// The error message
111        message: String,
112        /// The package that failed to publish
113        package: Option<String>,
114    },
115
116    /// Artifact packaging error.
117    #[error("Artifact error: {message}")]
118    #[diagnostic(
119        code(cuenv::release::artifact),
120        help("Check that the binary exists and is readable")
121    )]
122    Artifact {
123        /// The error message
124        message: String,
125        /// The path that caused the error
126        path: Option<PathBuf>,
127    },
128
129    /// Backend error (GitHub, Homebrew, etc.).
130    #[error("{backend} backend error: {message}")]
131    #[diagnostic(code(cuenv::release::backend))]
132    Backend {
133        /// The backend that failed
134        backend: String,
135        /// The error message
136        message: String,
137        /// Help text for the user
138        help: Option<String>,
139    },
140
141    /// Wrapped I/O error.
142    #[error("I/O error: {0}")]
143    #[diagnostic(code(cuenv::release::io))]
144    Io(#[from] std::io::Error),
145
146    /// Wrapped JSON error.
147    #[error("JSON error: {0}")]
148    #[diagnostic(code(cuenv::release::json))]
149    Json(#[from] serde_json::Error),
150
151    /// Wrapped TOML parsing error.
152    #[error("TOML parse error: {0}")]
153    #[diagnostic(code(cuenv::release::toml_parse))]
154    TomlParse(#[from] toml::de::Error),
155
156    /// Wrapped TOML serialization error.
157    #[error("TOML serialization error: {0}")]
158    #[diagnostic(code(cuenv::release::toml_ser))]
159    TomlSer(#[from] toml::ser::Error),
160}
161
162impl Error {
163    /// Create a new changeset I/O error.
164    #[must_use]
165    pub fn changeset_io(message: impl Into<String>, path: Option<PathBuf>) -> Self {
166        Self::ChangesetIo {
167            message: message.into(),
168            path,
169            source: None,
170        }
171    }
172
173    /// Create a new changeset I/O error with source.
174    #[must_use]
175    pub fn changeset_io_with_source(
176        message: impl Into<String>,
177        path: Option<PathBuf>,
178        source: std::io::Error,
179    ) -> Self {
180        Self::ChangesetIo {
181            message: message.into(),
182            path,
183            source: Some(source),
184        }
185    }
186
187    /// Create a new changeset parse error.
188    #[must_use]
189    pub fn changeset_parse(message: impl Into<String>, path: Option<PathBuf>) -> Self {
190        Self::ChangesetParse {
191            message: message.into(),
192            path,
193        }
194    }
195
196    /// Create a new invalid version error.
197    #[must_use]
198    pub fn invalid_version(version: impl Into<String>) -> Self {
199        Self::InvalidVersion {
200            version: version.into(),
201        }
202    }
203
204    /// Create a new package not found error.
205    #[must_use]
206    pub fn package_not_found(name: impl Into<String>) -> Self {
207        Self::PackageNotFound { name: name.into() }
208    }
209
210    /// Create a new configuration error.
211    #[must_use]
212    pub fn config(message: impl Into<String>, help: impl Into<String>) -> Self {
213        Self::Config {
214            message: message.into(),
215            help: help.into(),
216        }
217    }
218
219    /// Create a new manifest error.
220    #[must_use]
221    pub fn manifest(message: impl Into<String>, path: Option<PathBuf>) -> Self {
222        Self::Manifest {
223            message: message.into(),
224            path,
225        }
226    }
227
228    /// Create a new git error.
229    #[must_use]
230    pub fn git(message: impl Into<String>) -> Self {
231        Self::Git {
232            message: message.into(),
233        }
234    }
235
236    /// Create a new publish error.
237    #[must_use]
238    pub fn publish(message: impl Into<String>, package: Option<String>) -> Self {
239        Self::Publish {
240            message: message.into(),
241            package,
242        }
243    }
244
245    /// Create a new artifact error.
246    #[must_use]
247    pub fn artifact(message: impl Into<String>, path: Option<PathBuf>) -> Self {
248        Self::Artifact {
249            message: message.into(),
250            path,
251        }
252    }
253
254    /// Create a new backend error.
255    #[must_use]
256    pub fn backend(
257        backend: impl Into<String>,
258        message: impl Into<String>,
259        help: Option<String>,
260    ) -> Self {
261        Self::Backend {
262            backend: backend.into(),
263            message: message.into(),
264            help,
265        }
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_changeset_io_error() {
275        let err = Error::changeset_io("failed to write", Some(PathBuf::from(".cuenv/test.md")));
276        assert!(err.to_string().contains("Changeset I/O error"));
277    }
278
279    #[test]
280    fn test_changeset_io_error_no_path() {
281        let err = Error::changeset_io("failed to write", None);
282        assert!(err.to_string().contains("Changeset I/O error"));
283    }
284
285    #[test]
286    fn test_changeset_io_with_source() {
287        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
288        let err = Error::changeset_io_with_source(
289            "failed to read",
290            Some(PathBuf::from("test.md")),
291            io_err,
292        );
293        assert!(err.to_string().contains("Changeset I/O error"));
294    }
295
296    #[test]
297    fn test_changeset_parse_error() {
298        let err = Error::changeset_parse("invalid frontmatter", None);
299        assert!(err.to_string().contains("Invalid changeset format"));
300    }
301
302    #[test]
303    fn test_changeset_parse_error_with_path() {
304        let err = Error::changeset_parse("bad yaml", Some(PathBuf::from("changes.md")));
305        assert!(err.to_string().contains("Invalid changeset format"));
306    }
307
308    #[test]
309    fn test_invalid_version_error() {
310        let err = Error::invalid_version("not-a-version");
311        assert!(err.to_string().contains("not-a-version"));
312    }
313
314    #[test]
315    fn test_package_not_found_error() {
316        let err = Error::package_not_found("missing-pkg");
317        assert!(err.to_string().contains("missing-pkg"));
318    }
319
320    #[test]
321    fn test_config_error() {
322        let err = Error::config("bad config", "check your settings");
323        assert!(err.to_string().contains("bad config"));
324    }
325
326    #[test]
327    fn test_manifest_error() {
328        let err = Error::manifest("invalid toml", Some(PathBuf::from("Cargo.toml")));
329        assert!(err.to_string().contains("Manifest error"));
330    }
331
332    #[test]
333    fn test_manifest_error_no_path() {
334        let err = Error::manifest("missing field", None);
335        assert!(err.to_string().contains("Manifest error"));
336    }
337
338    #[test]
339    fn test_git_error() {
340        let err = Error::git("not a repository");
341        assert!(err.to_string().contains("Git error"));
342    }
343
344    #[test]
345    fn test_publish_error() {
346        let err = Error::publish("auth failed", Some("my-pkg".to_string()));
347        assert!(err.to_string().contains("Publish failed"));
348    }
349
350    #[test]
351    fn test_publish_error_no_package() {
352        let err = Error::publish("network error", None);
353        assert!(err.to_string().contains("Publish failed"));
354    }
355
356    #[test]
357    fn test_no_changesets_error() {
358        let err = Error::NoChangesets;
359        assert!(err.to_string().contains("No changesets found"));
360    }
361
362    #[test]
363    fn test_artifact_error() {
364        let err = Error::artifact(
365            "binary not found",
366            Some(PathBuf::from("target/release/bin")),
367        );
368        assert!(err.to_string().contains("Artifact error"));
369    }
370
371    #[test]
372    fn test_artifact_error_no_path() {
373        let err = Error::artifact("compression failed", None);
374        assert!(err.to_string().contains("Artifact error"));
375    }
376
377    #[test]
378    fn test_backend_error() {
379        let err = Error::backend("GitHub", "rate limited", Some("wait 1 hour".to_string()));
380        assert!(err.to_string().contains("GitHub"));
381        assert!(err.to_string().contains("rate limited"));
382    }
383
384    #[test]
385    fn test_backend_error_no_help() {
386        let err = Error::backend("Homebrew", "push failed", None);
387        assert!(err.to_string().contains("Homebrew"));
388    }
389
390    #[test]
391    fn test_from_io_error() {
392        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
393        let err: Error = io_err.into();
394        assert!(err.to_string().contains("I/O error"));
395    }
396
397    #[test]
398    fn test_error_debug() {
399        let err = Error::NoChangesets;
400        let debug = format!("{err:?}");
401        assert!(debug.contains("NoChangesets"));
402    }
403}