Skip to main content

harmont_cli/
error.rs

1use thiserror::Error;
2
3/// Exit codes for the CLI.
4pub const EXIT_SUCCESS: i32 = 0;
5pub const EXIT_BUILD_FAILED: i32 = 1;
6pub const EXIT_USAGE: i32 = 2;
7pub const EXIT_AUTH: i32 = 3;
8pub const EXIT_NETWORK: i32 = 4;
9pub const EXIT_API: i32 = 5;
10/// Pipeline-level invalid configuration (unknown runner, no default executor).
11pub const EXIT_PIPELINE_INVALID: i32 = 7;
12
13#[derive(Debug, Error)]
14pub enum HmError {
15    #[error("not authenticated\n  → run `hm login`")]
16    NotAuthenticated,
17
18    #[error("API error (HTTP {status}): {message}")]
19    Api { status: u16, message: String },
20
21    #[error("network error: {0}")]
22    Network(#[from] reqwest::Error),
23
24    #[error("pipeline not found: {slug}\n  → list available pipelines with `hm pipeline list`")]
25    PipelineNotFound { slug: String },
26
27    #[error(
28        "error: manual builds are disabled for this pipeline\n  \u{2192} ask the pipeline owner to set allow_manual=True\n\nhm run --help   for more"
29    )]
30    PipelineManualDisabled,
31
32    #[error("configuration error: {0}")]
33    Config(String),
34
35    #[error("docker error: {0}\n  → check that the Docker daemon is running (`docker version`)")]
36    Docker(String),
37
38    #[error("DSL engine error: {0}")]
39    DslEngine(String),
40
41    #[error("pipeline render error: {0}")]
42    PipelineRender(String),
43
44    #[error("local scheduler error: {0}")]
45    LocalScheduling(String),
46
47    #[error(
48        "step '{step_key}' requested runner '{runner}', but no runner provides it (available: {available:?})"
49    )]
50    UnknownRunner {
51        step_key: String,
52        runner: String,
53        available: Vec<String>,
54    },
55
56    #[error("no default step executor is registered")]
57    NoDefaultExecutor,
58
59    /// A failure surfaced across the `hm_exec::ExecutionBackend` boundary,
60    /// already rendered in the project's error doctrine. Carries its own
61    /// [`ErrorCategory`] so the process exit code is preserved (the doctrine
62    /// message lives in `0`; the category drives `exit_code()`).
63    #[error("{0}")]
64    Backend(String, ErrorCategory),
65
66    #[error("{0}")]
67    Other(#[from] anyhow::Error),
68}
69
70/// Coarse error category.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum ErrorCategory {
73    BuildFailed,
74    Usage,
75    Auth,
76    Network,
77    Api,
78    PipelineInvalid,
79}
80
81impl ErrorCategory {
82    #[must_use]
83    pub const fn exit_code(self) -> i32 {
84        match self {
85            Self::BuildFailed => EXIT_BUILD_FAILED,
86            Self::Usage => EXIT_USAGE,
87            Self::Auth => EXIT_AUTH,
88            Self::Network => EXIT_NETWORK,
89            Self::Api => EXIT_API,
90            Self::PipelineInvalid => EXIT_PIPELINE_INVALID,
91        }
92    }
93}
94
95impl HmError {
96    #[must_use]
97    pub const fn category(&self) -> ErrorCategory {
98        match self {
99            Self::NotAuthenticated => ErrorCategory::Auth,
100            Self::Api { status, .. } if *status == 401 || *status == 403 => ErrorCategory::Auth,
101            Self::Config(_) | Self::DslEngine(_) | Self::PipelineRender(_) => ErrorCategory::Usage,
102            Self::Api { .. }
103            | Self::PipelineNotFound { .. }
104            | Self::PipelineManualDisabled
105            | Self::LocalScheduling(_) => ErrorCategory::Api,
106            Self::Network(_) | Self::Docker(_) => ErrorCategory::Network,
107            Self::UnknownRunner { .. } | Self::NoDefaultExecutor => ErrorCategory::PipelineInvalid,
108            Self::Backend(_, category) => *category,
109            Self::Other(_) => ErrorCategory::BuildFailed,
110        }
111    }
112
113    #[must_use]
114    pub const fn exit_code(&self) -> i32 {
115        self.category().exit_code()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::{ErrorCategory, HmError};
122
123    #[test]
124    fn pipeline_manual_disabled_renders_section5_shape() {
125        let s = format!("{}", HmError::PipelineManualDisabled);
126        assert_eq!(
127            s,
128            "error: manual builds are disabled for this pipeline\n  \u{2192} ask the pipeline owner to set allow_manual=True\n\nhm run --help   for more"
129        );
130    }
131
132    #[test]
133    fn pipeline_manual_disabled_is_api_category() {
134        assert_eq!(
135            HmError::PipelineManualDisabled.category(),
136            ErrorCategory::Api
137        );
138        assert_eq!(HmError::PipelineManualDisabled.exit_code(), super::EXIT_API);
139    }
140}