feat(cli): give failures a taxonomy exit code instead of a blanket 1#1058
Open
joshrotenberg wants to merge 1 commit into
Open
feat(cli): give failures a taxonomy exit code instead of a blanket 1#1058joshrotenberg wants to merge 1 commit into
joshrotenberg wants to merge 1 commit into
Conversation
Every runtime failure exited 1, so a script could tell that redisctl failed but not why, and had to match on message text to branch. Adds an error::exit_code module with the taxonomy from #1045 (usage, config, auth, not-found, validation, conflict, upstream, rate-limited, network, timeout, cancelled) and a RedisCtlError::exit_code() that maps each variant to one, exhaustively, so a new variant has to declare its code. main exits with it instead of 1, and the two pre-dispatch exits in the prefix-inference layer use CONFIG and USAGE. USAGE is 2 to match clap, which already exits 2 when argument parsing fails. GENERIC stays 1 for Other and OutputError, which are not classified yet. ApiError is the only variant carrying a server status, so it recovers 404/409/429 from the message the way suggestions() already does for 404. $ redisctl enterprise database list; echo $? 3 The -o json/yaml envelope now carries exit_code alongside code, the field #1048 deferred to this change. Part of #1045 (GAP-AUTOMATION-02).
joshrotenberg
marked this pull request as ready for review
July 26, 2026 19:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1045, item 3 (GAP-AUTOMATION-02).
Problem
Every runtime failure exited 1, from a blanket
process::exit(1). A script could tell thatredisctlfailed but not why, so branching on failure type meant matching on message text.Change
crates/redisctl/src/error.rsgains anexit_codemodule holding the taxonomy the issue names, andRedisCtlError::exit_code()mapping each variant to one:GENERICOther,OutputErrorUSAGEUnsupportedDeploymentType,FileErrorCONFIGConfiguration,ProfileNotFound,ProfileTypeMismatch,NoProfileConfigured,MissingCredentialsAUTHAuthenticationFailedNOT_FOUNDApiError(404)VALIDATIONInvalidInputCONFLICTApiError(409)UPSTREAMApiError(other)RATE_LIMITEDApiError(429)NETWORKConnectionErrorTIMEOUTTimeoutCANCELLEDCancelledThe match is exhaustive, so a new variant has to declare its code, the same way
code()already works.main.rsexits withe.exit_code()rather than 1. The two pre-dispatch exits in the prefix-inference layer do not have aRedisCtlErrorin hand, so they name their code directly: theConfigErrorarms exitCONFIG, and the "cannot determine platform" arm exitsUSAGE.The
-o json/-o yamlenvelope now carriesexit_codealongsidecode. That is the field #1048 explicitly deferred to this change.Choices worth flagging
USAGEis 2 because clap already exits 2 when argument parsing fails. Both mean the invocation was wrong, so a script sees one code for that class rather than two.GENERICstays 1, so anything not yet classified keeps its current behavior.Otheris the anyhow catch-all andOutputErrorcovers serialization and IO.ApiErrorsniffs the status out of its message. It is the only variant that carries a server status, so without this, not-found, conflict and rate-limited would be unreachable codes.suggestions()already does the same for 404. The real fix is a status field on the variant, which belongs with theConfig/Configurationmerge in item 2 of Automation contract: tracing corrupts -o json, duplicate error line, exit codes, error envelope #1045.Gap found while testing
--profile <nonexistent>still exits 1, not 3. It never reachesRedisCtlError::ProfileNotFound:connection.rs:174andconnection.rs:389report it throughanyhow::Context::with_context, which lands inOther. This is the "354 error paths collapse into a generic variant" problem described in item 2 of #1045, and fixing it means changingconnection.rs, which is outside this change. Worth doing next, since a nonexistent profile is the most common config failure there is.Behavior
Exit codes only. No message, format or control-flow changes. Success still exits 0.
Checks
cargo fmt --all -- --check,cargo clippy --all-targets -- -D warningsandcargo testall pass locally (full suite green).cargo doc --no-deps --all-featuresproduces only the pre-existing warnings incli/enterprise.rsandredisctl-mcp, unchanged by this diff.Tests added: four unit tests in
error.rscovering the per-variant mapping, theApiErrorstatus split, the envelope carrying the code, and an assertion that no classified variant falls back to 1. The existing-o jsonenvelope test now also asserts the process exit code matches the envelope, plus one integration test that a config failure exits 3.