Skip to content

fix(cli): stop reporting success for updates that never ran - #5555

Closed
bettercalln1ck wants to merge 1 commit into
Osmantic:mainfrom
bettercalln1ck:fix/4179-update-exit-codes
Closed

bettercalln1ck wants to merge 1 commit into
Osmantic:mainfrom
bettercalln1ck:fix/4179-update-exit-codes

Conversation

@bettercalln1ck

Copy link
Copy Markdown
Contributor

Summary

Fixes #4179.
Fixes #4175.

Closes #4179 and #4175 — both are cmd_update telling the caller an update happened when it did not.

Exit codes (#4179). _check_version_compat returns non-zero for three different reasons:

  1. the operator declines a downgrade at the prompt,
  2. the operator declines a major-version jump at the prompt,
  3. the tool hard-blocks an install below min_compatible_ods_version (UPGRADE BLOCKED … use 'ods update --force' to override).

cmd_update collapsed all three into exit 0. Case 3 is the damaging one: the tool refused to run, and every caller — CI, a wrapper script, an operator reading $? — was told the update succeeded.

Cases 1-2 and case 3 are not the same event, so they no longer share an exit code. The function returns 2 when a person answers "no" and 1 when the tool refuses; cmd_update maps 2 to exit 0 and anything else non-zero to exit 1. Declining stays a success on purpose — that matches how ods-restore.sh treats a cancelled restore, which its own test asserts ("Cancel is not an error (returns 0)").

Pre-update backup (#4175). The fallback path ran:

if ! "$INSTALL_DIR/ods-backup.sh" "$_snap_label"; then
    warn "Pre-update backup failed; proceeding without safety net."
fi

ods-backup.sh has no positional argument. The label falls through to its *) catch-all, which prints Unknown option: pre-update-… and exits 1 — so this branch has never once produced a backup, on any host that lacks ods-update.sh. And because the failure is downgraded to a warning, the update proceeds regardless. It now passes --description, which is the flag that actually exists.

The primary path (ods-update.sh backup "$label") is fine — cmd_backup there does take a positional name — which is why this went unnoticed.

AI Assistance

AI-assisted: an AI coding assistant drafted the patch and the regression test and ran the validation recorded below. I reviewed the diff, chose the validation, and am accountable for the change.

Release Lane

  • Stable hotfix targeting release/2.6.x
  • Mainline change targeting main
  • Next-minor work targeting the next feature/minor release
  • Not sure; reviewer should help classify

Stable hotfix reason:

n/a

Changed Surface

  • Docs only
  • Tests only
  • Dashboard UI
  • Dashboard API / host agent
  • Installer / bootstrap / lifecycle
  • Docker Compose / service manifests
  • Model routing / Hermes / capabilities
  • Network exposure / auth / proxy
  • Dependencies / runtime wiring

Risk And Validation

  • Risk level: Medium
  • Validation run:
    • git diff --check
    • Markdown/link sanity for docs
    • Focused tests listed below
    • Dashboard lint/test/build
    • Extension audit / compose validation
    • Release-grade fleet or scoped hardware validation
    • Stable-lane patch validation, if targeting release/2.6.x
    • Not required because: confined to two call sites and one return contract inside cmd_update; the neighbouring CLI suites are unchanged and the new test pins both the declined and refused paths so the exit-code split cannot silently collapse again.

Commands/results:

$ bash tests/test-update-abort-exit-codes.sh
[PASS] a bare positional label is rejected by ods-backup.sh (the old call)
[PASS] cmd_update passes the label via --description
[PASS] no bare-positional invocation remains
[PASS] an operator declining returns the distinct 'declined' code
[PASS] the hard compatibility block still returns a failure code
[PASS] cmd_update exits non-zero when the tool refuses the update
[PASS] abort reasons are no longer collapsed
PASS=7 FAIL=0

# five of the seven fail with ods-cli restored from origin/main
$ git checkout origin/main -- ods/ods-cli
$ bash tests/test-update-abort-exit-codes.sh
PASS=2 FAIL=5

# neighbouring CLI suites
$ bash tests/test-ods-cli-pipefail-tolerance.sh   -> PASS
$ bash tests/test-cli-install-dir-resolution.sh   -> PASS
$ bash tests/test-update-script-backup.sh         -> PASS
$ bash tests/test-cli-update-verification.sh      -> FAIL, identically on a
  pristine main worktree (its fixture lacks bin/ods-host-agent.py), so
  pre-existing and unrelated — verified side by side.

$ make lint
All lint checks passed.
$ shellcheck --exclude=SC1091,SC2034 --severity=error \
    ods/ods-cli ods/tests/test-update-abort-exit-codes.sh
(clean)

Operational Change Check

  • This is not an operational change.
  • This is an operational change and validation is recorded above.
  • This is an operational change and validation is intentionally deferred for:

Notes For Reviewers

  • The first assertion runs the real ods-backup.sh with the exact argv cmd_update used to pass, and confirms it is rejected with Unknown option. That is the evidence the fallback never worked, rather than an inference from reading the parser.
  • The exit-code assertions are source-level (the behaviour needs an interactive prompt and a version-mismatched install to exercise end to end). They pin both directions: the declined path keeps its distinct code, and the hard block keeps a failure code — so a future change cannot quietly re-collapse them into one branch.
  • Deliberate asymmetry worth reviewing: an operator answering "no" still exits 0. I took that from ods-restore.sh's existing convention rather than inventing a new one. If you would rather every abort be non-zero, that is a one-line change to the mapping — but it would make interactive ods update + "n" look like a failure to any wrapper.
  • Wired into both ods/Makefile and the integration-smoke job in test-linux.yml; the workflow entry is the one that gates, since no workflow invokes make test.

Two ways cmd_update claimed an update it did not perform.

Exit codes (Osmantic#4179): _check_version_compat returns non-zero for three different
reasons — the operator declining a downgrade, the operator declining a major
jump, and the tool hard-blocking an install below min_compatible_ods_version —
and cmd_update mapped all three to `exit 0`. Automation calling `ods update`
was told the update succeeded when the tool had refused it outright.

Those are not the same event, so they no longer share an exit code. The
function now returns 2 when a person answers "no" and 1 when it refuses;
cmd_update exits 0 for the former and 1 for the latter. Declining stays a
success, matching how ods-restore treats a cancelled restore.

Pre-update backup (Osmantic#4175): the fallback ran `ods-backup.sh "$_snap_label"`,
but that parser has no positional argument — the label lands in its `*)`
catch-all, prints "Unknown option" and exits 1. The failure was downgraded to
"proceeding without safety net", so on every host without ods-update.sh the
pre-update backup has never once been taken. It now passes --description,
which is the flag that exists.
@Lightheartdevs

Copy link
Copy Markdown
Collaborator

Thanks for this contribution. We're closing outside-contributor pull requests that have had no activity for 14 days or more, so the queue reflects work that can still land. This isn't a judgment on the change itself. If it's still needed, please rebase onto main and reopen it or open a focused PR. See #7253 for details and the contribution policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: Update compat abort maps to exit 0 fix: Pre-update backup passes label as invalid CLI arg

2 participants