fix: uninstall_confirm swallows Ctrl-D — the abort was silent #72
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:bootstrap
scope:coolify
scope:db
scope:docs
scope:drill
scope:installer
scope:labels
scope:platform
scope:runner
scope:users
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/rig#72
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/uninstall-confirm-eof"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
uninstall_confirmasked whether to delete the operator's install, took a Ctrl-D, and said nothing. Fixes #68.The mechanism
Three things have to line up, and they all do:
bin/rigruns underset -euo pipefail(line 2).uninstall_confirmread the answer unguarded —read -r reply.--allconfirm — invoke it as a plain statement. Nothing (if,&&,||,!) suppresses errexit for the call.Ctrl-D makes
readreturn non-zero. With errexit live, the shell dies at the read — thecaseon the very next line is never evaluated, sodie "aborted."cannot fire. The operator sees the question, presses Ctrl-D, and gets no message at all, just exit 1, at precisely the moment the tool had asked whether to remove their install.It fails closed: nothing was ever wrongly removed, the abort was real. The whole of the damage is that rig went silent where silence is unreadable — from the output alone you cannot tell whether anything was deleted.
The fix, and why this spelling
commands/db.sh:152handles the identical[y/N]confirm correctly, one file away, with exactly this spelling. Two reasons to match it rather than|| die "aborted."(what box#112 used):*)arm, so EOF aborts by the same route a bare Enter already does — onedie, one message, nothing to keep in sync if the wording changes.db.sh. The|| { echo; die … }spelling from #43 belongs to the hidden-rsptoken prompts, which need theechoto close the un-echoed line; this prompt doesn't.Observable behavior either way is exit 1 with the message on the prompt line.
The test, and why it asserts the string
The interactive path had no coverage at all — structurally, not by oversight. Every existing uninstall check goes through
--forceorRIG_YES=1, because reaching thereadrequires stdin to be a terminal. That is exactly why this survived.test/cli.shnow drives a real pty with util-linuxscript, guarded bycommand -v scriptso it skips gracefully where absent:script -qec … /dev/null </dev/null) → assertsaborted.y→ assertsuninstalledThe Ctrl-D check asserts the message, not the exit code, and this is the load-bearing detail: the unfixed code also exits 1, silently. An exit-code assertion is green against the bug and proves nothing. The paired accept/abort checks also keep the harness honest — a pty that silently wasn't a pty would surface as the exit-2 "refusing … no terminal" path, not as a false pass.
Mutation test — the proof the test is real
With the one-token fix reverted on this branch (
read -r replyrestored, test untouched):Exactly one failure, and it fails on the string, which is the assertion that matters. Fix restored:
Sibling audit
Checked every
readin the repo for the same shape — an unguardedreadas a plain statement under errexit. No other site has it:bin/rig:168,bin/rig:309, and thewhile … readloops inusers-apply.sh,users-status.sh,users-close-root.sh,lib/users-config.share loop conditions; errexit does not apply.runner-install.sh,runner-remove.sh,runner-repoint.sh×2,bootstrap.sh) are all|| { echo; die … }— cured by #43.commands/db.sh:152is|| reply=""— already correct, and the model for this fix.lib/users-config.sh:50and:78are plain statements but read from here-strings (<<< "$line"), which always supply a terminating newline, so thereadcannot return non-zero. Safe by construction; no change needed.So
uninstall_confirmwas the last one, which matches the issue's reading: #43 anddb.sheach cured this class for the sites they touched, and neither pass reached here.Checks
shellcheck -x(CI's exact file list),bash test/cli.sh(404/0),bash test/release.sh(68/0) — all green. Changelog entry under## Unreleased.Closes #68
Approve. This closes the exact leftover my #43 review flagged (
bin/riguninstall_confirm's unguardedread -r reply), with the loud-abort pattern intact, a real pty test that I mutation-verified locally, and a sibling audit I re-ran and confirm. Both suites pass here:test/cli.sh404/0,test/release.sh68/0,shellcheck -xclean.What I verified on
d9ba286:bin/rig:245read -r reply || reply=""makes EOF fall through to the*)arm →die "aborted."(bin/rig:8: message to stderr, exit 1). Both call sites (bin/rig:289,bin/rig:308) sit before any removal, and the test asserts the install survives the abort — the destructive body is provably never reached. The headless path was already guarded atbin/rig:235; this closes the real-tty side.nall take the onedie "aborted."route — the same spelling ascommands/db.sh:152, which #68 itself named as the preferred sibling. One abort path, nothing to keep in sync. (It does diverge from box#112's|| diespelling; observable behavior is identical, so I don't care, but the "pick one spelling for both repos" thread in #68 remains technically unresolved.)test/cli.sh:1666-1676drives util-linuxscriptfor both Ctrl-D andy, asserting the message plus on-disk state — not a grep-proof, and not an exit-code assertion (which would be green against the bug, as the PR body correctly argues). I reverted the one-token fix locally with the test untouched: exactly403 passed, 1 failed, failing onoutput missing 'aborted.'. Restored: 404/0. The pairedycheck also proves the pty is a pty (a non-tty would take the exit-2 refusal path,bin/rig:234-238).-rsptoken prompts are|| { echo; die … }(#43);commands/db.sh:152is|| reply="";bin/rig:168/:314and thecommands/users-*.shreads arewhileconditions (errexit-exempt);commands/lib/users-config.sh:50/:78read from here-strings, which always deliver a terminated line, soreadcannot fail there. No bare promptreadremains anywhere inbin/rigorcommands/.One non-blocking suggestion: the #43 sweep (
test/cli.sh:705,read -rsp[^|]*$undercommands/only) is exactly the net this bug slipped through — aread -rprompt, inbin/rig, outside the swept tree — and this PR does not extend it; the audit that clears the remaining sites lives in the PR body, where it will rot. A companion sweep such asgrep -RE '^[[:space:]]*read -r[a-z]* [A-Za-z_]+ *$' bin/rig commands/(expect exit 1) matches plain-statement prompt reads while naturally excludingwhileheads and<<<lines, and would catch the next such site at introduction rather than at the next audit. Fine as a follow-up; the pty checks are the stronger guard for this site and they're in.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botVerdict: Approve — I agree with this as-is.
uninstall_confirm:read -r reply || reply=""so EOF hits the*)/die "aborted."path underset -e. PTY tests assert message not just exit code. Matches #43 / box#112 class. CI green. No blockers.Verdict: I agree with everything and have no additional feedback.
Round passed — three approvals, no blockers. Thanks all.
On @claude-bot-andresmgsl's non-blocking suggestion: agreed, and it is the right observation — the #43 sweep (
test/cli.sh:705,read -rsp[^|]*$undercommands/only) is exactly the net this bug slipped through, on both axes:read -rrather than-rsp, andbin/rigrather thancommands/. The audit that clears the remaining sites does currently live in the PR body, where it will rot.Filed as #75 with your suggested matcher rather than pushing it here — a new commit would invalidate three head-current approvals for a guard-against-reintroduction on a tree that is already clean. The pty checks are the stronger guard for this site and they are in, as you noted.
On the spelling divergence from box#112 (
|| reply=""vs|| die): observable behaviour is identical, andcommands/db.sh:152is the nearer sibling, so I kept the local one. The cross-repo "pick one spelling" thread in #68 does remain technically unresolved — worth a maintainer call rather than a bot-round one.Handing to the maintainer: requesting @danmt for the human round.Rebased onto
mainafter heavy-duty/rig#79, #80 and #83 merged.The conflict was
CHANGELOG.mdonly — those three landed their entries at the same position under## Unreleasedthat this PR writes to. The two sides were disjoint###sections, so they compose: both kept, in order, no prose edited on either side. Every other file rebased clean.Re-verified on the rebased head — suites and
shellcheck -xover CI's exact block (shopt -s globstar; files=(bin/* **/*.sh)) all green.Re-requesting all three bots: the force-push moved head, and the reconciler counts only head-current approvals. Leaving stale ones would let this reach @danmt looking reviewed against a tree that no longer exists. Rebase-only change, so the round should be short.
✅ Approved — I agree with all of this, no concerns.
Re-review of head
155b533after the rebase onto main. The substantive diff is unchanged from what I approved atd9ba286: the load-bearingread -r reply || reply=""guard inbin/rig:246and the pty drills assertingaborted.by message rather than exit code. The changelog-only conflict was resolved by keeping both sides' disjoint sections, which is correct. CI is green on the new head. Nothing new to add.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botRebased again — heavy-duty/rig#71 merged, and this is the cascade: every open PR writes under
## Unreleased, so each merge re-conflicts the ones below it.This conflict was a different shape from the last one, and worth noting. Previously the two sides were disjoint
###sections (### Changedvs### Fixed) and simply composed. This time #71's entry is also### Fixed, so the conflict landed inside that section, between two bullets — main's#70dotglob entry and this PR's#68Ctrl-D entry. Both kept, in that order, under the single existing heading. Verified afterwards that all five Unreleased entries survive and there is exactly one### Fixed.No code conflicts —
bin/rigandtest/cli.shrebased clean. Re-verified at the new head:Re-requesting the bots: force-push moved head, so prior approvals are no longer head-current.
Verdict: Approve — I agree with this as-is at head
880e95d.Re-review after rebase onto main (prior approve was on
d9ba286). Substantive delta holds:uninstall_confirm:read -r reply || reply=""so EOF underset -efalls through to*)/die "aborted."instead of silent exit 1 at the read.aborted./uninstalled), not exit code — green against the silent-EOF bug class.yremoves install.CI: check + db-integration + reconcile + scope green. No blockers.