Cover completion failures in the package scriptlet contract

pacman continues a transaction after a failed scriptlet. Model that in
the shared package-scriptlet contract: a failed pre_upgrade followed by
post_upgrade must keep the removal blocker while a rule remains, a
clean retry recovers, a stranded blocker with a live rule is cleaned by
the next completed installation, a fresh install that cannot remove a
leftover rule leaves publication refused, and the sweep does not depend
on inherited glob state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Afonso OliveiraandClaude Fable 5.1 committed 2026-09-17 20:41:45 +01:00
1 parent ea617b9126
commit c8407ee73f
1 file changed
+51 -3
@@ -115,9 +115,49 @@ for name in omarchy-settings omarchy-settings-dev; do
enable_locked 1000 15
pre_upgrade && post_upgrade
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# pacman does not stop a transaction on a failed scriptlet, so a failed
# pre_upgrade can be followed directly by post_upgrade. Completion must
# not clear the blocker while a rule remains, and a clean retry recovers.
enable_locked 1000 15
TEST_DELETE_FAIL=1 assert_status 1 pre_upgrade
TEST_DELETE_FAIL=1 assert_status 1 post_upgrade
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
assert_status 1 enable_locked 1000 15
post_upgrade
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# A stranded blocker plus a live rule from an interrupted removal is
# cleaned by the next completed installation, not merely unblocked.
enable_locked 1000 15
: >"$REMOVAL_BLOCKER"
post_install
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# A fresh install has no earlier step, so completion must hold the blocker
# itself while it sweeps: a leftover rule it cannot remove leaves
# publication refused rather than merely reporting an error.
enable_locked 1000 15
rm -f "$REMOVAL_BLOCKER"
TEST_DELETE_FAIL=1 assert_status 1 post_install
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
assert_status 1 enable_locked 1000 15
post_install
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# The sweep must not depend on the glob state pacman's shell inherits.
enable_locked 1000 15
( set -f; GLOBIGNORE='*' post_upgrade )
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# A failure before the lock is even taken, such as an untrusted lock
# directory, must still leave publication refused.
enable_locked 1000 15
rm -f "$REMOVAL_BLOCKER"
TEST_BAD_PATH="$test_tmp/run/lock" assert_status 1 post_install
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
TEST_BAD_PATH="$test_tmp/run/lock" assert_status 1 pre_upgrade
[[ -e $REMOVAL_BLOCKER ]]
post_install
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
)
done
pass "both settings packages revoke grants, block publication, and recover on installation"
pass "both settings packages revoke grants, block publication, and recover on installation only with the namespace empty"
reset_grant
# Hold the source lock, then start package removal. A native flock on the
@@ -148,13 +188,21 @@ done
/usr/bin/bash -euo pipefail -c 'source "$1"; pre_remove; post_remove' bash "$test_tmp/omarchy-settings.install" >"$test_tmp/removal.log" 2>&1 &
removal=$!
children+=("$removal")
# The removal announces itself before waiting for the lock, so a publisher
# still holding it is refused rather than allowed to publish a rule that the
# removal would delete a moment later.
for (( attempt=0; attempt<200; attempt++ )); do
[[ ! -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || break
sleep 0.01
done
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || fail "removal did not announce itself before waiting for the lock"
touch "$test_tmp/release"
wait "$publisher" || fail "publisher failed" "$(cat "$test_tmp/publisher.log")"
if wait "$publisher"; then fail "publisher was allowed to publish after removal announced itself" "$(cat "$test_tmp/publisher.log")"; fi
wait "$removal" || fail "removal failed" "$(cat "$test_tmp/removal.log")"
children=()
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]]
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]]
pass "native lock serializes grant publication with package removal"
pass "an announced package removal refuses a waiting publisher and clears the namespace"
# systemd-tmpfiles operates on an explicit disposable root, never the host.
reset_grant