Keep the Quattro upgrade from aborting silently into an unsafe state (#6617)

The script is fetched from the branch but calls into the installed
/usr/share/omarchy tree, which can lag it. A packaged build without
bin/omarchy-done aborted apply_user_transition under set -e two thirds of
the way through: NetworkManager was already enabled, iwd was not yet
disabled, and nothing was printed, so the run read as finished.

The completion markers are now written directly instead of through
omarchy-done, and the two remaining unguarded packaged commands warn
rather than abort. Retiring iwd moves up next to the NetworkManager
enable it depends on, so no failure in between can leave both enabled.
An aborted run now says so instead of returning to the prompt on a green
progress line.

Fixes #6575

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-08 10:16:41 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 5e5a6e8c89
commit 318bf2c43a
2 changed files with 91 additions and 16 deletions
+37 -12
View File
@@ -322,6 +322,8 @@ fi
sudo_keepalive_pid="" sudo_keepalive_pid=""
hyprland_config_reload_suppressed=0 hyprland_config_reload_suppressed=0
upgrade_started=0
upgrade_completed=0
cleanup_on_exit() { cleanup_on_exit() {
local exit_status=$? local exit_status=$?
@@ -334,6 +336,14 @@ cleanup_on_exit() {
kill "$sudo_keepalive_pid" 2>/dev/null || true kill "$sudo_keepalive_pid" 2>/dev/null || true
fi fi
# set -e aborts without a word, and the last thing on screen is a green
# progress line, so a half-finished upgrade otherwise reads as a finished one.
if (( exit_status != 0 && upgrade_started && ! upgrade_completed )); then
printf '\n\033[31mUpgrade incomplete - do NOT reboot.\033[0m\n' >&2
printf 'The system is part Omarchy 3 and part Omarchy quattro; rebooting now can leave it without a working network or desktop.\n' >&2
printf 'Fix the error reported above and run this script again. Re-running is safe and resumes the remaining steps.\n' >&2
fi
exit "$exit_status" exit "$exit_status"
} }
trap cleanup_on_exit EXIT trap cleanup_on_exit EXIT
@@ -1040,11 +1050,9 @@ USER_UNITS
cleanup_retired_services() { cleanup_retired_services() {
log "Disabling services retired by Omarchy quattro" log "Disabling services retired by Omarchy quattro"
# Do not stop iwd or systemd-networkd mid-upgrade; disabling is enough and # iwd is retired alongside the NetworkManager enable it depends on. Do not
# avoids dropping a running Wi-Fi connection before the user can reboot into # stop systemd-networkd mid-upgrade; disabling is enough and avoids dropping a
# NetworkManager. # running connection before the user can reboot into NetworkManager.
as_root systemctl disable iwd.service >/dev/null 2>&1 || true
local networkd_unit local networkd_unit
for networkd_unit in \ for networkd_unit in \
systemd-networkd.service \ systemd-networkd.service \
@@ -1339,7 +1347,12 @@ EOF
enable_system_service linux-modules-cleanup.service enable_system_service linux-modules-cleanup.service
enable_system_service docker.socket enable_system_service docker.socket
enable_system_service systemd-resolved.service enable_system_service systemd-resolved.service
# NetworkManager replaces iwd, and both halves of that swap belong in one
# step: a machine that boots with both enabled has two daemons fighting over
# the Wi-Fi adapter. iwd is disabled but never stopped, so the running
# connection survives until the reboot.
enable_system_service NetworkManager.service enable_system_service NetworkManager.service
as_root systemctl disable iwd.service >/dev/null 2>&1 || true
enable_system_service bluetooth.service enable_system_service bluetooth.service
enable_system_service power-profiles-daemon.service enable_system_service power-profiles-daemon.service
enable_system_service sddm.service enable_system_service sddm.service
@@ -2256,11 +2269,15 @@ if command -v xdg-mime >/dev/null 2>&1; then
xdg-mime default HEY.desktop x-scheme-handler/mailto || true xdg-mime default HEY.desktop x-scheme-handler/mailto || true
fi fi
"$root/bin/omarchy-done" mark first-run-user # This script is fetched from the branch and runs against whatever packaged
rm -f "$state_dir/first-run-user.done" # tree the channel currently serves, so the packaged tree can be older than the
# script. The completion markers are written directly rather than through
"$root/bin/omarchy-done" mark finalize-user # omarchy-done, because a command missing from an older build would abort the
rm -f "$state_dir/finalize-user.done" # upgrade two thirds of the way through.
done_dir="$state_dir/done"
mkdir -p "$done_dir"
touch "$done_dir/first-run-user" "$done_dir/finalize-user"
rm -f "$state_dir/first-run-user.done" "$state_dir/finalize-user.done"
USER_SETUP USER_SETUP
} }
@@ -2360,6 +2377,10 @@ refresh_current_theme_after_upgrade() {
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
} }
# Everything below mutates the system, so a non-zero exit from here on leaves a
# half-upgraded machine that must not be rebooted.
upgrade_started=1
# Suppress live Hyprland reloads before anything touches the legacy config # Suppress live Hyprland reloads before anything touches the legacy config
# trees; the swap windows otherwise surface transient "source= globbing error" # trees; the swap windows otherwise surface transient "source= globbing error"
# popups in the running session. # popups in the running session.
@@ -2381,8 +2402,10 @@ cleanup_legacy_user_paths
apply_system_transition apply_system_transition
apply_user_transition apply_user_transition
apply_user_hardware_transition apply_user_hardware_transition
run_as_user_omarchy omarchy-refresh-applications run_as_user_omarchy omarchy-refresh-applications ||
run_as_user_omarchy omarchy-bar defaults warn "Could not refresh application launchers; run 'omarchy refresh applications' after reboot."
run_as_user_omarchy omarchy-bar defaults ||
warn "Could not restore the default bar widgets; run 'omarchy bar defaults' after reboot."
cleanup_retired_services cleanup_retired_services
ensure_sleep_lock_service ensure_sleep_lock_service
remove_retired_default_packages remove_retired_default_packages
@@ -2401,6 +2424,8 @@ else
warn "Leaving retired Omarchy 3 session processes running until reboot." warn "Leaving retired Omarchy 3 session processes running until reboot."
fi fi
upgrade_completed=1
cat <<EOF cat <<EOF
WARNING: You must address any errors in the above before rebooting. WARNING: You must address any errors in the above before rebooting.
+54 -4
View File
@@ -39,12 +39,24 @@ if grep -F 'skip-first-run-update-notification' "$upgrade_to_quattro" >/dev/null
fi fi
pass "Omarchy 4 upgrade completes first-run as one lifecycle" pass "Omarchy 4 upgrade completes first-run as one lifecycle"
grep -F '"$root/bin/omarchy-done" mark first-run-user' "$upgrade_to_quattro" >/dev/null grep -F 'touch "$done_dir/first-run-user" "$done_dir/finalize-user"' "$upgrade_to_quattro" >/dev/null
grep -F 'rm -f "$state_dir/first-run-user.done"' "$upgrade_to_quattro" >/dev/null grep -F 'rm -f "$state_dir/first-run-user.done" "$state_dir/finalize-user.done"' "$upgrade_to_quattro" >/dev/null
grep -F '"$root/bin/omarchy-done" mark finalize-user' "$upgrade_to_quattro" >/dev/null
grep -F 'rm -f "$state_dir/finalize-user.done"' "$upgrade_to_quattro" >/dev/null
pass "Omarchy 4 upgrade completes first-run and migrates legacy completion markers" pass "Omarchy 4 upgrade completes first-run and migrates legacy completion markers"
# The script runs from the branch against whatever packaged tree the channel
# serves, so a packaged command missing from an older build must never be able
# to abort the upgrade partway through.
if grep -F '"$root/bin/omarchy-done"' "$upgrade_to_quattro" >/dev/null; then
fail "Omarchy 4 upgrade writes completion markers without the packaged omarchy-done"
fi
pass "Omarchy 4 upgrade writes completion markers without the packaged omarchy-done"
for guarded_step in omarchy-refresh-applications 'omarchy-bar defaults'; do
grep -F "run_as_user_omarchy $guarded_step ||" "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade survives a packaged tree without $guarded_step"
done
pass "Omarchy 4 upgrade survives a packaged tree missing top-level commands"
grep -F 'configure_snapper_policy' "$upgrade_to_quattro" >/dev/null grep -F 'configure_snapper_policy' "$upgrade_to_quattro" >/dev/null
grep -F '/usr/share/omarchy/install/config/snapper.sh' "$upgrade_to_quattro" >/dev/null grep -F '/usr/share/omarchy/install/config/snapper.sh' "$upgrade_to_quattro" >/dev/null
grep -F 'bash -euo pipefail "$snapper_config_script"' "$upgrade_to_quattro" >/dev/null grep -F 'bash -euo pipefail "$snapper_config_script"' "$upgrade_to_quattro" >/dev/null
@@ -61,6 +73,44 @@ grep -F 'systemd-networkd.socket' "$upgrade_to_quattro" >/dev/null
grep -F 'systemd-networkd-resolve-hook.socket' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd-resolve-hook.socket' "$upgrade_to_quattro" >/dev/null
pass "Omarchy 4 upgrade retires systemd-networkd for NetworkManager" pass "Omarchy 4 upgrade retires systemd-networkd for NetworkManager"
# Booting with both managers enabled leaves them fighting over the Wi-Fi
# adapter, so enabling NetworkManager and disabling iwd cannot be separated by
# any step that might abort in between.
function_body() {
awk -v name="$1" '$0 == name "() {" { inside = 1; next } inside && $0 == "}" { exit } inside' "$upgrade_to_quattro"
}
if function_body cleanup_retired_services | grep -F 'systemctl disable iwd' >/dev/null; then
fail "Omarchy 4 upgrade does not retire iwd in a step separate from the NetworkManager enable"
fi
grep -A1 -F ' enable_system_service NetworkManager.service' "$upgrade_to_quattro" |
grep -F 'as_root systemctl disable iwd.service' >/dev/null ||
fail "Omarchy 4 upgrade retires iwd in the step that enables NetworkManager"
pass "Omarchy 4 upgrade switches from iwd to NetworkManager atomically"
# set -e aborts silently, so only an explicit banner distinguishes a
# half-upgraded system from a finished one.
grep -Fx 'trap cleanup_on_exit EXIT' "$upgrade_to_quattro" >/dev/null ||
fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently"
cleanup_body=$(function_body cleanup_on_exit)
grep -F 'upgrade_started && ! upgrade_completed' <<<"$cleanup_body" >/dev/null ||
fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently"
grep -F 'Upgrade incomplete - do NOT reboot.' <<<"$cleanup_body" >/dev/null ||
fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently"
grep -F 'exit "$exit_status"' <<<"$cleanup_body" >/dev/null ||
fail "Omarchy 4 upgrade preserves the failing exit status"
grep -F '>&2' <<<"$cleanup_body" >/dev/null ||
fail "Omarchy 4 upgrade reports an aborted run on stderr"
started_line=$(grep -n '^upgrade_started=1$' "$upgrade_to_quattro" | cut -d: -f1)
completed_line=$(grep -n '^upgrade_completed=1$' "$upgrade_to_quattro" | cut -d: -f1)
suppress_line=$(grep -n '^suppress_hyprland_config_reload$' "$upgrade_to_quattro" | cut -d: -f1)
shell_line=$(grep -n '^if start_omarchy_shell_session; then$' "$upgrade_to_quattro" | cut -d: -f1)
[[ -n $started_line && -n $completed_line && -n $suppress_line && -n $shell_line ]] ||
fail "upgrade progress markers and the mutating step range exist"
(( started_line < suppress_line )) || fail "the upgrade is marked started before the first mutation"
(( completed_line > shell_line )) || fail "the upgrade is marked complete only after the last step"
pass "Omarchy 4 upgrade reports an aborted run instead of exiting silently"
grep -F 'omarchy-bar defaults' "$upgrade_to_quattro" >/dev/null grep -F 'omarchy-bar defaults' "$upgrade_to_quattro" >/dev/null
pass "Omarchy 4 upgrade restores service-aware bar defaults" pass "Omarchy 4 upgrade restores service-aware bar defaults"