1use thiserror::Error;
2
3pub 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;
10pub 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#[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}