fix(cli): partial failures set the exit code and the headline (CLI-6) - #747
Draft
padak wants to merge 1 commit into
Draft
fix(cli): partial failures set the exit code and the headline (CLI-6)#747padak wants to merge 1 commit into
padak wants to merge 1 commit into
Conversation
Commands that survive a per-item failure collected it into the result and then printed a fixed green "Success:" line and exited 0 anyway. A `sync clone` where every config failed reported `Success ... 0 created` and exit 0; a `sync push` where half the configs failed reported success too. The failures showed only as warnings underneath, so nothing in the exit code distinguished a clean run from a total failure and no script could branch on it. Add `exit_on_item_failures()` to the command helpers and apply it to the commands the audit found still missing it. The exit code follows the convention the bulk storage commands already used (any per-item failure is a general error, exit 1): - `sync push` (single project and `--all-projects`) - `sync clone` - `sync pull` / `sync diff` with `--all-projects` - `org setup` (`projects_failed`) - `project invite --from-csv` (`failed`) The human headline now states the failure instead of claiming success -- `Failed: Pushed: 3 created, 0 updated, 0 deleted, 1 failed` -- so the failed count is in the main line rather than only in the warnings below it. `sync push`'s inline human rendering moved into `_render_push_result` because its early `return`s for `no_changes` / `dry_run` would otherwise bypass the exit call. `--json` output is unchanged and is emitted BEFORE the non-zero exit, so a JSON caller still receives the complete payload including the per-item error detail. Read-only multi-project fan-outs (`billing credits`, `job list`, `schedule list`, `notification list`) are deliberately excluded: they document per-project degradation as intended behavior, and failing the whole read because one project is unreachable would break existing callers.
Contributor
|
Dear Claude, without reviewing this PR any further, I'd just like to highlight this one thing – |
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.
What
Commands that survive a per-item failure collected it into the result and then printed a fixed green
Success:line and exited 0 anyway. Async clonewhere every config failed reportedSuccess ... 0 createdand exit 0; async pushwhere half the configs failed reported success too. The failures showed only as warnings underneath, so nothing in the exit code distinguished a clean run from a total failure -- a person had to read thecreatedcount and count the warning lines, and a script could not branch on it at all.Why
Per-item resilience is deliberate: one bad config must not abort a whole push. The defect is that the resilience never reached the reporting. This makes the outcome legible in both places a caller looks:
storage delete-table,storage file-tagandstorage describe-migratealready used, now applied where the audit found it missing;Failed: Pushed: 3 created, 0 updated, 0 deleted, 1 failed.--jsonoutput is unchanged and is emitted before the non-zero exit, so a JSON caller still receives the complete payload including the per-item error detail. Exit 1 here never means "no output".Commands changed
sync push(single +--all-projects)errors[]/summary.failedsync cloneerrors[]sync pull/sync diffwith--all-projectssummary.failedorg setupprojects_failedproject invite --from-csvfailedA clean run is untouched: no failures still means the green success line and exit 0.
no_changesand--dry-runresults are not failures.Scope decision: read-only fan-outs excluded
billing credits,job list,schedule list,notification listand the other multi-project reads document per-project degradation as intended behavior ("per-project failures degrade individually, the run never aborts"). Failing the whole read because one project is unreachable would break existing callers, so they still exit 0 and report the per-project error inerrors. This is stated explicitly in the newgotchas.mdentry so an agent knows which array to check.The audit also confirmed the bulk storage commands (
delete-table,truncate-table,delete-column,delete-bucket,file-delete,file-tag,snapshot-delete,describe-batch,describe-migrate) were already correct -- no change needed there.Implementation note
sync push's inline human rendering moved into_render_push_resultbecause its earlyreturns forno_changes/dry_runwould otherwise bypass the exit call. Behavior for those two statuses is byte-identical.commands/sync.pygrows by 23 code lines and stays over the 800-line soft ceiling it was already over before this PR (1156 -> 1179). Splitting it is a much larger refactor than this bug fix warrants; flagging it rather than bundling it.How it was tested
tests/test_partial_failure_exit_code.py-- 16 tests covering every changed command, both directions (failure -> exit 1, clean -> exit 0), theno_changes/--dry-runnon-failure cases, and that--jsonstill emits the full payload alongside the non-zero exit.Verified the tests are not vacuous: with
src/reverted tomain, 9 of the 16 fail; the other 7 are the clean-run control cases that must keep passing.make checkpasses (6585 passed, 12 skipped).Docs
Per convention #17:
gotchas.md(new entry,(since vNEXT)),commands-reference.md(four entries, incl. correcting the now-wrong "Exit 0 even withfailed > 0-- inspect the JSON" line),CLAUDE.mdcommand list, andcontext.pyAGENT_CONTEXTexit-code section. No command was added, renamed or removed.Fixes #745