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("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 #[error("{0}")]
64 Backend(String, ErrorCategory),
65
66 #[error("{0}")]
67 Other(#[from] anyhow::Error),
68}
69
70#[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}