Improve update and migration flow

This commit is contained in:
Ryan Hughes
2026-06-04 18:38:25 -04:00
parent c8ed1e77ae
commit 0804962619
39 changed files with 2068 additions and 84 deletions
+9 -2
View File
@@ -175,10 +175,17 @@ This copies `/etc/skel/.config/hypr/hyprlock.conf` to `~/.config/hypr/hyprlock.c
# Migrations
To create a new migration, run `omarchy-dev-add-migration --no-edit`. This creates a migration file named after the unix timestamp of the last commit.
Read `docs/migrations.md` before creating or changing migrations.
Migrations are split by execution scope:
- `migrations/system/<timestamp>.sh` — root, noninteractive, safe to run from pacman via `omarchy-migrate-system` (`omarchy` `post_upgrade` calls it). Use for `/etc`, `/usr`, `/boot`, services, hardware quirks, and other system state. Do not prompt.
- `migrations/user/<timestamp>.sh` — current user/session, may touch `~/.config`, `~/.local`, user systemd, browser prefs, DBus/session state, and may prompt if necessary. Runs through `omarchy-migrate` / `omarchy-migrate-user`; pending state is per-user based on missing files under `~/.local/state/omarchy/migrations/user/`.
To create a new migration, run `omarchy-dev-add-migration system --no-edit` or `omarchy-dev-add-migration user --no-edit` based on scope.
New migration format:
- File permissions must be `0644` (`-rw-r--r--`); migrations are sourced, not executed directly
- File permissions must be `0644` (`-rw-r--r--`); migration runners execute them with `bash -euo pipefail`, not through executable bits
- No shebang line
- Start with an `echo` describing what the migration does
- Use `$OMARCHY_PATH` to reference the omarchy directory
+19 -2
View File
@@ -1,12 +1,29 @@
#!/bin/bash
# omarchy:summary=Create a new Omarchy migration in the current source tree.
# omarchy:args=<system|user> [--no-edit]
set -euo pipefail
scope="${1:-}"
case "$scope" in
system|user)
shift
;;
-h|--help|"")
echo "Usage: omarchy-dev-add-migration <system|user> [--no-edit]"
exit 0
;;
*)
echo "Unknown migration scope: $scope" >&2
echo "Usage: omarchy-dev-add-migration <system|user> [--no-edit]" >&2
exit 1
;;
esac
cd "${OMARCHY_PATH:-$(pwd)}"
mkdir -p migrations
migration_file="migrations/$(git log -1 --format=%cd --date=unix).sh"
mkdir -p "migrations/$scope"
migration_file="migrations/$scope/$(git log -1 --format=%cd --date=unix).sh"
touch "$migration_file"
if [[ ${1:-} != "--no-edit" ]]; then
+4 -4
View File
@@ -18,7 +18,7 @@ For shipped configs see /etc/skel (new users) and omarchy-reinstall-configs
(existing users explicitly resyncing).
--first-install is used by the ISO in the target chroot. It marks shipped
migrations complete for the freshly-created user.
user migrations complete for the freshly-created user.
Idempotency marker: ~/.local/state/omarchy/finalize-user.done
USAGE
@@ -123,9 +123,9 @@ xdg-settings set default-web-browser chromium.desktop
xdg-mime default HEY.desktop x-scheme-handler/mailto
if (( first_install )); then
mkdir -p "$state_dir/migrations"
for migration in "$OMARCHY_PATH"/migrations/*.sh; do
[[ -f $migration ]] && touch "$state_dir/migrations/$(basename "$migration")"
mkdir -p "$state_dir/migrations/user"
for migration in "$OMARCHY_PATH"/migrations/user/*.sh; do
[[ -f $migration ]] && touch "$state_dir/migrations/user/$(basename "$migration")"
done
fi
+11
View File
@@ -92,6 +92,17 @@ run_first_run_step() {
fi
}
wait_for_notifications
MIGRATION_NOTIFY_WATCH_MARKER="$state_dir/user-migration-notify-watch-enabled"
if [[ ! -f $MIGRATION_NOTIFY_WATCH_MARKER || $force -eq 1 ]]; then
if systemctl --user enable --now omarchy-update-user-notify.path >/dev/null 2>&1; then
touch "$MIGRATION_NOTIFY_WATCH_MARKER"
fi
fi
run_first_run_step "notify about pending user migrations" omarchy-migrate-notify
USER_MARKER="$state_dir/first-run-user.done"
if [[ ! -f $USER_MARKER || $force -eq 1 ]]; then
run_first_run_step "install Voxtype post-update hook" \
+89 -11
View File
@@ -1,19 +1,97 @@
#!/bin/bash
# omarchy:summary=Run pending Omarchy migrations for the current user.
# omarchy:summary=Run pending Omarchy system and user migrations.
# omarchy:args=[--pending [all|system|user]]
set -euo pipefail
STATE_DIR="$HOME/.local/state/omarchy/migrations"
mkdir -p "$STATE_DIR"
mode="run"
pending_scope="all"
for file in "$OMARCHY_PATH"/migrations/*.sh; do
[[ -f $file ]] || continue
filename=$(basename "$file")
usage() {
echo "Usage: omarchy-migrate [--pending [all|system|user]]"
}
if [[ ! -f $STATE_DIR/$filename ]]; then
echo -e "\e[32m\nRunning migration (${filename%.sh})\e[0m"
bash "$file"
touch "$STATE_DIR/$filename"
fi
while (($#)); do
case "$1" in
--pending|--check)
mode="pending"
shift
if [[ ${1:-} == "all" || ${1:-} == "system" || ${1:-} == "user" ]]; then
pending_scope="$1"
shift
fi
;;
--pending=all|--pending=system|--pending=user)
mode="pending"
pending_scope="${1#--pending=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
pending_output=""
append_pending() {
local scope="$1"
local command="$2"
local output=""
local migration=""
if output=$("$command" --pending 2>/dev/null); then
while IFS= read -r migration; do
[[ -n $migration ]] || continue
pending_output+="$scope/$migration"$'\n'
done <<<"$output"
fi
}
if [[ $mode == "pending" ]]; then
case "$pending_scope" in
all)
append_pending system omarchy-migrate-system
append_pending user omarchy-migrate-user
;;
system)
append_pending system omarchy-migrate-system
;;
user)
append_pending user omarchy-migrate-user
;;
esac
if [[ -n $pending_output ]]; then
printf '%s' "$pending_output"
exit 0
else
exit 1
fi
fi
wait_for_pacman_transaction() {
local lock_file=/var/lib/pacman/db.lck
[[ -e $lock_file ]] || return 0
echo "Waiting for pacman transaction to finish before running Omarchy migrations..."
for _ in {1..900}; do
[[ -e $lock_file ]] || return 0
sleep 1
done
echo "Pacman transaction is still running; Omarchy migrations will retry on next login."
exit 0
}
wait_for_pacman_transaction
omarchy-migrate-system
omarchy-migrate-user
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# omarchy:summary=Notify the user when Omarchy has pending user migrations
set -euo pipefail
pending_migrations=$(omarchy-migrate --pending user 2>/dev/null) || exit 0
pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l)
if (( pending_count == 1 )); then
message="1 new Omarchy user migration is available. Click to run it in a terminal."
else
message="$pending_count new Omarchy user migrations are available. Click to run them in a terminal."
fi
notify_command=$(printf 'if [[ -n $(omarchy-notification-send -u critical -g  "Run Omarchy Migrations" %q -a) ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-migrate; fi' "$message")
if command -v systemd-run >/dev/null 2>&1; then
unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)"
systemd-run --user --scope --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0
fi
print_pending_migrations() {
echo "Omarchy has pending user migrations. Run omarchy-migrate in a terminal to apply them:"
while IFS= read -r migration; do
[[ -n $migration ]] || continue
printf ' %s\n' "$migration"
done <<<"$pending_migrations"
}
if [[ -t 1 ]]; then
print_pending_migrations
else
print_pending_migrations >&2
fi
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# omarchy:summary=Run pending Omarchy system migrations as root.
# omarchy:args=[--pending]
# omarchy:requires-sudo=true
set -euo pipefail
mode="run"
while (($#)); do
case "$1" in
--pending|--check)
mode="pending"
shift
;;
-h|--help)
echo "Usage: omarchy-migrate-system [--pending]"
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
STATE_DIR="${OMARCHY_SYSTEM_MIGRATION_STATE:-/var/lib/omarchy/migrations/system}"
MIGRATIONS_DIR="$OMARCHY_PATH/migrations/system"
pending_migrations() {
[[ -d $MIGRATIONS_DIR ]] || return 0
for file in "$MIGRATIONS_DIR"/*.sh; do
[[ -f $file ]] || continue
filename=$(basename "$file")
if [[ ! -f $STATE_DIR/$filename ]]; then
printf '%s\n' "$filename"
fi
done
}
if [[ $mode == "pending" ]]; then
pending_output=$(pending_migrations)
if [[ -n $pending_output ]]; then
printf '%s\n' "$pending_output"
exit 0
else
exit 1
fi
fi
if (( EUID != 0 )) && [[ -z ${OMARCHY_SYSTEM_MIGRATION_STATE:-} ]]; then
if ! "$0" --pending >/dev/null; then
exit 0
fi
exec sudo --preserve-env=OMARCHY_PATH "$0" "$@"
fi
mkdir -p "$STATE_DIR"
[[ -d $MIGRATIONS_DIR ]] || exit 0
for file in "$MIGRATIONS_DIR"/*.sh; do
[[ -f $file ]] || continue
filename=$(basename "$file")
if [[ ! -f $STATE_DIR/$filename ]]; then
echo -e "\e[32m\nRunning system migration (${filename%.sh})\e[0m"
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file"
touch "$STATE_DIR/$filename"
fi
done
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# omarchy:summary=Run pending Omarchy migrations for the current user.
# omarchy:args=[--pending]
set -euo pipefail
mode="run"
while (($#)); do
case "$1" in
--pending|--check)
mode="pending"
shift
;;
-h|--help)
echo "Usage: omarchy-migrate-user [--pending]"
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
STATE_DIR="${OMARCHY_USER_MIGRATION_STATE:-$HOME/.local/state/omarchy/migrations/user}"
MIGRATIONS_DIR="$OMARCHY_PATH/migrations/user"
pending_migrations() {
[[ -d $MIGRATIONS_DIR ]] || return 0
for file in "$MIGRATIONS_DIR"/*.sh; do
[[ -f $file ]] || continue
filename=$(basename "$file")
if [[ ! -f $STATE_DIR/$filename ]]; then
printf '%s\n' "$filename"
fi
done
}
if [[ $mode == "pending" ]]; then
pending_output=$(pending_migrations)
if [[ -n $pending_output ]]; then
printf '%s\n' "$pending_output"
exit 0
else
exit 1
fi
fi
mkdir -p "$STATE_DIR"
[[ -d $MIGRATIONS_DIR ]] || exit 0
for file in "$MIGRATIONS_DIR"/*.sh; do
[[ -f $file ]] || continue
filename=$(basename "$file")
if [[ ! -f $STATE_DIR/$filename ]]; then
echo -e "\e[32m\nRunning user migration (${filename%.sh})\e[0m"
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file"
touch "$STATE_DIR/$filename"
fi
done
+1 -1
View File
@@ -23,4 +23,4 @@ sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirr
omarchy-hook pre-refresh-pacman
# Reset all package DBs and then update
sudo pacman -Syyuu --noconfirm
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm
+2 -2
View File
@@ -11,8 +11,8 @@ set -e
omarchy-refresh-pacman
# Downgrade any packages to the stable setup
sudo pacman -Suu --noconfirm
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Suu --noconfirm
# Ensure all packages are installed
mapfile -t packages < <(grep -v '^#' "$OMARCHY_PATH/install/omarchy-base.packages" | grep -v '^$')
sudo pacman -Syu --noconfirm --needed "${packages[@]}"
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm --needed "${packages[@]}"
+88 -2
View File
@@ -7,14 +7,100 @@
set -e
if [[ -z $OMARCHY_UPDATE_LOGGED ]]; then
if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
script_command=$(printf '%q ' "$0" "$@")
exec env OMARCHY_UPDATE_LOGGED=1 script -qefc "$script_command" "/tmp/omarchy-update.log"
fi
acquire_update_lock() {
local lock_dir="${XDG_RUNTIME_DIR:-/tmp}"
local lock_path="$lock_dir/omarchy-update.lock"
local lock_fd_path=""
mkdir -p "$lock_dir" 2>/dev/null || true
if [[ -n ${OMARCHY_UPDATE_LOCK_FD:-} && -e /proc/$$/fd/$OMARCHY_UPDATE_LOCK_FD ]]; then
lock_fd_path=$(readlink -f "/proc/$$/fd/$OMARCHY_UPDATE_LOCK_FD" 2>/dev/null || true)
if [[ $lock_fd_path == "$(readlink -m "$lock_path")" ]] && flock -n "$OMARCHY_UPDATE_LOCK_FD"; then
return 0
fi
fi
exec {OMARCHY_UPDATE_LOCK_FD}>"$lock_path"
if ! flock -n "$OMARCHY_UPDATE_LOCK_FD"; then
echo "An Omarchy update is already running."
exit 1
fi
export OMARCHY_UPDATE_LOCK_FD
}
update_disabled_idle=0
sleep_inhibit_pid=""
disable_sleep_for_update() {
command -v systemd-inhibit >/dev/null 2>&1 || return 0
systemd-inhibit \
--what=sleep:idle \
--who=omarchy-update \
--why="Omarchy update in progress" \
--mode=block \
sleep infinity >/dev/null 2>&1 &
sleep_inhibit_pid=$!
}
disable_idle_for_update() {
local stay_awake_state="$HOME/.local/state/omarchy/indicators/stay-awake"
if [[ ! -f $stay_awake_state ]]; then
omarchy-toggle-idle stay-awake >/dev/null 2>&1 || true
update_disabled_idle=1
fi
}
restore_update_inhibitors() {
if (( update_disabled_idle )); then
omarchy-toggle-idle allow-idle >/dev/null 2>&1 || true
fi
if [[ -n $sleep_inhibit_pid ]]; then
kill "$sleep_inhibit_pid" >/dev/null 2>&1 || true
wait "$sleep_inhibit_pid" >/dev/null 2>&1 || true
fi
}
run_update_pipeline() {
disable_sleep_for_update
disable_idle_for_update
omarchy-update-keyring
omarchy-update-system-pkgs
omarchy-migrate
omarchy-hook post-update
omarchy-update-aur-pkgs
omarchy-update-mise
omarchy-update-orphan-pkgs
omarchy-update-analyze-logs
# Re-check after updates so the status bar reflects any remaining updates.
# This clears the state when everything is current, so a separate reset command
# is not needed.
if omarchy-update-available >/dev/null; then
omarchy-shell -q omarchy.system-update refresh
else
omarchy-shell -q omarchy.system-update clear
fi
omarchy-update-restart
}
acquire_update_lock
trap restore_update_inhibitors EXIT
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
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
omarchy-snapshot create || (($? == 127))
omarchy-update-perform
run_update_pipeline
fi
+111 -7
View File
@@ -1,15 +1,119 @@
#!/bin/bash
# omarchy:summary=Check whether the Omarchy package has an available update.
# omarchy:summary=Check whether system or AUR packages have available updates.
set -e
set -euo pipefail
if pacman -Qu omarchy >/tmp/omarchy-update-available.$$ 2>/dev/null; then
cat /tmp/omarchy-update-available.$$
rm -f /tmp/omarchy-update-available.$$
CHECK_TIMEOUT="${OMARCHY_UPDATE_CHECK_TIMEOUT:-60}"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/updates"
TMP_PARENT="${TMPDIR:-/tmp}"
mkdir -p "$STATE_DIR"
tmp_dir=$(mktemp -d "$TMP_PARENT/omarchy-update-available.XXXXXX")
trap 'rm -rf "$tmp_dir"' EXIT
packages_raw="$tmp_dir/packages.raw"
packages_tmp="$tmp_dir/packages"
aur_raw="$tmp_dir/aur.raw"
aur_tmp="$tmp_dir/aur"
errors_tmp="$tmp_dir/errors"
packages_file="$STATE_DIR/packages"
aur_file="$STATE_DIR/aur"
available_file="$STATE_DIR/available"
checked_at_file="$STATE_DIR/checked-at"
error_file="$STATE_DIR/error"
: >"$packages_raw"
: >"$packages_tmp"
: >"$aur_raw"
: >"$aur_tmp"
: >"$errors_tmp"
strip_update_output() {
local source="$1"
local destination="$2"
sed -E 's/\x1B\[[0-9;]*m//g' "$source" | sed '/^[[:space:]]*$/d' >"$destination"
}
check_system_packages() {
local status=0
if command -v checkupdates >/dev/null 2>&1 && command -v fakeroot >/dev/null 2>&1; then
CHECKUPDATES_DB="$tmp_dir/checkupdates-db" timeout "$CHECK_TIMEOUT" checkupdates --nocolor >"$packages_raw" 2>"$tmp_dir/checkupdates.err" || status=$?
case "$status" in
0|2)
;;
124)
echo "System package update check timed out" >>"$errors_tmp"
;;
*)
echo "System package update check failed" >>"$errors_tmp"
sed '/^[[:space:]]*$/d' "$tmp_dir/checkupdates.err" >>"$errors_tmp" || true
;;
esac
elif command -v pacman >/dev/null 2>&1; then
# Fallback for systems that have not picked up pacman-contrib/fakeroot yet.
# This only reports updates already known to the local sync database.
pacman -Qu --color never >"$packages_raw" 2>/dev/null || true
else
echo "pacman is not available" >>"$errors_tmp"
fi
strip_update_output "$packages_raw" "$packages_tmp"
}
check_aur_packages() {
local status=0
command -v pacman >/dev/null 2>&1 || return 0
command -v yay >/dev/null 2>&1 || return 0
pacman -Qem >/dev/null 2>&1 || return 0
timeout "$CHECK_TIMEOUT" yay --color never -Qua >"$aur_raw" 2>"$tmp_dir/yay.err" || status=$?
case "$status" in
0|1)
;;
124)
echo "AUR package update check timed out" >>"$errors_tmp"
;;
*)
echo "AUR package update check failed" >>"$errors_tmp"
sed '/^[[:space:]]*$/d' "$tmp_dir/yay.err" >>"$errors_tmp" || true
;;
esac
strip_update_output "$aur_raw" "$aur_tmp"
grep -vw '\[ignored\]$' "$aur_tmp" >"$aur_tmp.filtered" || true
mv "$aur_tmp.filtered" "$aur_tmp"
}
check_system_packages
check_aur_packages
cp "$packages_tmp" "$packages_file"
cp "$aur_tmp" "$aur_file"
cat "$packages_tmp" "$aur_tmp" >"$available_file"
date --iso-8601=seconds >"$checked_at_file"
if [[ -s $errors_tmp ]]; then
cp "$errors_tmp" "$error_file"
else
rm -f "$error_file"
fi
if [[ -s $available_file ]]; then
cat "$available_file"
exit 0
fi
rm -f /tmp/omarchy-update-available.$$
echo "Omarchy is up to date"
if [[ -s $error_file ]]; then
cat "$error_file" >&2
fi
echo "System is up to date"
exit 1
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
# omarchy:summary=Ensure the status bar icon offering the available update is removed
omarchy-shell -q SystemUpdate refresh
exit 0
+23 -7
View File
@@ -1,13 +1,29 @@
#!/bin/bash
# omarchy:summary=Remove orphaned system packages after updates
# omarchy:summary=Review and optionally remove orphaned system packages after updates
# omarchy:requires-sudo=true
orphans=$(pacman -Qtdq || true)
if [[ -n $orphans ]]; then
echo -e "\e[32m\nRemove orphan system packages\e[0m"
for pkg in $orphans; do
sudo pacman -Rs --noconfirm "$pkg" || true
done
set -e
mapfile -t orphans < <(pacman -Qtdq 2>/dev/null || true)
(( ${#orphans[@]} )) || exit 0
echo -e "\e[32m\nOrphan system packages\e[0m"
printf ' %s\n' "${orphans[@]}"
echo
if [[ ! -t 0 || ! -t 1 ]]; then
echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them."
echo
exit 0
fi
if ! gum confirm --default=false "Remove ${#orphans[@]} orphaned package(s)?"; then
echo "Keeping orphaned packages."
echo
exit 0
fi
echo -e "\e[32m\nRemoving orphan system packages\e[0m"
sudo pacman -Rns "${orphans[@]}"
echo
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# omarchy:summary=Prevent direct pacman system upgrades from bypassing omarchy update.
# omarchy:hidden=true
set -euo pipefail
if [[ ${OMARCHY_UPDATE_PACMAN:-} == "1" || ${OMARCHY_ALLOW_DIRECT_PACMAN:-} == "1" ]]; then
exit 0
fi
pacman_args=()
if [[ -n ${OMARCHY_PACMAN_CMDLINE:-} ]]; then
read -r -a pacman_args <<<"$OMARCHY_PACMAN_CMDLINE"
elif [[ -r /proc/$PPID/cmdline ]]; then
mapfile -d '' -t pacman_args <"/proc/$PPID/cmdline" || true
fi
is_system_upgrade=0
has_sync=0
has_sysupgrade=0
for arg in "${pacman_args[@]}"; do
case "$arg" in
--sync)
has_sync=1
;;
--sysupgrade)
has_sysupgrade=1
;;
--*)
;;
-*)
[[ $arg == *S* ]] && has_sync=1
[[ $arg == *u* ]] && has_sysupgrade=1
;;
esac
done
if (( has_sync && has_sysupgrade )); then
is_system_upgrade=1
fi
(( is_system_upgrade )) || exit 0
cat >&2 <<'MESSAGE'
Woah partner...
This looks like a direct pacman system upgrade. Omarchy updates should normally
run through:
omarchy update
That path handles the update transcript, snapshot, keyrings, package upgrade,
migrations, post-update hooks, shell update state, and restart checks together.
If you really meant to bypass Omarchy for this transaction, rerun pacman with:
sudo env OMARCHY_ALLOW_DIRECT_PACMAN=1 pacman -Syu
MESSAGE
exit 1
+3 -22
View File
@@ -1,26 +1,7 @@
#!/bin/bash
# omarchy:summary=Run the full Omarchy update pipeline
# omarchy:summary=Compatibility wrapper for omarchy-update -y.
# omarchy:hidden=true
# omarchy:requires-sudo=true
set -e
# Ensure screensaver/sleep doesn't set in during updates
hyprctl dispatch 'hl.dsp.window.tag({ tag = "+noidle" })' &>/dev/null || hyprctl dispatch tagwindow +noidle &>/dev/null || true
# Perform all update steps
omarchy-update-keyring
omarchy-update-available-reset
omarchy-update-system-pkgs
omarchy-migrate
omarchy-update-aur-pkgs
omarchy-update-mise
omarchy-update-orphan-pkgs
omarchy-hook post-update
omarchy-update-analyze-logs
omarchy-update-restart
# Re-enable screensaver/sleep after updates
hyprctl dispatch 'hl.dsp.window.tag({ tag = "-noidle" })' &>/dev/null || hyprctl dispatch tagwindow -- -noidle &>/dev/null || true
exec omarchy-update -y
+1 -1
View File
@@ -10,7 +10,7 @@ echo -e "\e[32m\nUpdate system packages\e[0m"
# Transition --overwrite: previous script-installed Omarchy wrote these paths
# as unowned files; pacman would refuse the omarchy-settings upgrade on first
# encounter. Drop each entry once the transition release is the baseline.
sudo pacman -Syyu --noconfirm \
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \
--overwrite '/etc/docker/daemon.json' \
--overwrite '/etc/gnupg/dirmngr.conf' \
--overwrite '/etc/modprobe.d/omarchy-usb-autosuspend.conf' \
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# omarchy:summary=Compatibility wrapper for omarchy-migrate-notify.
# omarchy:hidden=true
exec omarchy-migrate-notify "$@"
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
# omarchy:summary=No-op now that omarchy-update-perform is responsible for idle management.
# omarchy:hidden=true
@@ -0,0 +1,11 @@
[Trigger]
Operation = Upgrade
Type = Package
Target = *
[Action]
Description = Checking Omarchy update entrypoint...
When = PreTransaction
Depends = omarchy
Exec = /usr/bin/omarchy-update-pacman-guard
AbortOnFail
@@ -0,0 +1,10 @@
[Unit]
Description=Watch for Omarchy user migrations
[Path]
PathExistsGlob=/usr/share/omarchy/migrations/user/*.sh
PathModified=/usr/share/omarchy/migrations/user
Unit=omarchy-update-user-notify.service
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,7 @@
[Unit]
Description=Notify about pending Omarchy user migrations
ConditionPathExistsGlob=/usr/share/omarchy/migrations/user/*.sh
[Service]
Type=oneshot
ExecStart=/usr/bin/omarchy-migrate-notify
+50 -8
View File
@@ -54,12 +54,16 @@ bin/omarchy-debug,
bin/omarchy-debug-idle,
bin/omarchy-upload-log ──► omarchy-settings /usr/bin/ (needed before omarchy is installed)
default/libalpm/hooks/00-omarchy-update-guard.hook
──► omarchy /usr/share/libalpm/hooks/00-omarchy-update-guard.hook
install/** ──► omarchy /usr/share/omarchy/install/
migrations/** ──► omarchy /usr/share/omarchy/migrations/
migrations/system/** ──► omarchy /usr/share/omarchy/migrations/system/
migrations/user/** ──► omarchy /usr/share/omarchy/migrations/user/
themes/** ──► omarchy /usr/share/omarchy/themes/
shell/** ──► omarchy /usr/share/omarchy/shell/
version ──► omarchy /usr/share/omarchy/version
+ /etc/skel/.local/state/omarchy/migrations/*
+ /etc/skel/.local/state/omarchy/migrations/user/*
config/** ──► omarchy-settings /etc/skel/.config/** (seeds new users)
/usr/share/omarchy/config/** (resync source)
@@ -95,7 +99,7 @@ default/** ──► omarchy-settings /usr/share/omarchy
│ + symlink /etc/fonts/conf.d/30-omarchy.conf
├─ xdg-terminal-exec/*.list /usr/share/xdg-terminal-exec/
├─ applications/mimeapps.list /usr/share/applications/mimeapps.list
├─ systemd/user/*.service /usr/lib/systemd/user/
├─ systemd/user/*.{service,path} /usr/lib/systemd/user/
├─ systemd/system-sleep/unmount-fuse /usr/lib/systemd/system-sleep/
├─ fonts/omarchy/omarchy.ttf /usr/share/fonts/omarchy/
├─ sddm/omarchy/ /usr/share/sddm/themes/omarchy/
@@ -164,7 +168,7 @@ It only does the things `/etc/skel` can't:
- `omarchy-refresh-applications` (composes generated `.desktop` launchers).
- Sources `install/user/all.sh` — theme, git, mise, keyring, per-user
hardware quirks (asus mic/mixer, framework f13 audio, …).
- On `--first-install`, marks every shipped migration as already applied
- On `--first-install`, marks every shipped user migration as already applied
for the freshly-created user.
Idempotency marker: `~/.local/state/omarchy/finalize-user.done`.
@@ -173,6 +177,43 @@ The ISO calls it as `omarchy-finalize-user --force --first-install` in the
target chroot as the install user, after `omarchy-setup-system` has finished
the root-side work.
## Migrations (`omarchy-migrate`)
See [`migrations.md`](migrations.md) for the full migration model, authoring
guidelines, and troubleshooting notes.
Omarchy migrations are split by scope:
- `migrations/system/*.sh` — root, noninteractive, safe to run from pacman.
The `omarchy` package `post_upgrade()` runs `omarchy-migrate-system` after an
`omarchy` package upgrade, so even explicit direct-pacman bypasses still get
system migrations applied. Completion state lives in
`/var/lib/omarchy/migrations/system/`.
- `migrations/user/*.sh` — current user/session, may be interactive, and may
touch `~/.config`, `~/.local`, user systemd, DBus, browser prefs, etc.
Completion state lives in `~/.local/state/omarchy/migrations/user/`.
Package upgrades happen as root, but user migrations must run as each user and
may be interactive. There is no root-owned "user migration required" marker. A
user migration is pending only when a script exists in `migrations/user/` and
that user's matching state file is missing.
Each graphical user has `omarchy-update-user-notify.path` watching the packaged
user migration directory. When that directory changes, or when the path unit is
started on login, `omarchy-update-user-notify.service` runs
`omarchy-migrate-notify` as that user. The notifier checks
`omarchy-migrate --pending user`. If this user has missing migration state, it
shows a notification that opens a terminal for `omarchy-migrate`. The notifier
never runs user migrations in the background.
`omarchy-migrate` waits for any active pacman transaction to finish, runs
pending system migrations, then runs pending user migrations. It does not need
`--force`; migrations happen when state files are missing. For watchers and
diagnostics, `omarchy-migrate --pending [all|system|user]` prints
scope-prefixed pending migration filenames and exits `0` when any are pending.
`omarchy update` runs `omarchy-migrate` after the package transaction in the
already-visible update terminal, then runs `omarchy-hook post-update`.
## First-run (`omarchy-first-run`)
Runs once on first interactive login, after the user manager is live. Used
@@ -183,9 +224,9 @@ systemd instance:
Voxtype post-update hook.
- `install/user/first-run/enable-user-units.sh``systemctl --user enable`
the shipped user units (`bt-agent`, `omarchy-sleep-lock`,
`omarchy-recover-internal-monitor`). Done here, not at finalize, because
`omarchy-recover-internal-monitor`, `omarchy-update-user-notify.path`). Done here, not at finalize, because
the user manager isn't reachable from the ISO chroot; `ConditionPath*`
in the unit files keeps them inert on hardware they don't apply to.
in the unit files keeps services inert when they don't apply.
- `install/user/first-run/gnome-theme.sh`,
`install/user/first-run/gtk-primary-paste.sh` — GNOME/GTK settings that
need the dconf daemon.
@@ -236,10 +277,11 @@ without backup.
| Default file at `~/.config/foo/` | `config/foo/` |
| `/etc/` drop-in we own outright | `etc/` |
| `/etc/` file owned by an upstream package | `default/`, then add to `etc-overrides` in `omarchy-settings` PKGBUILD + scriptlet |
| Package-owned system file (e.g. systemd user service in `/usr/lib`) | `default/`, document the mapping in `default/package-defaults.tsv`, then add the `install -Dm644` line in `omarchy-settings` PKGBUILD |
| Package-owned system file (e.g. systemd user service/path in `/usr/lib`) | `default/`, document the mapping in `default/package-defaults.tsv`, then add the `install -Dm644` line in `omarchy-settings` PKGBUILD |
| Per-user file that's static but lives outside `~/.config` | `default/`, then add `install -Dm644 ... $pkgdir/etc/skel/...` in `omarchy-settings` PKGBUILD |
| Runtime tweak that needs `$HOME` or live system state | extend `omarchy-finalize-user`, or add a per-user leaf under `install/user/` and wire into `install/user/all.sh` |
| One-time root-side setup step | `install/config/*.sh` or `install/hardware/*.sh`, wire into `omarchy-setup-system` or `install/hardware/all.sh` |
| Noninteractive root fix for existing installs | `migrations/system/<unix-timestamp>.sh` |
| User/session fix for existing installs | `migrations/user/<unix-timestamp>.sh` |
| User-facing `omarchy-*` command | `bin/omarchy-<group>-<verb>` — see `GROUP_DESCRIPTIONS` in `bin/omarchy` |
| New theme | `themes/<name>/` (+ matching templates under `default/themed/` if they need theme colors) |
| One-shot fix for installed systems | `migrations/<unix-timestamp>.sh` (use `omarchy-dev-add-migration --no-edit`) |
+307
View File
@@ -0,0 +1,307 @@
# Omarchy migrations
Omarchy migrations are one-time repair scripts for existing installs. They are
used when a package update needs to change state that pacman cannot safely own by
itself.
They are for both humans and agents:
- Users should know when migrations run, where state is stored, and how to retry
or inspect pending work.
- Agents should know which migration scope to use, how to write safe migration
scripts, and what should **not** be a migration.
## The two migration scopes
Omarchy migrations are split by execution context:
```text
migrations/system/*.sh
migrations/user/*.sh
```
### System migrations
System migrations run as root and must be noninteractive.
Use system migrations for machine-wide state, for example:
- `/etc` drop-ins or legacy config cleanup
- `/boot` / bootloader cleanup
- systemd system units or service state
- hardware quirks
- package-owned layout transitions
State lives in:
```text
/var/lib/omarchy/migrations/system/<migration filename>
```
### User migrations
User migrations run as the current graphical/login user and may be interactive.
Use user migrations for per-user or session-aware state, for example:
- `~/.config` changes
- `~/.local` state
- user systemd units
- browser/editor preferences
- DBus/session-dependent updates
- prompts that need to happen in a visible terminal
State lives in:
```text
~/.local/state/omarchy/migrations/user/<migration filename>
```
A user migration is pending for a given user only when the script exists under
`/usr/share/omarchy/migrations/user/` and that user's matching state file is
missing.
There is no global root-owned “user migrations required” queue.
## When migrations run
### During `omarchy update`
`omarchy update` is the normal update path. It runs package updates, then:
```bash
omarchy-migrate
omarchy-hook post-update
```
`omarchy-migrate`:
1. waits for any active pacman transaction to finish
2. runs pending system migrations
3. runs pending user migrations for the current user
System migrations usually already ran during the pacman transaction via the
`omarchy` package `post_upgrade()`, but the second check is intentional and
idempotent.
### During direct pacman updates
Raw `sudo pacman -Syu` is guarded. Users should normally run:
```bash
omarchy update
```
If a user explicitly bypasses the guard, the `omarchy` package still runs system
migrations from `post_upgrade()`:
```bash
omarchy-migrate-system
```
User migrations are never run by pacman. Instead, the user session watches the
packaged user migration directory and runs a notifier. The notifier checks:
```bash
omarchy-migrate --pending user
```
If that user has pending user migrations, it shows a notification that opens a
terminal for:
```bash
omarchy-migrate
```
The notifier never runs user migrations silently in the background.
### Manually
Users can safely run:
```bash
omarchy-migrate
```
at any time. Already-completed migrations are skipped.
## Inspecting pending migrations
Use:
```bash
omarchy-migrate --pending
omarchy-migrate --pending system
omarchy-migrate --pending user
```
Exit behavior:
- `0` — one or more matching migrations are pending
- non-zero — no matching migrations are pending
Output is scope-prefixed:
```text
system/100-system.sh
user/200-user.sh
```
The lower-level runners also support `--pending` and print filenames without the
scope prefix:
```bash
omarchy-migrate-system --pending
omarchy-migrate-user --pending
```
Prefer `omarchy-migrate --pending ...` in user-facing tools and watchers.
## Writing migrations
Create a migration with:
```bash
omarchy-dev-add-migration system --no-edit
omarchy-dev-add-migration user --no-edit
```
Choose the scope based on the state being changed, not on convenience.
### File format
Migrations are plain shell files:
- filename: unix timestamp, generated by `omarchy-dev-add-migration`
- mode: `0644`
- no shebang
- start with an `echo` describing the migration
- use `$OMARCHY_PATH` for Omarchy-owned files
Example system migration:
```bash
echo "Remove legacy Limine config"
rm -f /boot/EFI/limine/limine.conf
```
Example user migration:
```bash
echo "Refresh user app launchers"
omarchy-refresh-applications
```
### Execution model
Migration runners execute scripts with strict shell settings:
```bash
bash -euo pipefail <migration>
```
`$OMARCHY_PATH` is exported into the migration process.
A migration is marked complete only after the script exits successfully. If it
fails, the marker is not written and the migration will be retried later.
Because failed migrations can be retried after doing partial work, migrations
must be idempotent. Prefer operations that are safe to run more than once:
```bash
mkdir -p ~/.config/example
rm -f ~/.config/example/legacy.conf
install -Dm644 "$OMARCHY_PATH/default/example.conf" ~/.config/example/example.conf
systemctl --user enable --now some-unit.service || true
```
Use `|| true` only when a failure is genuinely acceptable.
## What belongs in a migration?
Good migration uses:
- deleting or moving legacy files that block the packaged layout
- marking or enabling new service state
- migrating old config paths to new config paths
- repairing known bad state from an earlier Omarchy release
- one-time compatibility for existing installs
Bad migration uses:
- fresh-install setup that belongs in `install/` or `/etc/skel`
- package installation that belongs in package dependencies or package lists
- recurring maintenance
- arbitrary cleanup that should be a user command
- pre-4 upgrade work that belongs in `omarchy-upgrade-to-4`
- hidden user/session work from pacman
## Fresh installs and new users
Fresh users created from the packaged layout should not have to run historical
user migrations. The package seeds user migration markers into `/etc/skel` so
new users start with shipped user migrations marked complete.
The ISO/finalization path also marks shipped user migrations complete for the
freshly-created install user when running:
```bash
omarchy-finalize-user --first-install
```
Existing users keep their own migration state and run only migrations whose
state files are missing.
## Troubleshooting
### See what is pending
```bash
omarchy-migrate --pending
```
### Retry failed migrations
Fix the underlying problem, then run:
```bash
omarchy-migrate
```
A failed migration is not marked complete, so it will retry.
### Re-run a completed migration manually
Remove its marker, then run the migration command again:
```bash
rm ~/.local/state/omarchy/migrations/user/<migration>.sh
omarchy-migrate
```
For system migrations:
```bash
sudo rm /var/lib/omarchy/migrations/system/<migration>.sh
omarchy-migrate
```
Be careful: migrations should be idempotent, but manually removing markers is an
advanced troubleshooting step.
## Agent checklist
Before adding a migration:
1. Is this a one-time repair for existing installs?
2. Is it system state or user/session state?
3. Can it run safely more than once?
4. Will it fail loudly if the important work fails?
5. Does it avoid hidden interactive work from pacman?
6. Does it belong in `omarchy-upgrade-to-4` instead?
7. Did you add or update tests if behavior changed?
If the answer to any of these is unclear, stop and ask before adding the
migration.
+318
View File
@@ -0,0 +1,318 @@
# Omarchy update process
This document describes the intended update behavior now that Omarchy is
package-backed. It covers the blessed update path plus what happens when a user attempts to
bypass it:
1. `omarchy update` — the blessed interactive Omarchy update flow.
2. `sudo pacman -Syu` — guarded by Omarchy and aborted with instructions unless
the user explicitly bypasses the guard.
The design goal is:
- System/root migrations should run automatically from package transactions when
safe.
- User/session migrations should never run invisibly from pacman because they may
need `$HOME`, DBus/session state, a graphical session, or user interaction.
- Users who bypass `omarchy update` should still get system migrations from
pacman and should be notified only when their own user migration state is
missing a shipped user migration.
## State and coordination files
| Path | Owner | Purpose |
| --- | --- | --- |
| `${XDG_RUNTIME_DIR:-/tmp}/omarchy-update.lock` | user | Prevent overlapping update runs. Owned by `omarchy-update`; compatibility wrappers inherit/respect it. |
| `/tmp/omarchy-update.log` | user | Transcript of `omarchy update`, used by `omarchy-update-analyze-logs`. |
| `~/.local/state/omarchy/updates/` | user | Update-check state for the shell widget (`packages`, `aur`, `available`, `checked-at`, `error`). |
| `/var/lib/omarchy/migrations/system/` | root | System migration markers. |
| `~/.local/state/omarchy/migrations/user/` | user | User migration markers. |
| `~/.local/state/omarchy/reboot-required` | user | Optional reboot marker checked by `omarchy-update-restart`. |
| `~/.local/state/omarchy/restart-*-required` | user | Optional service/app restart markers checked by `omarchy-update-restart`. |
## Migration layout
See [`migrations.md`](migrations.md) for the full migration model, authoring
guidelines, and troubleshooting notes.
Migrations are scoped by directory:
```text
migrations/system/*.sh
migrations/user/*.sh
```
### System migrations
System migrations are root-owned, noninteractive fixes for existing installs.
They may touch `/etc`, `/usr`, `/boot`, system services, hardware configuration,
and other machine-wide state.
Runner:
```bash
omarchy-migrate-system
```
Completion state:
```text
/var/lib/omarchy/migrations/system/<migration filename>
```
Pacman integration:
```text
pkgbuilds/omarchy/omarchy.install post_upgrade()
```
The `omarchy` package calls `omarchy-migrate-system` from `post_upgrade()`.
The runner is idempotent, so repeated calls only check the state files.
### User migrations
User migrations are current-user/session fixes. They may touch `~/.config`,
`~/.local`, user systemd units, browser/editor preferences, DBus/session state,
or ask questions.
Runner:
```bash
omarchy-migrate-user
```
Public command:
```bash
omarchy-migrate
```
Completion state:
```text
~/.local/state/omarchy/migrations/user/<migration filename>
```
A user migration is pending only when a script exists in `migrations/user/` and
that user's matching state file is missing. There is no global root-owned queue
for user migrations.
`omarchy-migrate` is the public command. It waits for any active pacman
transaction to finish, runs pending system migrations, then runs pending user
migrations. It does not need `--force`; migrations should happen when they are
pending.
For watchers and diagnostics, `omarchy-migrate --pending [all|system|user]`
prints pending migration names and exits `0` when any are pending. Output is
scope-prefixed, for example:
```text
system/100-system.sh
user/200-user.sh
```
When no matching migrations are pending, it prints nothing and exits non-zero.
## Raw pacman guard
The `omarchy` package installs an ALPM pre-transaction hook alongside its guard
binary:
```text
/usr/share/libalpm/hooks/00-omarchy-update-guard.hook
/usr/bin/omarchy-update-pacman-guard
```
It triggers on package upgrades and runs:
```bash
omarchy-update-pacman-guard
```
The guard detects direct pacman system-upgrade commands like `pacman -Syu` or
`pacman --sync --refresh --sysupgrade`. If the upgrade was not launched by an
Omarchy update command, the hook exits non-zero with `AbortOnFail`, which stops
the transaction before packages are changed.
`omarchy-update-system-pkgs`, `omarchy-refresh-pacman`, `omarchy-reinstall-pkgs`,
and the v4 upgrader run pacman through:
```bash
env OMARCHY_UPDATE_PACMAN=1 pacman ...
```
so the guard allows Omarchy-owned update flows. A user can intentionally bypass
the guard with:
```bash
sudo env OMARCHY_ALLOW_DIRECT_PACMAN=1 pacman -Syu
```
The guard does not start `omarchy update` itself because pacman is already in a
transaction setup path; it only aborts with instructions.
## Path 1: `omarchy update`
High-level flow:
```text
omarchy-update
├─ ensure transcript logging through script(1) → /tmp/omarchy-update.log
├─ acquire update lock
├─ confirm unless -y
├─ create snapper snapshot, if snapper is installed
└─ run update pipeline
├─ block system sleep and temporarily enable shell stay-awake mode
├─ omarchy-update-keyring
├─ omarchy-update-system-pkgs
├─ omarchy-migrate
├─ omarchy-hook post-update
├─ omarchy-update-aur-pkgs
├─ omarchy-update-mise
├─ omarchy-update-orphan-pkgs
├─ omarchy-update-analyze-logs
├─ omarchy-update-available, then refresh/clear shell indicator
├─ omarchy-update-restart
└─ release sleep inhibitor and restore shell idle state, if changed
```
Important behavior:
- `omarchy update` checks/runs system and user migrations in the same visible
terminal via `omarchy-migrate`.
- `omarchy update` still benefits from pacman-triggered system migrations
because the system migration hook runs as part of the pacman transaction.
- A failure should leave enough output in `/tmp/omarchy-update.log` and the
terminal transcript to debug.
## Path 2: direct `sudo pacman -Syu` attempt
High-level flow:
```text
sudo pacman -Syu
├─ pre-transaction guard aborts and tells the user to run omarchy update
└─ if explicitly bypassed, upgrades omarchy and related packages
├─ omarchy post_upgrade runs omarchy-migrate-system as root
└─ user session notices migration directory changes
├─ omarchy-update-user-notify.path triggers, if enabled
├─ omarchy-migrate-notify checks omarchy-migrate --pending user
├─ if this user has missing migration state, show notification
└─ click opens terminal: omarchy-migrate
```
Fallbacks:
- `omarchy-first-run` enables the user notification path unit.
- `omarchy-first-run` also invokes `omarchy-migrate-notify` on graphical
startup, so users who updated before the path unit existed still get prompted
if they have missing user migration state.
- The notifier is only a prompt. It does not run user migrations in the
background.
- Direct pacman updates do not run `omarchy-hook post-update` unless the user
explicitly runs that hook; without a package-update marker, the only user-side
pending state we can derive is missing user migration markers.
## Shell update indicator
The bar widget `omarchy.system-update` runs:
```bash
omarchy-update-available
```
`omarchy-update-available` uses `checkupdates` with a temporary database when
available, plus `yay -Qua` for AUR updates when foreign packages are installed.
It stores the result in:
```text
~/.local/state/omarchy/updates/packages
~/.local/state/omarchy/updates/aur
~/.local/state/omarchy/updates/available
~/.local/state/omarchy/updates/checked-at
~/.local/state/omarchy/updates/error
```
Exit codes:
- `0` — updates are available; stdout is the update list.
- non-zero — no updates are available; stdout says the system is up to date.
The widget uses the line count to show/hide itself. It also watches the
`available` state file, so manually running `omarchy-update-available` updates
the widget as soon as the file changes. Hovering the update icon opens a panel
that lists Omarchy packages first, then all other package updates.
## Update-related binaries
This inventory is intentionally opinionated. Some commands are useful as stable
leaf commands; others exist mostly because the old update flow accreted small
scripts.
| Binary | Current purpose | Keep? / Question |
| --- | --- | --- |
| `omarchy-update` | Public user command. Adds transcript logging, lock, confirmation, snapshot, sleep/idle inhibitors, package updates, migrations, hooks, update-state refresh, and restart checks. | **Keep.** This is the blessed entry point and owns the update pipeline. |
| `omarchy-update-perform` | Hidden compatibility wrapper for `omarchy-update -y`. | **Temporary.** Keep only for old callers; new code should call `omarchy-update` directly. |
| `omarchy-update-confirm` | Gum confirmation copy for `omarchy update`. | **Question.** Could be inlined into `omarchy-update`; separate file only helps keep copy isolated. |
| `omarchy-update-keyring` | Ensures Omarchy keyring and Arch keyring are current before the main transaction. | **Keep, but review.** It uses targeted `pacman -Sy` for keyring bootstrapping; acceptable for this special case but should remain tightly scoped. |
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with targeted transition `--overwrite` entries so the ALPM guard allows the transaction and early package-layout conflicts are handled. | **Keep for now.** Small leaf command, clear/testable. |
| `omarchy-migrate-system` | Runs root/system migrations from `migrations/system`. Called by `omarchy` package `post_upgrade()` and by `omarchy-migrate` when system migration state is missing. | **Keep.** This is the important direct-`pacman -Syu` integration. |
| `omarchy-migrate-user` | Checks `migrations/user` against this user's state and runs only missing user migrations. Supports `--pending` and prints pending filenames. | **Keep internal.** Public users should generally run `omarchy-migrate`. |
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs pending system and user migrations. Supports `--pending [all|system|user]` and prints scope-prefixed pending filenames. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
| `omarchy-update-pacman-guard` | ALPM pre-transaction guard that aborts direct `pacman -Syu` style upgrades unless Omarchy set `OMARCHY_UPDATE_PACMAN=1` or the user explicitly set `OMARCHY_ALLOW_DIRECT_PACMAN=1`. | **Keep internal/hidden.** This is what nudges users back to `omarchy update`. |
| `omarchy-migrate-notify` | Internal notification helper for direct pacman updates. Uses `omarchy-migrate --pending user` and shows notification only when this user has pending migrations. | **Keep internal/hidden.** Clear name now that the public command is `omarchy-migrate`. |
| `omarchy-update-user-notify` | Hidden compatibility wrapper for `omarchy-migrate-notify`. | **Temporary.** Keep only for old callers. |
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. Writes update state. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
| `omarchy-update-mise` | Runs `mise up` for mise-managed tools. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
| `omarchy-update-restart` | Prompts for reboot after kernel/Hyprland updates and restarts components with `restart-*-required` markers. | **Keep.** Important final step; may eventually include service-restart checks. |
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
| `omarchy-update-time` | Restarts `systemd-timesyncd`. | **Question.** Not really an update command. Consider renaming/moving under system/time maintenance. |
## Closed decisions
1. **System migrations run from the `omarchy` package**
- `omarchy.install post_upgrade()` calls `omarchy-migrate-system`.
- We do not need a separate system-migration ALPM hook from
`omarchy-settings`.
2. **Root vs user migrations stay separate**
- Do not use `su {user}` from pacman to run user migrations.
- User migrations need the user's real session and should be visible.
- Pending user work is determined only by comparing `migrations/user/*.sh`
against `~/.local/state/omarchy/migrations/user/`.
3. **Migration notification naming**
- The real helper is `omarchy-migrate-notify`.
- `omarchy-update-user-notify` remains only as a hidden compatibility wrapper.
4. **Update pipeline ownership**
- `omarchy-update` owns the full update pipeline now.
- `omarchy-update-perform` is only a hidden compatibility wrapper for
`omarchy-update -y`.
5. **Mise remains in the blessed update path**
- `omarchy-update-mise` intentionally runs as part of `omarchy update`.
6. **Orphan cleanup stays in the update path for now**
- It is prompt-only and never removes packages noninteractively.
7. **Direct pacman user follow-up is based on actual migration state**
- Direct `sudo pacman -Syu` no longer uses a fake user-update marker.
- User notifications are shown only when `omarchy-migrate --pending user`
finds missing per-user migration state.
## Remaining concerns
1. **Pacman guard scope**
- The guard detects direct pacman sysupgrade invocations and allows Omarchy
commands that set `OMARCHY_UPDATE_PACMAN=1`.
- We may regret blocking some legitimate package-manager frontends or
maintenance flows. Keep an eye on what should be allowed versus redirected
to `omarchy update`.
2. **Pacnew/pacsave handling is still missing**
- Package-backed Omarchy should warn about or help process `.pacnew` and
`.pacsave` files after updates.
+2
View File
@@ -30,6 +30,7 @@ evince
exfatprogs
expac
eza
fakeroot
fastfetch
fcitx5
fcitx5-gtk
@@ -91,6 +92,7 @@ nvim
obs-studio
obsidian
omarchy-nvim
pacman-contrib
pamixer
pinta
plocate
+2 -1
View File
@@ -14,4 +14,5 @@ set -euo pipefail
systemctl --user enable --now \
bt-agent.service \
omarchy-recover-internal-monitor.service \
omarchy-sleep-lock.service
omarchy-sleep-lock.service \
omarchy-update-user-notify.path
View File
+214 -3
View File
@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
@@ -9,11 +10,86 @@ BarWidget {
moduleName: "omarchy.system-update"
property bool updateAvailable: false
property int updateCount: 0
property string updateOutput: ""
property var updateLines: []
property var omarchyUpdateLines: []
property var otherUpdateLines: []
property bool popupOpen: false
property bool buttonHovered: false
property bool popupHovered: popup.containsMouse
readonly property string stateHome: Quickshell.env("XDG_STATE_HOME") || (Quickshell.env("HOME") + "/.local/state")
readonly property string availableStatePath: stateHome + "/omarchy/updates/available"
function close() { popupOpen = false }
function refresh() {
updateOutput = ""
if (!updateProc.running) updateProc.running = true
}
function clear() {
updateOutput = ""
updateLines = []
omarchyUpdateLines = []
otherUpdateLines = []
updateCount = 0
updateAvailable = false
popupOpen = false
}
function packageName(line) {
return String(line || "").trim().split(/\s+/)[0] || ""
}
function isOmarchyPackage(line) {
var pkg = packageName(line)
return pkg === "omarchy" || pkg.indexOf("omarchy-") === 0
}
function countLabel(count) {
return count === 1 ? "1 update" : count + " updates"
}
function parseUpdateText(text) {
var lines = String(text || "").split(/\r?\n/).filter(function(line) {
return line.trim().length > 0
})
var omarchyLines = []
var otherLines = []
for (var i = 0; i < lines.length; i++) {
if (isOmarchyPackage(lines[i])) omarchyLines.push(lines[i])
else otherLines.push(lines[i])
}
omarchyUpdateLines = omarchyLines
otherUpdateLines = otherLines
updateLines = omarchyLines.concat(otherLines)
updateCount = updateLines.length
updateAvailable = updateCount > 0
if (!updateAvailable) popupOpen = false
}
function applyUpdateOutput(exitCode) {
var output = String(updateStdout.text || updateOutput || "")
parseUpdateText(exitCode === 0 ? output : "")
}
function showPopup() {
hideTimer.stop()
if (updateAvailable) popupOpen = true
}
function scheduleHide() {
hideTimer.restart()
}
onButtonHoveredChanged: buttonHovered ? showPopup() : scheduleHide()
onPopupHoveredChanged: popupHovered ? hideTimer.stop() : scheduleHide()
onUpdateAvailableChanged: if (!updateAvailable) popupOpen = false
visible: updateAvailable
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
@@ -24,17 +100,31 @@ BarWidget {
function refresh(): void {
root.refresh()
}
function clear(): void {
root.clear()
}
}
Process {
id: updateProc
command: ["bash", "-lc", "omarchy-update-available"]
stdout: StdioCollector { waitForEnd: true }
stdout: StdioCollector { id: updateStdout; waitForEnd: true; onStreamFinished: root.updateOutput = text }
onExited: function(exitCode) {
root.updateAvailable = exitCode === 0
root.applyUpdateOutput(exitCode)
}
}
FileView {
id: availableState
path: root.availableStatePath
watchChanges: true
printErrors: false
onLoaded: root.parseUpdateText(text())
onLoadFailed: root.clear()
onFileChanged: reload()
}
Timer {
interval: 21600000
running: true
@@ -43,13 +133,134 @@ BarWidget {
onTriggered: root.refresh()
}
Timer {
id: hideTimer
interval: 220
onTriggered: {
if (!root.buttonHovered && !root.popupHovered) root.popupOpen = false
}
}
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.updateAvailable ? "\uf021" : ""
fontSize: Style.font.caption
tooltipText: root.updateAvailable ? "Omarchy update available" : ""
tooltipText: ""
onPressed: function() { root.bar.run("omarchy-launch-floating-terminal-with-presentation omarchy-update") }
}
HoverHandler {
id: hoverHandler
target: button
onHoveredChanged: root.buttonHovered = hovered
}
PopupCard {
id: popup
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen && root.updateAvailable
triggerMode: "hover"
contentWidth: popup.fittedContentWidth(Style.space(460))
contentHeight: popup.fittedContentHeight(panelColumn.implicitHeight, Style.space(520))
Flickable {
id: updateFlick
anchors.fill: parent
contentWidth: width
contentHeight: panelColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
Column {
id: panelColumn
width: updateFlick.width
spacing: Style.space(12)
Row {
width: parent.width
spacing: Style.space(8)
Text {
text: "\uf021"
color: root.bar ? root.bar.urgent : Color.urgent
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.title
verticalAlignment: Text.AlignVCenter
}
Column {
width: parent.width - Style.space(28)
spacing: Style.space(2)
Text {
width: parent.width
text: countLabel(root.updateCount) + " available"
color: Color.popups.text
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
Text {
width: parent.width
text: "Click to run omarchy update"
color: Qt.darker(Color.popups.text, 1.35)
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
}
UpdateSection {
title: "Omarchy"
lines: root.omarchyUpdateLines
width: parent.width
}
UpdateSection {
title: "Other packages"
lines: root.otherUpdateLines
width: parent.width
}
}
}
}
component UpdateSection: Column {
id: section
property string title: ""
property var lines: []
visible: lines.length > 0
spacing: Style.space(6)
Text {
width: section.width
text: section.title
color: root.bar ? root.bar.foreground : Color.foreground
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Repeater {
model: section.lines
delegate: Text {
width: section.width
text: modelData
color: Color.popups.text
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WrapAnywhere
}
}
}
}
+4 -1
View File
@@ -177,7 +177,10 @@ assert_output_contains "theme set help names binary" "$output" "omarchy-theme-se
output=$(timeout 5 "$CLI" update --help)
assert_output_contains "mutating command help does not execute target" "$output" "omarchy-update"
assert_output_contains "root command help shows related child commands" "$output" "omarchy update perform"
if [[ $output == *"omarchy update perform"* ]]; then
fail "hidden update perform compatibility wrapper is not shown"
fi
pass "hidden update perform compatibility wrapper is not shown"
output=$("$CLI" screenshot --help)
assert_output_contains "root alias resolves to command help" "$output" "omarchy-capture-screenshot"
+10
View File
@@ -94,6 +94,7 @@ from pathlib import Path
root = Path(os.environ["ROOT"])
pkgbuild = (root.parent / "omarchy-pkgs/pkgbuilds/omarchy-settings/PKGBUILD").read_text()
omarchy_pkgbuild = (root.parent / "omarchy-pkgs/pkgbuilds/omarchy/PKGBUILD").read_text()
errors = []
package_defaults = [
("default/uwsm/env.d/10-omarchy", "/usr/share/uwsm/env.d/10-omarchy", "uwsm/env"),
@@ -105,6 +106,8 @@ package_defaults = [
("default/systemd/user/bt-agent.service", "/usr/lib/systemd/user/bt-agent.service", "systemd/user/bt-agent.service"),
("default/systemd/user/omarchy-sleep-lock.service", "/usr/lib/systemd/user/omarchy-sleep-lock.service", "systemd/user/omarchy-sleep-lock.service"),
("default/systemd/user/omarchy-recover-internal-monitor.service", "/usr/lib/systemd/user/omarchy-recover-internal-monitor.service", "systemd/user/omarchy-recover-internal-monitor.service"),
("default/systemd/user/omarchy-update-user-notify.service", "/usr/lib/systemd/user/omarchy-update-user-notify.service", "systemd/user/omarchy-update-user-notify.service"),
("default/systemd/user/omarchy-update-user-notify.path", "/usr/lib/systemd/user/omarchy-update-user-notify.path", "systemd/user/omarchy-update-user-notify.path"),
("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
]
@@ -116,6 +119,13 @@ for source, destination, legacy in package_defaults:
if destination and (source not in pkgbuild or destination not in pkgbuild):
errors.append(f"PKGBUILD does not explicitly install {source} -> {destination}")
guard_hook = "default/libalpm/hooks/00-omarchy-update-guard.hook"
guard_destination = "/usr/share/libalpm/hooks/00-omarchy-update-guard.hook"
if not (root / guard_hook).exists():
errors.append(f"missing package default source: {guard_hook}")
if guard_hook not in omarchy_pkgbuild or guard_destination not in omarchy_pkgbuild:
errors.append(f"omarchy PKGBUILD does not install {guard_hook} -> {guard_destination}")
if errors:
print("\n".join(errors), file=sys.stderr)
sys.exit(1)
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mkdir -p "$stub_bin" "$test_home"
cat >"$stub_bin/omarchy-migrate-user" <<'SH'
#!/bin/bash
if [[ ${1:-} == "--pending" && ${OMARCHY_TEST_PENDING_MIGRATIONS:-0} == 1 ]]; then
echo 200-user.sh
exit 0
else
exit 1
fi
SH
chmod +x "$stub_bin/omarchy-migrate-user"
cat >"$stub_bin/systemd-run" <<'SH'
#!/bin/bash
exit 1
SH
chmod +x "$stub_bin/systemd-run"
run_notify() {
HOME="$test_home" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
OMARCHY_TEST_PENDING_MIGRATIONS="$1" \
"$ROOT/bin/omarchy-migrate-notify"
}
run_notify 0 >"$test_tmp/not-pending.out" 2>"$test_tmp/not-pending.err"
[[ ! -s $test_tmp/not-pending.out ]] || fail "migration notifier stays quiet on stdout without pending migrations"
[[ ! -s $test_tmp/not-pending.err ]] || fail "migration notifier stays quiet on stderr without pending migrations"
pass "migration notifier ignores users with no pending migrations"
run_notify 1 >"$test_tmp/pending.out" 2>"$test_tmp/pending.err"
grep -q 'Omarchy has pending user migrations' "$test_tmp/pending.err" || fail "migration notifier explains pending migrations without notification system"
grep -q 'user/200-user.sh' "$test_tmp/pending.err" || fail "migration notifier lists pending migration names"
pass "migration notifier reports pending user migrations"
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
test_root="$test_tmp/omarchy"
test_home="$test_tmp/home"
system_state="$test_tmp/system-state"
mkdir -p "$test_root/migrations/system" "$test_root/migrations/user" "$test_home" "$system_state"
cat >"$test_root/migrations/system/100-system.sh" <<'SH'
[[ $OMARCHY_PATH == "$TEST_EXPECTED_OMARCHY_PATH" ]]
echo system >>"$TEST_CALLS"
SH
cat >"$test_root/migrations/user/200-user.sh" <<'SH'
[[ $OMARCHY_PATH == "$TEST_EXPECTED_OMARCHY_PATH" ]]
echo user >>"$TEST_CALLS"
SH
calls="$test_tmp/calls"
if ! HOME="$test_home" OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-migrate-user" --pending >"$test_tmp/user-pending.out"; then
fail "user migration runner reports pending migrations before state exists"
fi
grep -q '^200-user\.sh$' "$test_tmp/user-pending.out" || fail "user migration runner lists pending migration filename"
pass "user migration runner detects pending migrations"
HOME="$test_home" \
OMARCHY_PATH="$test_root" \
TEST_EXPECTED_OMARCHY_PATH="$test_root" \
TEST_CALLS="$calls" \
"$ROOT/bin/omarchy-migrate-user" >"$test_tmp/user-first.out"
[[ $(grep -c '^user$' "$calls") -eq 1 ]] || fail "user migration runner runs user migrations"
! grep -q '^system$' "$calls" || fail "user migration runner does not run system migrations"
[[ -f $test_home/.local/state/omarchy/migrations/user/200-user.sh ]] || fail "user migration runner records user migration marker"
pass "user migration runner only runs user migrations"
HOME="$test_home" \
OMARCHY_PATH="$test_root" \
TEST_EXPECTED_OMARCHY_PATH="$test_root" \
TEST_CALLS="$calls" \
"$ROOT/bin/omarchy-migrate-user" >"$test_tmp/user-second.out"
[[ $(grep -c '^user$' "$calls") -eq 1 ]] || fail "user migration runner skips completed migrations"
pass "user migration runner skips completed migrations"
if HOME="$test_home" OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-migrate-user" --pending >"$test_tmp/user-not-pending.out"; then
fail "user migration runner reports no pending migrations after state exists"
fi
pass "user migration runner detects no pending migrations"
if ! OMARCHY_PATH="$test_root" OMARCHY_SYSTEM_MIGRATION_STATE="$system_state" "$ROOT/bin/omarchy-migrate-system" --pending >"$test_tmp/system-pending.out"; then
fail "system migration runner reports pending migrations before state exists"
fi
grep -q '^100-system\.sh$' "$test_tmp/system-pending.out" || fail "system migration runner lists pending migration filename"
OMARCHY_PATH="$test_root" \
OMARCHY_SYSTEM_MIGRATION_STATE="$system_state" \
TEST_EXPECTED_OMARCHY_PATH="$test_root" \
TEST_CALLS="$calls" \
"$ROOT/bin/omarchy-migrate-system" >"$test_tmp/system-first.out"
[[ $(grep -c '^system$' "$calls") -eq 1 ]] || fail "system migration runner runs system migrations"
[[ -f $system_state/100-system.sh ]] || fail "system migration runner records system migration marker"
pass "system migration runner only runs system migrations"
if OMARCHY_PATH="$test_root" OMARCHY_SYSTEM_MIGRATION_STATE="$system_state" "$ROOT/bin/omarchy-migrate-system" --pending >"$test_tmp/system-not-pending.out"; then
fail "system migration runner reports no pending migrations after state exists"
fi
pass "system migration runner detects no pending migrations"
failure_root="$test_tmp/failure-omarchy"
failure_home="$test_tmp/failure-home"
failure_system_state="$test_tmp/failure-system-state"
mkdir -p "$failure_root/migrations/system" "$failure_root/migrations/user" "$failure_home" "$failure_system_state"
cat >"$failure_root/migrations/user/300-fail-user.sh" <<'SH'
echo user-before-fail >>"$TEST_CALLS"
false
echo user-after-fail >>"$TEST_CALLS"
SH
set +e
HOME="$failure_home" \
OMARCHY_PATH="$failure_root" \
TEST_CALLS="$calls" \
"$ROOT/bin/omarchy-migrate-user" >"$test_tmp/user-failure.out" 2>"$test_tmp/user-failure.err"
user_failure_status=$?
set -e
[[ $user_failure_status -ne 0 ]] || fail "user migration runner exits non-zero when a migration fails"
[[ ! -f $failure_home/.local/state/omarchy/migrations/user/300-fail-user.sh ]] || fail "user migration runner does not mark failed migration complete"
grep -q '^user-before-fail$' "$calls" || fail "user migration runner started failing migration"
! grep -q '^user-after-fail$' "$calls" || fail "user migration runner stops failing migration under strict mode"
pass "user migration runner does not mark failed migrations complete"
cat >"$failure_root/migrations/system/400-fail-system.sh" <<'SH'
echo system-before-fail >>"$TEST_CALLS"
false
echo system-after-fail >>"$TEST_CALLS"
SH
set +e
OMARCHY_PATH="$failure_root" \
OMARCHY_SYSTEM_MIGRATION_STATE="$failure_system_state" \
TEST_CALLS="$calls" \
"$ROOT/bin/omarchy-migrate-system" >"$test_tmp/system-failure.out" 2>"$test_tmp/system-failure.err"
system_failure_status=$?
set -e
[[ $system_failure_status -ne 0 ]] || fail "system migration runner exits non-zero when a migration fails"
[[ ! -f $failure_system_state/400-fail-system.sh ]] || fail "system migration runner does not mark failed migration complete"
grep -q '^system-before-fail$' "$calls" || fail "system migration runner started failing migration"
! grep -q '^system-after-fail$' "$calls" || fail "system migration runner stops failing migration under strict mode"
pass "system migration runner does not mark failed migrations complete"
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mkdir -p "$stub_bin" "$test_home"
cat >"$stub_bin/omarchy-migrate-system" <<'SH'
#!/bin/bash
if [[ ${1:-} == "--pending" ]]; then
[[ ${OMARCHY_TEST_PENDING_SYSTEM:-1} == 1 ]] || exit 1
echo 100-system.sh
exit 0
fi
echo system >>"$TEST_CALLS"
SH
chmod +x "$stub_bin/omarchy-migrate-system"
cat >"$stub_bin/omarchy-migrate-user" <<'SH'
#!/bin/bash
if [[ ${1:-} == "--pending" ]]; then
[[ ${OMARCHY_TEST_PENDING_USER:-1} == 1 ]] || exit 1
echo 200-user.sh
exit 0
fi
echo user >>"$TEST_CALLS"
SH
chmod +x "$stub_bin/omarchy-migrate-user"
run_migrate() {
HOME="$test_home" \
PATH="$stub_bin:$PATH" \
TEST_CALLS="$test_tmp/calls" \
"$ROOT/bin/omarchy-migrate" "$@"
}
: >"$test_tmp/calls"
run_migrate >"$test_tmp/migrate.out"
[[ $(sed -n '1p' "$test_tmp/calls") == "system" ]] || fail "omarchy-migrate runs system migrations first"
[[ $(sed -n '2p' "$test_tmp/calls") == "user" ]] || fail "omarchy-migrate runs user migrations second"
[[ $(wc -l <"$test_tmp/calls") -eq 2 ]] || fail "omarchy-migrate only delegates to system and user migration runners"
pass "omarchy-migrate runs system and user migrations without force"
run_migrate --pending >"$test_tmp/pending-all.out"
grep -q '^system/100-system\.sh$' "$test_tmp/pending-all.out" || fail "omarchy-migrate --pending lists pending system migrations"
grep -q '^user/200-user\.sh$' "$test_tmp/pending-all.out" || fail "omarchy-migrate --pending lists pending user migrations"
pass "omarchy-migrate --pending lists all pending migrations"
run_migrate --pending user >"$test_tmp/pending-user.out"
grep -q '^user/200-user\.sh$' "$test_tmp/pending-user.out" || fail "omarchy-migrate --pending user lists user migrations"
! grep -q '^system/' "$test_tmp/pending-user.out" || fail "omarchy-migrate --pending user omits system migrations"
pass "omarchy-migrate --pending user scopes pending output"
if OMARCHY_TEST_PENDING_SYSTEM=0 OMARCHY_TEST_PENDING_USER=0 run_migrate --pending >"$test_tmp/not-pending.out"; then
fail "omarchy-migrate --pending exits non-zero without pending migrations"
fi
[[ ! -s $test_tmp/not-pending.out ]] || fail "omarchy-migrate --pending stays quiet without pending migrations"
pass "omarchy-migrate --pending reports no pending migrations"
if run_migrate --force >"$test_tmp/force.out" 2>&1; then
fail "omarchy-migrate rejects obsolete --force option"
fi
grep -q 'Unknown option: --force' "$test_tmp/force.out" || fail "omarchy-migrate reports obsolete --force option"
pass "omarchy-migrate no longer needs --force"
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
test_home="$test_tmp/home"
state_home="$test_tmp/state"
mkdir -p "$stub_bin" "$test_home" "$state_home"
cat >"$stub_bin/fakeroot" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$stub_bin/fakeroot"
cat >"$stub_bin/checkupdates" <<'SH'
#!/bin/bash
case "${TEST_CHECKUPDATES:-updates}" in
updates)
printf 'linux 6.1-1 -> 6.1-2\nomarchy 4.0.0-1 -> 4.0.1-1\n'
exit 0
;;
none)
exit 2
;;
fail)
echo "check failed" >&2
exit 1
;;
esac
SH
chmod +x "$stub_bin/checkupdates"
cat >"$stub_bin/pacman" <<'SH'
#!/bin/bash
case "$1" in
-Qem)
[[ ${TEST_AUR_INSTALLED:-0} == "1" ]] && exit 0 || exit 1
;;
-Qu)
if [[ ${TEST_PACMAN_FALLBACK_UPDATES:-0} == "1" ]]; then
echo "fallback-pkg 1-1 -> 1-2"
fi
exit 0
;;
esac
exit 0
SH
chmod +x "$stub_bin/pacman"
cat >"$stub_bin/yay" <<'SH'
#!/bin/bash
if [[ ${TEST_YAY_UPDATES:-0} == "1" ]]; then
echo "aur-helper 1-1 -> 1-2"
fi
exit 0
SH
chmod +x "$stub_bin/yay"
run_checker() {
HOME="$test_home" \
XDG_STATE_HOME="$state_home" \
PATH="$stub_bin:$PATH" \
OMARCHY_UPDATE_CHECK_TIMEOUT=5 \
"$ROOT/bin/omarchy-update-available"
}
capture_checker() {
local stdout_file="$1"
local stderr_file="$2"
shift 2
set +e
(
export "$@"
run_checker
) >"$stdout_file" 2>"$stderr_file"
local status=$?
set -e
return "$status"
}
stdout="$test_tmp/stdout"
stderr="$test_tmp/stderr"
if capture_checker "$stdout" "$stderr" TEST_CHECKUPDATES=updates TEST_AUR_INSTALLED=0; then
status=0
else
status=$?
fi
[[ $status -eq 0 ]] || fail "update checker exits successfully when system updates are available"
grep -q '^linux ' "$stdout" || fail "update checker prints system package updates"
grep -q '^omarchy ' "$state_home/omarchy/updates/packages" || fail "update checker stores system package updates"
[[ $(wc -l <"$state_home/omarchy/updates/available") -eq 2 ]] || fail "update checker stores combined update list"
pass "update checker detects system package updates"
if capture_checker "$stdout" "$stderr" TEST_CHECKUPDATES=none TEST_AUR_INSTALLED=1 TEST_YAY_UPDATES=1; then
status=0
else
status=$?
fi
[[ $status -eq 0 ]] || fail "update checker exits successfully when AUR updates are available"
grep -q '^aur-helper ' "$stdout" || fail "update checker prints AUR package updates"
grep -q '^aur-helper ' "$state_home/omarchy/updates/aur" || fail "update checker stores AUR package updates"
pass "update checker detects AUR package updates"
if capture_checker "$stdout" "$stderr" TEST_CHECKUPDATES=none TEST_AUR_INSTALLED=0; then
status=0
else
status=$?
fi
[[ $status -eq 1 ]] || fail "update checker exits non-zero when no updates are available"
grep -q '^System is up to date$' "$stdout" || fail "update checker prints up-to-date message"
[[ ! -s "$state_home/omarchy/updates/available" ]] || fail "update checker clears combined update list when up to date"
pass "update checker reports up-to-date systems"
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
test_home="$test_tmp/home"
runtime_dir="$test_tmp/runtime"
mkdir -p "$stub_bin" "$test_home" "$runtime_dir"
run_with_lock_env() {
HOME="$test_home" \
XDG_RUNTIME_DIR="$runtime_dir" \
XDG_STATE_HOME="$test_tmp/state" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
"$@"
}
write_stub() {
local name="$1"
local body="$2"
cat >"$stub_bin/$name" <<SH
#!/bin/bash
$body
SH
chmod +x "$stub_bin/$name"
}
for command in \
omarchy-toggle-idle \
systemd-inhibit \
omarchy-update-keyring \
omarchy-update-system-pkgs \
omarchy-migrate \
omarchy-update-aur-pkgs \
omarchy-update-mise \
omarchy-update-orphan-pkgs \
omarchy-hook \
omarchy-update-analyze-logs \
omarchy-shell \
omarchy-update-restart; do
write_stub "$command" 'exit 0'
done
write_stub omarchy-update-available 'exit 1'
# omarchy-update should hold the lock before snapshotting, so a second update
# cannot even enter its pre-update snapshot.
update_snapshot_marker="$test_tmp/update-snapshot-started"
write_stub omarchy-snapshot 'echo started >"$TEST_MARKER"; sleep 2; exit 0'
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$update_snapshot_marker" run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-first.out" 2>&1 &
update_pid=$!
for _ in {1..50}; do
[[ -f $update_snapshot_marker ]] && break
sleep 0.05
done
[[ -f $update_snapshot_marker ]] || fail "first omarchy-update reached snapshot under lock"
set +e
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$test_tmp/update-second-snapshot-started" run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-second.out" 2>&1
update_second_status=$?
set -e
wait "$update_pid"
[[ $update_second_status -ne 0 ]] || fail "second omarchy-update exits non-zero while update lock is held"
grep -q "already running" "$test_tmp/update-second.out" || fail "second omarchy-update reports held update lock"
[[ ! -f $test_tmp/update-second-snapshot-started ]] || fail "second omarchy-update did not snapshot while lock was held"
pass "omarchy-update prevents overlapping top-level updates"
# omarchy-update-perform is now only a compatibility wrapper around
# omarchy-update -y, but it should still respect the same update lock.
perform_marker="$test_tmp/perform-started"
write_stub omarchy-update-keyring 'echo started >"$TEST_MARKER"; sleep 2; exit 0'
TEST_MARKER="$perform_marker" run_with_lock_env "$ROOT/bin/omarchy-update-perform" >"$test_tmp/perform-first.out" 2>&1 &
perform_pid=$!
for _ in {1..50}; do
[[ -f $perform_marker ]] && break
sleep 0.05
done
[[ -f $perform_marker ]] || fail "first omarchy-update-perform delegated to update under lock"
set +e
TEST_MARKER="$test_tmp/perform-second-started" run_with_lock_env "$ROOT/bin/omarchy-update-perform" >"$test_tmp/perform-second.out" 2>&1
perform_second_status=$?
set -e
wait "$perform_pid"
[[ $perform_second_status -ne 0 ]] || fail "second omarchy-update-perform exits non-zero while update lock is held"
grep -q "already running" "$test_tmp/perform-second.out" || fail "second omarchy-update-perform reports held update lock"
[[ ! -f $test_tmp/perform-second-started ]] || fail "second omarchy-update-perform did not snapshot while lock was held"
pass "omarchy-update-perform compatibility wrapper respects update lock"
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mkdir -p "$stub_bin" "$test_home"
write_stub() {
local name="$1"
local body="$2"
cat >"$stub_bin/$name" <<SH
#!/bin/bash
$body
SH
chmod +x "$stub_bin/$name"
}
run_orphan_checker() {
HOME="$test_home" PATH="$stub_bin:$PATH" "$ROOT/bin/omarchy-update-orphan-pkgs"
}
write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then printf "old-lib\nunused-tool\n"; exit 0; fi; exit 1'
write_stub sudo 'echo "sudo should not be called" >&2; exit 99'
write_stub gum 'echo "gum should not be called" >&2; exit 99'
run_orphan_checker >"$test_tmp/noninteractive.out" 2>"$test_tmp/noninteractive.err"
grep -q '^ old-lib$' "$test_tmp/noninteractive.out" || fail "orphan checker lists orphan packages"
grep -q 'Re-run omarchy-update-orphan-pkgs in a terminal' "$test_tmp/noninteractive.out" || fail "orphan checker does not remove packages non-interactively"
pass "orphan checker only reports orphans non-interactively"
write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then exit 0; fi; exit 1'
run_orphan_checker >"$test_tmp/none.out" 2>"$test_tmp/none.err"
[[ ! -s $test_tmp/none.out ]] || fail "orphan checker stays quiet when no orphans exist"
pass "orphan checker stays quiet without orphans"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
run_guard() {
OMARCHY_PACMAN_CMDLINE="$1" "$ROOT/bin/omarchy-update-pacman-guard"
}
if run_guard "pacman -Syu --noconfirm" >"$test_tmp/direct.out" 2>"$test_tmp/direct.err"; then
fail "pacman guard blocks direct system upgrades"
fi
grep -q 'omarchy update' "$test_tmp/direct.err" || fail "pacman guard explains omarchy update entrypoint"
pass "pacman guard blocks direct pacman -Syu"
if run_guard "pacman --sync --refresh --sysupgrade" >"$test_tmp/long.out" 2>"$test_tmp/long.err"; then
fail "pacman guard blocks long-form system upgrades"
fi
pass "pacman guard blocks long-form pacman sysupgrade"
OMARCHY_UPDATE_PACMAN=1 run_guard "pacman -Syu --noconfirm" >"$test_tmp/omarchy.out" 2>"$test_tmp/omarchy.err"
[[ ! -s $test_tmp/omarchy.err ]] || fail "pacman guard stays quiet for omarchy update pacman call"
pass "pacman guard allows omarchy update pacman call"
OMARCHY_ALLOW_DIRECT_PACMAN=1 run_guard "pacman -Syu --noconfirm" >"$test_tmp/override.out" 2>"$test_tmp/override.err"
[[ ! -s $test_tmp/override.err ]] || fail "pacman guard stays quiet for explicit direct pacman override"
pass "pacman guard allows explicit direct pacman override"
run_guard "pacman -S firefox" >"$test_tmp/install.out" 2>"$test_tmp/install.err"
[[ ! -s $test_tmp/install.err ]] || fail "pacman guard stays quiet for non-sysupgrade pacman command"
pass "pacman guard ignores non-system-upgrade transactions"