feat(error): add structured exit codes for scriptable error handling (#428)
* feat(error): add structured exit codes for scriptable error handling
Replace the hardcoded `std::process::exit(1)` with a type-specific exit
code derived from the GwsError variant:
0 — success
1 — API error (GwsError::Api)
2 — auth error (GwsError::Auth)
3 — validation (GwsError::Validation)
4 — discovery (GwsError::Discovery)
5 — internal (GwsError::Other)
This allows shell scripts to branch on failure type without parsing the
JSON error output:
gws drive files list ...
case $? in
1) echo "API error — check your params" ;;
2) echo "Auth error — run: gws auth login" ;;
3) echo "Bad arguments" ;;
esac
Changes:
- Add GwsError::exit_code() mapping variants to codes
- Update main() to call std::process::exit(err.exit_code())
- Document exit codes in gws --help (print_usage)
- Document exit codes in README under new Exit Codes section
- Add 6 unit tests including a regression guard asserting all codes are distinct
* refactor(error): replace magic exit-code numbers with named constants
Add EXIT_CODE_API/AUTH/VALIDATION/DISCOVERY/OTHER associated constants
on GwsError so callers and tests reference symbolic names rather than
bare integers. Update exit_code() match arms and all tests accordingly.
The distinctness test now validates the constants array directly.
Addresses code-review feedback requesting named constants.
* refactor(error): centralize exit code help text via EXIT_CODE_DOCUMENTATION
Add a module-level EXIT_CODE_DOCUMENTATION constant — a static slice of
(code, description) pairs built from the EXIT_CODE_* constants. Replace
the hardcoded println! block in print_usage() with a loop over this slice
so the help output is always in sync with the defined constants and cannot
drift out of date.
Addresses code-review feedback requesting a single source of truth.
This commit is contained in:
committed by
GitHub
parent
087066f946
commit
247e27a876
@@ -0,0 +1,18 @@
|
||||
---
|
||||
"@googleworkspace/cli": minor
|
||||
---
|
||||
|
||||
Add structured exit codes for scriptable error handling
|
||||
|
||||
`gws` now exits with a type-specific code instead of always using `1`:
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `0` | Success |
|
||||
| `1` | API error — Google returned a 4xx/5xx response |
|
||||
| `2` | Auth error — credentials missing, expired, or invalid |
|
||||
| `3` | Validation error — bad arguments, unknown service, invalid flag |
|
||||
| `4` | Discovery error — could not fetch the API schema document |
|
||||
| `5` | Internal error — unexpected failure |
|
||||
|
||||
Exit codes are documented in `gws --help` and in the README.
|
||||
@@ -33,6 +33,7 @@ npm install -g @googleworkspace/cli
|
||||
- [AI Agent Skills](#ai-agent-skills)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Exit Codes](#exit-codes)
|
||||
- [Architecture](#architecture)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Development](#development)
|
||||
@@ -317,6 +318,27 @@ All variables are optional. See [`.env.example`](.env.example) for a copy-paste
|
||||
|
||||
Environment variables can also be set in a `.env` file (loaded via [dotenvy](https://crates.io/crates/dotenvy)).
|
||||
|
||||
## Exit Codes
|
||||
|
||||
`gws` uses structured exit codes so scripts can branch on the failure type without parsing error output.
|
||||
|
||||
| Code | Meaning | Example cause |
|
||||
|------|---------|---------------|
|
||||
| `0` | Success | Command completed normally |
|
||||
| `1` | API error | Google returned a 4xx/5xx response |
|
||||
| `2` | Auth error | Credentials missing, expired, or invalid |
|
||||
| `3` | Validation error | Bad arguments, unknown service, invalid flag |
|
||||
| `4` | Discovery error | Could not fetch the API schema document |
|
||||
| `5` | Internal error | Unexpected failure |
|
||||
|
||||
```bash
|
||||
gws drive files list --params '{"fileId": "bad"}'
|
||||
echo $? # 1 — API error
|
||||
|
||||
gws unknown-service files list
|
||||
echo $? # 3 — validation error (unknown service)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
`gws` uses a **two-phase parsing** strategy:
|
||||
|
||||
+101
@@ -39,7 +39,51 @@ pub enum GwsError {
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Human-readable exit code table, keyed by (code, description).
|
||||
///
|
||||
/// Used by `print_usage()` so the help text stays in sync with the
|
||||
/// constants defined below without requiring manual updates in two places.
|
||||
pub const EXIT_CODE_DOCUMENTATION: &[(i32, &str)] = &[
|
||||
(0, "Success"),
|
||||
(GwsError::EXIT_CODE_API, "API error — Google returned an error response"),
|
||||
(GwsError::EXIT_CODE_AUTH, "Auth error — credentials missing or invalid"),
|
||||
(GwsError::EXIT_CODE_VALIDATION, "Validation — bad arguments or input"),
|
||||
(GwsError::EXIT_CODE_DISCOVERY, "Discovery — could not fetch API schema"),
|
||||
(GwsError::EXIT_CODE_OTHER, "Internal — unexpected failure"),
|
||||
];
|
||||
|
||||
impl GwsError {
|
||||
/// Exit code for [`GwsError::Api`] variants.
|
||||
pub const EXIT_CODE_API: i32 = 1;
|
||||
/// Exit code for [`GwsError::Auth`] variants.
|
||||
pub const EXIT_CODE_AUTH: i32 = 2;
|
||||
/// Exit code for [`GwsError::Validation`] variants.
|
||||
pub const EXIT_CODE_VALIDATION: i32 = 3;
|
||||
/// Exit code for [`GwsError::Discovery`] variants.
|
||||
pub const EXIT_CODE_DISCOVERY: i32 = 4;
|
||||
/// Exit code for [`GwsError::Other`] variants.
|
||||
pub const EXIT_CODE_OTHER: i32 = 5;
|
||||
|
||||
/// Map each error variant to a stable, documented exit code.
|
||||
///
|
||||
/// | Code | Meaning |
|
||||
/// |------|----------------------------------------------|
|
||||
/// | 0 | Success (never returned here) |
|
||||
/// | 1 | API error — Google returned an error response |
|
||||
/// | 2 | Auth error — credentials missing or invalid |
|
||||
/// | 3 | Validation error — bad arguments or input |
|
||||
/// | 4 | Discovery error — could not fetch API schema |
|
||||
/// | 5 | Internal error — unexpected failure |
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
GwsError::Api { .. } => Self::EXIT_CODE_API,
|
||||
GwsError::Auth(_) => Self::EXIT_CODE_AUTH,
|
||||
GwsError::Validation(_) => Self::EXIT_CODE_VALIDATION,
|
||||
GwsError::Discovery(_) => Self::EXIT_CODE_DISCOVERY,
|
||||
GwsError::Other(_) => Self::EXIT_CODE_OTHER,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
match self {
|
||||
GwsError::Api {
|
||||
@@ -126,6 +170,63 @@ pub fn print_error_json(err: &GwsError) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_exit_code_api() {
|
||||
let err = GwsError::Api {
|
||||
code: 404,
|
||||
message: "Not Found".to_string(),
|
||||
reason: "notFound".to_string(),
|
||||
enable_url: None,
|
||||
};
|
||||
assert_eq!(err.exit_code(), GwsError::EXIT_CODE_API);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit_code_auth() {
|
||||
assert_eq!(
|
||||
GwsError::Auth("bad token".to_string()).exit_code(),
|
||||
GwsError::EXIT_CODE_AUTH
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit_code_validation() {
|
||||
assert_eq!(
|
||||
GwsError::Validation("missing arg".to_string()).exit_code(),
|
||||
GwsError::EXIT_CODE_VALIDATION
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit_code_discovery() {
|
||||
assert_eq!(
|
||||
GwsError::Discovery("fetch failed".to_string()).exit_code(),
|
||||
GwsError::EXIT_CODE_DISCOVERY
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit_code_other() {
|
||||
assert_eq!(
|
||||
GwsError::Other(anyhow::anyhow!("oops")).exit_code(),
|
||||
GwsError::EXIT_CODE_OTHER
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit_codes_are_distinct() {
|
||||
// Ensure all named constants are unique (regression guard).
|
||||
let codes = [
|
||||
GwsError::EXIT_CODE_API,
|
||||
GwsError::EXIT_CODE_AUTH,
|
||||
GwsError::EXIT_CODE_VALIDATION,
|
||||
GwsError::EXIT_CODE_DISCOVERY,
|
||||
GwsError::EXIT_CODE_OTHER,
|
||||
];
|
||||
let unique: std::collections::HashSet<i32> = codes.iter().copied().collect();
|
||||
assert_eq!(unique.len(), codes.len(), "exit codes must be distinct: {codes:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_to_json_api() {
|
||||
let err = GwsError::Api {
|
||||
|
||||
+6
-1
@@ -49,7 +49,7 @@ async fn main() {
|
||||
|
||||
if let Err(err) = run().await {
|
||||
print_error_json(&err);
|
||||
std::process::exit(1);
|
||||
std::process::exit(err.exit_code());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +460,11 @@ fn print_usage() {
|
||||
" GOOGLE_WORKSPACE_PROJECT_ID Override the GCP project ID for quota and billing"
|
||||
);
|
||||
println!();
|
||||
println!("EXIT CODES:");
|
||||
for (code, description) in crate::error::EXIT_CODE_DOCUMENTATION {
|
||||
println!(" {:<5}{}", code, description);
|
||||
}
|
||||
println!();
|
||||
println!("COMMUNITY:");
|
||||
println!(" Star the repo: https://github.com/googleworkspace/cli");
|
||||
println!(" Report bugs / request features: https://github.com/googleworkspace/cli/issues");
|
||||
|
||||
Reference in New Issue
Block a user