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("no active organization\n  → run `hm org switch <slug>` or set HARMONT_ORG=<slug>")]
19    NoOrganization,
20
21    #[error("API error (HTTP {status}): {message}")]
22    Api { status: u16, message: String },
23
24    #[error("network error: {0}")]
25    Network(#[from] reqwest::Error),
26
27    #[error(
28        "source archive exceeds the {max_mb} MB limit\n  → trim the source tree or add ignores in .harmontignore"
29    )]
30    ArchiveTooLarge { max_mb: u64 },
31
32    #[error("pipeline not found: {slug}\n  → list available pipelines with `hm pipeline list`")]
33    PipelineNotFound { slug: String },
34
35    #[error(
36        "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"
37    )]
38    PipelineManualDisabled,
39
40    #[error("configuration error: {0}")]
41    Config(String),
42
43    #[error("docker error: {0}\n  → check that the Docker daemon is running (`docker version`)")]
44    Docker(String),
45
46    #[error("DSL engine error: {0}")]
47    DslEngine(String),
48
49    #[error("pipeline render error: {0}")]
50    PipelineRender(String),
51
52    #[error("local scheduler error: {0}")]
53    LocalScheduling(String),
54
55    #[error(
56        "step '{step_key}' requested runner '{runner}', but no runner provides it (available: {available:?})"
57    )]
58    UnknownRunner {
59        step_key: String,
60        runner: String,
61        available: Vec<String>,
62    },
63
64    #[error("no default step executor is registered")]
65    NoDefaultExecutor,
66
67    #[error("{0}")]
68    Other(#[from] anyhow::Error),
69}
70
71/// Coarse error category.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum ErrorCategory {
74    BuildFailed,
75    Usage,
76    Auth,
77    Network,
78    Api,
79    PipelineInvalid,
80}
81
82impl ErrorCategory {
83    #[must_use]
84    pub const fn exit_code(self) -> i32 {
85        match self {
86            Self::BuildFailed => EXIT_BUILD_FAILED,
87            Self::Usage => EXIT_USAGE,
88            Self::Auth => EXIT_AUTH,
89            Self::Network => EXIT_NETWORK,
90            Self::Api => EXIT_API,
91            Self::PipelineInvalid => EXIT_PIPELINE_INVALID,
92        }
93    }
94}
95
96impl HmError {
97    #[must_use]
98    pub const fn category(&self) -> ErrorCategory {
99        match self {
100            Self::NotAuthenticated => ErrorCategory::Auth,
101            Self::Api { status, .. } if *status == 401 || *status == 403 => ErrorCategory::Auth,
102            Self::NoOrganization
103            | Self::ArchiveTooLarge { .. }
104            | Self::Config(_)
105            | Self::DslEngine(_)
106            | Self::PipelineRender(_) => ErrorCategory::Usage,
107            Self::Api { .. }
108            | Self::PipelineNotFound { .. }
109            | Self::PipelineManualDisabled
110            | Self::LocalScheduling(_) => ErrorCategory::Api,
111            Self::Network(_) | Self::Docker(_) => ErrorCategory::Network,
112            Self::UnknownRunner { .. } | Self::NoDefaultExecutor => ErrorCategory::PipelineInvalid,
113            Self::Other(_) => ErrorCategory::BuildFailed,
114        }
115    }
116
117    #[must_use]
118    pub const fn exit_code(&self) -> i32 {
119        self.category().exit_code()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::{ErrorCategory, HmError};
126
127    #[test]
128    fn pipeline_manual_disabled_renders_section5_shape() {
129        let s = format!("{}", HmError::PipelineManualDisabled);
130        assert_eq!(
131            s,
132            "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"
133        );
134    }
135
136    #[test]
137    fn pipeline_manual_disabled_is_api_category() {
138        assert_eq!(
139            HmError::PipelineManualDisabled.category(),
140            ErrorCategory::Api
141        );
142        assert_eq!(HmError::PipelineManualDisabled.exit_code(), super::EXIT_API);
143    }
144}