Apply stable CLI failure exit codes
This commit is contained in:
parent
50d298ead7
commit
b808dad0e5
8 changed files with 224 additions and 7 deletions
|
|
@ -1676,12 +1676,16 @@ fn admin_suspend_tenant_report(args: AdminSuspendTenantArgs) -> Result<Value> {
|
|||
}
|
||||
|
||||
fn emit_report<T: Serialize>(report: &T, json_output: bool) -> Result<()> {
|
||||
let value = serde_json::to_value(report)?;
|
||||
let mut value = serde_json::to_value(report)?;
|
||||
let exit_code = apply_command_report_exit_code(&mut value);
|
||||
if json_output {
|
||||
println!("{}", serde_json::to_string_pretty(&value)?);
|
||||
} else {
|
||||
println!("{}", human_report(&value));
|
||||
}
|
||||
if let Some(exit_code) = exit_code {
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -2046,6 +2050,33 @@ fn compact_json(value: &Value) -> String {
|
|||
serde_json::to_string(value).unwrap_or_else(|_| "<unprintable>".to_owned())
|
||||
}
|
||||
|
||||
fn apply_command_report_exit_code(value: &mut Value) -> Option<i32> {
|
||||
for pointer in [
|
||||
"/machine_error",
|
||||
"/run_start/machine_error",
|
||||
"/restart_request/machine_error",
|
||||
"/cancel_request/machine_error",
|
||||
"/task_restart/machine_error",
|
||||
] {
|
||||
let Some(machine_error) = value.pointer_mut(pointer) else {
|
||||
continue;
|
||||
};
|
||||
let Some(exit_code) = machine_error
|
||||
.get("stable_exit_code")
|
||||
.and_then(Value::as_i64)
|
||||
.and_then(|code| i32::try_from(code).ok())
|
||||
.filter(|code| *code != 0)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(object) = machine_error.as_object_mut() {
|
||||
object.insert("process_exit_code_applied".to_owned(), json!(true));
|
||||
}
|
||||
return Some(exit_code);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn cli_error_summary(message: &str) -> Value {
|
||||
let category = classify_cli_error_message(message);
|
||||
cli_error_summary_for_category(category, message)
|
||||
|
|
@ -2230,7 +2261,25 @@ fn cli_error_next_actions(category: &str) -> Vec<&'static str> {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
fn main() {
|
||||
if let Err(error) = run_cli() {
|
||||
let message = error.to_string();
|
||||
let machine_error = cli_error_summary(&message);
|
||||
let category = machine_error
|
||||
.get("category")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown");
|
||||
let exit_code = machine_error
|
||||
.get("stable_exit_code")
|
||||
.and_then(Value::as_i64)
|
||||
.and_then(|code| i32::try_from(code).ok())
|
||||
.unwrap_or(1);
|
||||
eprintln!("Error ({category}, exit {exit_code}): {error:#}");
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_cli() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Commands::Doctor(args) => {
|
||||
|
|
@ -2912,15 +2961,25 @@ fn artifact_name_from_path(path: &str) -> String {
|
|||
path.rsplit('/').next().unwrap_or(path).to_owned()
|
||||
}
|
||||
|
||||
fn response_error_message(response: &Value, fallback: &str) -> String {
|
||||
response
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| fallback.to_owned())
|
||||
}
|
||||
|
||||
fn process_restart_request_summary(response: &Value, requires_confirmation: bool) -> Value {
|
||||
if response.get("type").and_then(Value::as_str) != Some("process_started") {
|
||||
let message = response_error_message(response, "coordinator rejected process restart");
|
||||
return json!({
|
||||
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
||||
"operation": "restart_virtual_process",
|
||||
"accepted": false,
|
||||
"requires_confirmation": requires_confirmation,
|
||||
"explicit_user_action": true,
|
||||
"error": response.get("message").cloned().unwrap_or(Value::Null),
|
||||
"error": message,
|
||||
"machine_error": cli_error_summary(&message),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2939,6 +2998,7 @@ fn process_restart_request_summary(response: &Value, requires_confirmation: bool
|
|||
|
||||
fn process_cancel_request_summary(response: &Value, requires_confirmation: bool) -> Value {
|
||||
if response.get("type").and_then(Value::as_str) != Some("process_cancellation_requested") {
|
||||
let message = response_error_message(response, "coordinator rejected process cancel");
|
||||
return json!({
|
||||
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
||||
"operation": "cancel_virtual_process",
|
||||
|
|
@ -2946,7 +3006,8 @@ fn process_cancel_request_summary(response: &Value, requires_confirmation: bool)
|
|||
"requires_confirmation": requires_confirmation,
|
||||
"explicit_user_action": true,
|
||||
"whole_process_cancel_available": true,
|
||||
"error": response.get("message").cloned().unwrap_or(Value::Null),
|
||||
"error": message,
|
||||
"machine_error": cli_error_summary(&message),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2975,6 +3036,7 @@ fn process_cancel_request_summary(response: &Value, requires_confirmation: bool)
|
|||
|
||||
fn task_restart_request_summary(response: &Value, requires_confirmation: bool) -> Value {
|
||||
if response.get("type").and_then(Value::as_str) != Some("task_restart") {
|
||||
let message = response_error_message(response, "coordinator rejected task restart");
|
||||
return json!({
|
||||
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
|
||||
"operation": "restart_selected_task",
|
||||
|
|
@ -2982,7 +3044,8 @@ fn task_restart_request_summary(response: &Value, requires_confirmation: bool) -
|
|||
"requires_confirmation": requires_confirmation,
|
||||
"explicit_user_action": true,
|
||||
"clean_boundary_required": true,
|
||||
"error": response.get("message").cloned().unwrap_or(Value::Null),
|
||||
"error": message,
|
||||
"machine_error": cli_error_summary(&message),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -4415,6 +4478,39 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_report_exit_code_marks_command_failures_only() {
|
||||
let run_start = run_start_summary(&json!({
|
||||
"type": "error",
|
||||
"message": "quota unavailable: resource limit exceeded for api_calls",
|
||||
}));
|
||||
let mut report = json!({
|
||||
"command": "run",
|
||||
"status": run_start["status"].clone(),
|
||||
"run_start": run_start,
|
||||
});
|
||||
|
||||
assert_eq!(apply_command_report_exit_code(&mut report), Some(22));
|
||||
assert_eq!(
|
||||
report["run_start"]["machine_error"]["process_exit_code_applied"],
|
||||
true
|
||||
);
|
||||
|
||||
let mut task_list = json!({
|
||||
"command": "task list",
|
||||
"tasks": [{
|
||||
"task": "compile",
|
||||
"state": "failed",
|
||||
"machine_error": cli_error_summary_for_category("program", "task exited with status 1"),
|
||||
}],
|
||||
});
|
||||
assert_eq!(apply_command_report_exit_code(&mut task_list), None);
|
||||
assert_eq!(
|
||||
task_list["tasks"][0]["machine_error"]["process_exit_code_applied"],
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_version_is_available() {
|
||||
let command = Cli::command();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue