Files
omarchy/bin/omarchy-update
T
David Heinemeier HanssonandClaude Opus 5.5 e1614f2bdb Ask for the sudo password once per omarchy update (#13323)
* Ask for the sudo password once per omarchy update

Every sudo call in omarchy update prompted, because the no-update wrapper
covered the whole run on top of per-phase revokes, and stay-awake revoked
the timestamp on its own entry and exit. A single update could ask four
times before the snapshot finished (#13319).

Authorize once, right after confirmation, starting from a revoked
timestamp so the prompt always belongs to this update. A background
keepalive refreshes it until the update is done. Prune, snapshot,
stay-awake, keyring, system packages, migrations, orphan removal, service
restarts, the post-update hook, and mise all share that authorization.

AUR builds run third-party PKGBUILD code, so they move to the end and run
cold: the keepalive stops, the timestamp is revoked, and yay and any bare
sudo use the no-update wrapper. The timestamp is revoked again after AUR
and on every exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the single authorization for passwordless sudo and ttyless inhibition

Authorize by running a command instead of sudo -v. Under the default
verifypw=all, -v prompts even when passwordless sudo is enabled, which
would have added a prompt those users never had.

Inside an update without a terminal, stay-awake now reuses the update's
authorization with a non-interactive sudo instead of asking again through
polkit. It falls back to polkit only if that authorization is gone.

The test sudo refuses a cold non-interactive call, as the real one does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:31:40 +02:00

161 lines
6.1 KiB
Bash
Executable File

#!/bin/bash -p
# omarchy:summary=Update Omarchy and system packages
# omarchy:alias=omarchy up
# omarchy:args=[-y]
# omarchy:examples=omarchy update | omarchy update -y
# omarchy:requires-sudo=true
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# Logging and lock acquisition re-exec this command with a sanitized PATH.
# Preserve the caller's path only for the later hook/mise phases.
user_path=${OMARCHY_UPDATE_USER_PATH:-$PATH}
unset OMARCHY_UPDATE_USER_PATH
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path. Starting cold means the single password
# prompt below always belongs to this update, never to an earlier session.
omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
# Validate the no-update wrapper up front: the AUR phase depends on it. Every
# other phase shares the one authorization, so it stays off PATH until then.
omarchy_security_enable_no_update_sudo
PATH="$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
unset OMARCHY_SUDO_NO_UPDATE
# Helpers that manage their own sudo boundary leave this authorization to us.
export PATH OMARCHY_UPDATE_SUDO_SESSION=1
update_stay_awake_stopped=0
sudo_keepalive_pid=""
# Ask for the password once, then keep sudo's timestamp fresh so long package
# downloads, migrations, hooks, and mise never outlast it and prompt again.
# Authorize by running a command rather than sudo -v: under the default
# verifypw=all, -v prompts even when passwordless sudo is enabled.
authorize_update() {
/usr/bin/sudo /usr/bin/true || return 1
local update_pid=$$
(
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
sleep_pid=""
trap - EXIT HUP INT
trap '[[ -z $sleep_pid ]] || kill "$sleep_pid" 2>/dev/null; exit 0' TERM
while :; do
sleep 60 &
sleep_pid=$!
wait "$sleep_pid"
kill -0 "$update_pid" 2>/dev/null || exit 0
/usr/bin/sudo -n /usr/bin/true 2>/dev/null || exit 0
done
) &
sudo_keepalive_pid=$!
}
end_update_authorization() {
if [[ -n $sudo_keepalive_pid ]]; then
kill "$sudo_keepalive_pid" 2>/dev/null || true
wait "$sudo_keepalive_pid" 2>/dev/null || true
sudo_keepalive_pid=""
fi
omarchy_security_revoke_sudo_timestamp
}
cleanup_update() {
local status=$?
trap - EXIT HUP INT TERM
if ! end_update_authorization; then
echo "Could not invalidate sudo before update cleanup." >&2
omarchy_security_exit_with_revoked_sudo 1
fi
if (( update_stay_awake_stopped == 0 )); then
omarchy-update-stay-awake stop || status=1
fi
omarchy_security_exit_with_revoked_sudo "$status"
}
if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
script_command=$(printf '%q ' "$0" "$@")
exec env OMARCHY_UPDATE_LOGGED=1 OMARCHY_UPDATE_USER_PATH="$user_path" script -qefc "$script_command" "/tmp/omarchy-update.log"
fi
if ! omarchy-update-lock held; then
exec env OMARCHY_UPDATE_USER_PATH="$user_path" omarchy-update-lock run "$0" "$@"
fi
trap 'echo ""; echo -e "\033[0;31mSomething went wrong during the update!\n\nPlease review the output above carefully, correct the error, and retry the update.\n\nIf you need assistance, get help from the community at https://omarchy.org/discord\033[0m"' ERR
trap cleanup_update EXIT
omarchy_security_install_signal_exit_traps
omarchy-update-requires-free-space
# -y suppresses Omarchy confirmation prompts; sudo authorization is still
# required. Interactive review steps report and move on instead of waiting.
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
authorize_update
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
# after it frees nothing until that snapshot ages out.
omarchy-update-pkg-prune
# 127 means Snapper is deliberately absent. Any other failure already said
# what went wrong, and a missing snapshot is not worth blocking an update
# over, but it must not pass for one either.
omarchy-snapshot create || (($? == 127)) ||
echo -e "\e[33mContinuing the update without a snapshot.\e[0m" >&2
omarchy-update-stay-awake start
# Preserve the established development-checkout update ordering.
omarchy-update-dev
omarchy-update-keyring
# Migrations ship with the packages installed here and are written against
# them, so everything below waits on this finishing. An upgrade that stopped
# takes the update with it rather than migrating against what is still on disk.
omarchy-update-system-pkgs
omarchy-migrate
omarchy-update-orphan-pkgs
omarchy-update-analyze-logs
omarchy-update-status
# Service restart helpers can need sudo. The reboot-only phase below
# performs no privileged work.
omarchy-update-restart --services-only
# Hooks and mise run with the caller's PATH so user-installed tools resolve.
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
# AUR builds run third-party PKGBUILD code, so they go last and never see the
# update's authorization: revoke it, then authenticate each sudo call with
# the no-update wrapper, for yay and any bare sudo a PKGBUILD runs, so an AUR
# install prompts without caching anything.
end_update_authorization
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-aur-pkgs
omarchy_security_revoke_sudo_timestamp
# The sleep inhibitor covers AUR builds, hooks and mise as well; releasing it
# needs no privilege because the held command already dropped to this user.
# Release it before offering a reboot: a confirmed reboot can terminate this
# process before its EXIT trap gets a chance to remove the persistent Stay
# Awake marker.
omarchy-update-stay-awake stop
update_stay_awake_stopped=1
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
fi