1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//! Exit codes for outcomes a script or agent needs to tell apart.
//!
//! `anyhow` collapses every error into exit 1, which is right for "the command
//! could not do what you asked". A deploy that is parked behind an approval
//! gate, or one the CLI stopped waiting on, is not that: the deploy itself is
//! fine. A wrapper that treats every non-zero as "the deploy failed" would
//! retry or roll back a deploy that is merely waiting.
//!
//! `main` downcasts the top-level error to `ExitWith` and uses its code.
use std::fmt;
/// Exit 2: the deploy is parked awaiting approval; nothing failed.
pub(crate) const EXIT_AWAITING_APPROVAL: i32 = 2;
/// Exit 3: the CLI stopped waiting before the deploy reached a terminal state.
pub(crate) const EXIT_STILL_RUNNING: i32 = 3;
#[derive(Debug)]
pub(crate) struct ExitWith {
pub(crate) code: i32,
pub(crate) message: String,
}
impl fmt::Display for ExitWith {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ExitWith {}
impl ExitWith {
pub(crate) fn new(code: i32, message: impl Into<String>) -> anyhow::Error {
anyhow::Error::new(ExitWith {
code,
message: message.into(),
})
}
}
/// The process exit code an error should produce: the `ExitWith` code when
/// the error carries one, else 1.
pub(crate) fn exit_code_for(error: &anyhow::Error) -> i32 {
error
.downcast_ref::<ExitWith>()
.map(|e| e.code)
.unwrap_or(1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_plain_error_exits_one() {
assert_eq!(exit_code_for(&anyhow::anyhow!("boom")), 1);
}
#[test]
fn an_exit_with_carries_its_code_through_context() {
let err = ExitWith::new(EXIT_AWAITING_APPROVAL, "parked");
assert_eq!(exit_code_for(&err), EXIT_AWAITING_APPROVAL);
// Wrapping with context must not hide the code; main sees the outer error.
let wrapped = err.context("while deploying");
assert_eq!(exit_code_for(&wrapped), EXIT_AWAITING_APPROVAL);
assert_eq!(wrapped.root_cause().to_string(), "parked");
}
}