Put a blocked package upgrade back to whoever is updating (#6830)
Pacman answers its own conflict question with No under --noconfirm, so one retired package can stop every update after it. Which package to drop is a decision rather than a cleanup, so run the upgrade again with pacman asking when there is a terminal to answer on, and report instead when -y promised not to ask. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
625c66301d
commit
5ca3030c5a
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
require_command script
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
stub_bin="$test_tmp/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
# Fails the first -Syu with the report under test, then succeeds. Every call
|
||||
# records its arguments and which of its streams reached a terminal: pacman puts
|
||||
# its questions on stderr once it is not running --noconfirm, so a retry meant
|
||||
# for a person has to keep that stream.
|
||||
cat >"$stub_bin/pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
attempt=$(($(cat "$PACMAN_ATTEMPTS") + 1))
|
||||
echo "$attempt" >"$PACMAN_ATTEMPTS"
|
||||
{
|
||||
printf 'args %s\n' "$*"
|
||||
for fd in 0 1 2; do
|
||||
if [[ -t $fd ]]; then printf 'tty%s yes\n' "$fd"; else printf 'tty%s no\n' "$fd"; fi
|
||||
done
|
||||
} >>"$PACMAN_CALLS"
|
||||
|
||||
if ((attempt == 1)); then
|
||||
cat "$CONFLICT_REPORT" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "upgrade complete"
|
||||
STUB
|
||||
|
||||
chmod +x "$stub_bin/sudo" "$stub_bin/pacman"
|
||||
|
||||
# Everything a blocked qemu-common upgrade leaves on stderr, and no more. The
|
||||
# ":: ... Remove qemu-block-gluster? [y/N]" pacman asked is deliberately absent:
|
||||
# under --noconfirm it goes to stdout, so nothing downstream of the report can
|
||||
# be built on having read it.
|
||||
write_conflict_report() {
|
||||
echo 0 >"$test_tmp/attempts"
|
||||
: >"$test_tmp/calls"
|
||||
{
|
||||
echo "error: unresolvable package conflicts detected"
|
||||
echo "error: failed to prepare transaction (conflicting dependencies)"
|
||||
} >"$test_tmp/report"
|
||||
}
|
||||
|
||||
update_env() {
|
||||
printf '%s\n' \
|
||||
"OMARCHY_REPLACED_DIR=$test_tmp/replaced" \
|
||||
"PACMAN_ATTEMPTS=$test_tmp/attempts" \
|
||||
"PACMAN_CALLS=$test_tmp/calls" \
|
||||
"CONFLICT_REPORT=$test_tmp/report" \
|
||||
"OWNED_PATHS=" \
|
||||
"OMARCHY_UPDATE_UNATTENDED=${OMARCHY_UPDATE_UNATTENDED:-}" \
|
||||
"OMARCHY_UPDATE_INTERACTIVE=${OMARCHY_UPDATE_INTERACTIVE:-}" \
|
||||
"PATH=$stub_bin:$ROOT/bin:$PATH"
|
||||
}
|
||||
|
||||
# No terminal on any stream, the way a cron or ssh caller arrives.
|
||||
run_headless() {
|
||||
mapfile -t environment < <(update_env)
|
||||
env "${environment[@]}" bash "$ROOT/bin/omarchy-update-system-pkgs" \
|
||||
</dev/null >"$test_tmp/out" 2>"$test_tmp/err"
|
||||
}
|
||||
|
||||
# script gives the update the pty that omarchy-update always runs it on, so the
|
||||
# terminal checks see what a person at the keyboard would give them. Its
|
||||
# transcript is stdout and stderr together, which is also what that person sees.
|
||||
# $1 optionally takes one stream back off the pty.
|
||||
run_on_terminal() {
|
||||
mapfile -t environment < <(update_env)
|
||||
env "${environment[@]}" \
|
||||
script -qec "bash '$ROOT/bin/omarchy-update-system-pkgs' ${1:-}" "$test_tmp/out" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
call_line() {
|
||||
awk -v call="$1" -v key="$2" \
|
||||
'$1 == "args" { n++ } n == call && $1 == key { sub(/^[^ ]+ /, ""); print }' "$test_tmp/calls"
|
||||
}
|
||||
|
||||
write_conflict_report
|
||||
run_on_terminal || fail "a package conflict is not resolved on a terminal"
|
||||
(($(cat "$test_tmp/attempts") == 2)) ||
|
||||
fail "a package conflict does not get an interactive retry"
|
||||
[[ $(call_line 2 args) == *"-Syu"* ]] ||
|
||||
fail "the interactive retry does not upgrade"
|
||||
[[ $(call_line 2 args) != *"--noconfirm"* ]] ||
|
||||
fail "the interactive retry still answers pacman's questions itself"
|
||||
[[ $(call_line 2 args) != *"--ask"* ]] ||
|
||||
fail "the interactive retry answers pacman's questions from a bitmask instead"
|
||||
pass "a package conflict is put back to the person running the update"
|
||||
|
||||
[[ $(call_line 2 tty0) == "yes" && $(call_line 2 tty2) == "yes" ]] ||
|
||||
fail "the interactive retry cannot be answered: pacman has no terminal left"
|
||||
pass "the interactive retry keeps the streams pacman asks and listens on"
|
||||
|
||||
# Which streams have to be a terminal follows from where pacman asks: stderr
|
||||
# carries the question once --noconfirm is gone, stdin carries the answer, and
|
||||
# stdout carries progress bars nobody has to see to answer.
|
||||
write_conflict_report
|
||||
run_on_terminal '>/dev/null' ||
|
||||
fail "a redirected progress stream is mistaken for an unattended update"
|
||||
(($(cat "$test_tmp/attempts") == 2)) ||
|
||||
fail "a conflict goes unasked when only stdout is redirected"
|
||||
pass "an answerable session is not turned away over its progress output"
|
||||
|
||||
write_conflict_report
|
||||
if run_on_terminal '2>/dev/null'; then
|
||||
fail "a conflict is asked about on a stream nobody is reading"
|
||||
fi
|
||||
(($(cat "$test_tmp/attempts") == 1)) ||
|
||||
fail "pacman is left prompting where the question cannot be seen"
|
||||
pass "a session that cannot show the question is not asked one"
|
||||
|
||||
[[ $(call_line 1 tty2) == "no" ]] ||
|
||||
fail "the first upgrade no longer captures the error report"
|
||||
pass "the first upgrade still captures its errors for the handler"
|
||||
|
||||
write_conflict_report
|
||||
if run_headless; then
|
||||
fail "a package conflict passes for a completed update without a terminal"
|
||||
fi
|
||||
(($(cat "$test_tmp/attempts") == 1)) ||
|
||||
fail "a package conflict is retried with no terminal to answer on"
|
||||
grep -q 'omarchy update' "$test_tmp/err" ||
|
||||
fail "a package conflict with no terminal does not say how to answer it"
|
||||
pass "a package conflict with no terminal reports instead of hanging"
|
||||
|
||||
write_conflict_report
|
||||
if OMARCHY_UPDATE_UNATTENDED=1 run_on_terminal; then
|
||||
fail "an unattended update stops on a prompt nobody answers"
|
||||
fi
|
||||
(($(cat "$test_tmp/attempts") == 1)) ||
|
||||
fail "an unattended update prompts anyway"
|
||||
pass "-y is kept: an unattended update never waits on an answer"
|
||||
|
||||
# The interactive upgrade skips the error capture the handler depends on, so
|
||||
# reaching it any other way would lose the report that drives every recovery.
|
||||
write_conflict_report
|
||||
if OMARCHY_UPDATE_INTERACTIVE=1 run_headless; then
|
||||
fail "a caller reaches the interactive upgrade on its own"
|
||||
fi
|
||||
[[ $(call_line 1 args) == *"--noconfirm"* ]] ||
|
||||
fail "a caller can ask for an interactive upgrade directly"
|
||||
pass "only the conflict handler can hand the upgrade to a person"
|
||||
Reference in New Issue
Block a user