diff --git a/AGENTS.md b/AGENTS.md index 23897a1b..8a8da1c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -262,7 +262,7 @@ This copies `$OMARCHY_PATH/config/hypr/hyprlock.conf` to `~/.config/hypr/hyprloc Read `docs/migrations.md` before creating or changing migrations. -Migrations are per-user and run through `omarchy-migrate` during `omarchy update` or from the migration notification. Put migrations directly under `migrations/.sh`. Pending state is per-user under `~/.local/state/omarchy/migrations/`, so every user gets a chance to run every migration. Migrations run as the user; privileged work should invoke the appropriate helper or privilege prompt, and no-op when another user already applied it. +Migrations are per-user and run through `omarchy-migrate` during `omarchy update` or from the login-time migration notification. Put migrations directly under `migrations/.sh`. Pending state is per-user under `~/.local/state/omarchy/migrations/`, so every user gets a chance to run every migration. Migrations run as the user; privileged work should invoke the appropriate helper or privilege prompt, and no-op when another user already applied it. To create a new migration, run `omarchy-dev-add-migration --no-edit`. diff --git a/bin/omarchy-first-run b/bin/omarchy-first-run index c31bd30c..a771a9c1 100755 --- a/bin/omarchy-first-run +++ b/bin/omarchy-first-run @@ -53,37 +53,6 @@ log_first_run() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$FIRST_RUN_LOG" } -notification_server_ready() { - if omarchy-cmd-present gdbus; then - gdbus call --session \ - --dest org.freedesktop.Notifications \ - --object-path /org/freedesktop/Notifications \ - --method org.freedesktop.Notifications.GetServerInformation >/dev/null 2>&1 - elif omarchy-cmd-present busctl; then - busctl --user call \ - org.freedesktop.Notifications \ - /org/freedesktop/Notifications \ - org.freedesktop.Notifications \ - GetServerInformation >/dev/null 2>&1 - else - return 0 - fi -} - -wait_for_notifications() { - omarchy-cmd-present omarchy-shell || return 0 - - for _ in {1..100}; do - if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then - return 0 - fi - sleep 0.1 - done - - log_first_run "Timed out waiting for notification service; continuing" - return 0 -} - run_first_run_step() { local name="$1" shift @@ -98,12 +67,6 @@ run_first_run_step() { fi } -wait_for_notifications - -run_first_run_step "enable migration notification watcher" \ - systemctl --user enable --now omarchy-update-user-notify.path -run_first_run_step "notify about pending migrations" omarchy-migrate-notify - run_first_run_step "install Voxtype post-update hook" \ omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook" run_first_run_step "install fingerprint setup post-update hook" \ @@ -118,7 +81,7 @@ run_first_run_step "set GTK primary paste" \ run_first_run_step "apply speaker tuning" \ bash "$OMARCHY_PATH/install/user/first-run/audio-tuning.sh" -wait_for_notifications +omarchy-notification-wait || log_first_run "Timed out waiting for notification service; continuing" run_first_run_step "show welcome notification" \ bash "$OMARCHY_PATH/install/user/first-run/welcome.sh" # The first-run notification scripts register action callbacks in background diff --git a/bin/omarchy-migrate b/bin/omarchy-migrate index fc801e35..056a578d 100755 --- a/bin/omarchy-migrate +++ b/bin/omarchy-migrate @@ -96,6 +96,7 @@ while IFS=$'\t' read -r name file marker; do fi done < <(migration_entries) -# Clear notifications queued while an update was installing migrations. The -# substring matches both the current and legacy notification titles. +# Clear a login-time notification the user left sitting there and then resolved +# by running migrations some other way. The substring matches both the current +# and legacy notification titles. omarchy-notification-dismiss "Omarchy Migrations" >/dev/null 2>&1 || true diff --git a/bin/omarchy-migrate-notify b/bin/omarchy-migrate-notify index 8f7935b8..1f9fb541 100755 --- a/bin/omarchy-migrate-notify +++ b/bin/omarchy-migrate-notify @@ -15,11 +15,17 @@ fi notify_command=$(printf 'if [[ -n $(omarchy-notification-send -u critical -g  "Pending Omarchy Migrations" %q -a) ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-migrate; fi' "$message") -if omarchy-cmd-present systemd-run; 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 +# This runs from omarchy-migrate-notify.service at graphical-session.target, +# which the session can reach before the shell has claimed +# org.freedesktop.Notifications. Without the wait the toast is sent into the +# void and the user never learns about their pending migrations. +omarchy-notification-wait || true +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 + +# Reached when there is no user manager to run the scope under, such as a +# non-graphical shell, so fall back to telling the user in the terminal. print_pending_migrations() { echo "Omarchy has pending migrations. Run omarchy-migrate in a terminal to apply them:" while IFS= read -r migration; do diff --git a/bin/omarchy-notification-wait b/bin/omarchy-notification-wait new file mode 100755 index 00000000..609dc326 --- /dev/null +++ b/bin/omarchy-notification-wait @@ -0,0 +1,30 @@ +#!/bin/bash + +# omarchy:summary=Wait for the desktop notification server to accept notifications +# omarchy:args=[timeout-seconds] +# omarchy:hidden=true + +set -uo pipefail + +timeout=${1:-10} + +notification_server_ready() { + busctl --user call \ + org.freedesktop.Notifications \ + /org/freedesktop/Notifications \ + org.freedesktop.Notifications \ + GetServerInformation >/dev/null 2>&1 +} + +# The shell has to be up to serve the IPC, and it has to have claimed the +# notification bus name before notify-send has anywhere to deliver. +attempts=$((timeout * 10)) +while (( attempts > 0 )); do + if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then + exit 0 + fi + attempts=$((attempts - 1)) + sleep 0.1 +done + +exit 1 diff --git a/default/systemd/user/omarchy-migrate-notify.service b/default/systemd/user/omarchy-migrate-notify.service new file mode 100644 index 00000000..79207652 --- /dev/null +++ b/default/systemd/user/omarchy-migrate-notify.service @@ -0,0 +1,16 @@ +[Unit] +Description=Notify about pending Omarchy migrations +# Login-only. There used to be an omarchy-update-user-notify.path watching +# /usr/share/omarchy/migrations, but pacman writes that directory during every +# update -- including the blessed `omarchy update`, which runs omarchy-migrate +# itself a step later -- so the watcher notified about migrations that were +# already being applied in the visible update terminal. Checking once per login +# is the only trigger that cannot collide with a running update. +ConditionPathIsDirectory=/usr/share/omarchy/migrations + +[Service] +Type=oneshot +ExecStart=/usr/bin/omarchy-migrate-notify + +[Install] +WantedBy=graphical-session.target diff --git a/default/systemd/user/omarchy-update-user-notify.path b/default/systemd/user/omarchy-update-user-notify.path deleted file mode 100644 index aaf5233f..00000000 --- a/default/systemd/user/omarchy-update-user-notify.path +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Watch for Omarchy migrations - -[Path] -# Edge-triggered watch only. PathExistsGlob= is level-triggered: it re-fires -# every time the triggered unit deactivates for as long as the glob matches, -# and applied migrations stay on disk forever, so a glob here busy-loops the -# oneshot service. The once-per-login check lives in the service's own -# WantedBy=graphical-session.target instead. -PathModified=/usr/share/omarchy/migrations -Unit=omarchy-update-user-notify.service - -[Install] -WantedBy=graphical-session.target diff --git a/default/systemd/user/omarchy-update-user-notify.service b/default/systemd/user/omarchy-update-user-notify.service deleted file mode 100644 index 92f25a65..00000000 --- a/default/systemd/user/omarchy-update-user-notify.service +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Notify about pending Omarchy migrations -ConditionPathIsDirectory=/usr/share/omarchy/migrations - -[Service] -Type=oneshot -ExecStart=/usr/bin/omarchy-migrate-notify - -[Install] -WantedBy=graphical-session.target diff --git a/docs/file-layout.md b/docs/file-layout.md index 2720d382..d612166a 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -197,13 +197,19 @@ migration. Migrations run as the user; privileged work should invoke the appropriate helper or privilege prompt. Migrations must be idempotent; machine-wide repairs should no-op when another user already applied them. -Each graphical user has `omarchy-update-user-notify.path` watching the packaged -migration directory for changes, and `omarchy-update-user-notify.service` is -also started once per login via its own `WantedBy=graphical-session.target`. -Either way the service runs `omarchy-migrate-notify` as that user. The notifier checks -`omarchy-migrate --pending`. If this user has missing migration state, it shows a -notification that opens a terminal for `omarchy-migrate`. The notifier never runs -migrations in the background. +Each graphical user has `omarchy-migrate-notify.service`, started once per login +through `WantedBy=graphical-session.target`. The package also ships +`omarchy-update-user-notify.service` as a symlink onto it, so users enabled +under the old unit name keep working before they reach migration `1785095882`. +It runs `omarchy-migrate-notify` as +that user, which checks `omarchy-migrate --pending`. If this user has missing +migration state, it shows a notification that opens a terminal for +`omarchy-migrate`. The notifier never runs migrations in the background. + +Login is the only trigger. Nothing watches the packaged migration directory: a +watcher cannot tell a bypassed `pacman -Syu` from the package transaction inside +a normal `omarchy update`, so it notified about migrations that `omarchy-migrate` +was already applying in the visible update terminal. `omarchy-migrate` waits for any active pacman transaction to finish, then runs pending migrations. It does not need `--force`; migrations happen when state @@ -221,8 +227,8 @@ 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`, `omarchy-update-user-notify.path`, - `omarchy-update-user-notify.service`). Done here, not at finalize, because + `omarchy-recover-internal-monitor`, `omarchy-migrate-notify.service`). + Done here, not at finalize, because the user manager isn't reachable from the ISO chroot; `ConditionPath*` in the unit files keeps services inert when they don't apply. - `install/user/first-run/gnome-theme.sh`, diff --git a/docs/migrations.md b/docs/migrations.md index 48c2125b..7e6d5538 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -43,16 +43,9 @@ omarchy-hook post-update `omarchy-migrate` waits for any active pacman transaction to finish, then runs all pending migrations for the current user in the visible update terminal. -### During direct pacman updates +### At login -Raw `sudo pacman -Syu` is guarded. Users should normally run: - -```bash -omarchy update -``` - -If a user explicitly bypasses the guard, user sessions watch the packaged -migration directory and run a notifier. The notifier checks: +Every graphical login starts `omarchy-migrate-notify.service`, which checks: ```bash omarchy-migrate --pending @@ -67,6 +60,15 @@ omarchy-migrate The notifier never runs migrations silently in the background. +This is what covers users who did not run the update themselves: someone who +bypassed the pacman guard with `sudo env OMARCHY_ALLOW_DIRECT_PACMAN=1 pacman +-Syu`, and any second user on the machine, whose migration markers are per-user +and therefore still missing after another user updated. + +Login is the only trigger on purpose. Watching the packaged migration directory +also fires during a normal `omarchy update`, which prompts for migrations that +`omarchy-migrate` is about to run in the visible update terminal. + ### Manually Users can safely run: diff --git a/docs/update-process.md b/docs/update-process.md index 200f1ffc..b12f4f51 100644 --- a/docs/update-process.md +++ b/docs/update-process.md @@ -149,20 +149,37 @@ High-level flow: sudo pacman -Syu ├─ pre-transaction guard aborts and tells the user to run omarchy update └─ if explicitly bypassed, upgrades omarchy and related packages - └─ user session notices migration directory changes - ├─ omarchy-update-user-notify.path triggers, if enabled + └─ at that user's next login + ├─ omarchy-migrate-notify.service starts with graphical-session.target ├─ omarchy-migrate-notify checks omarchy-migrate --pending ├─ if this user has missing migration state, show notification └─ click opens terminal: omarchy-migrate ``` +Login is deliberately the only trigger. A watcher on the packaged migration +directory cannot distinguish a bypassed `pacman -Syu` from the package +transaction inside a normal `omarchy update`, so it fired notifications for +migrations that `omarchy-migrate` was about to apply in the visible update +terminal. The retired unit was `omarchy-update-user-notify.path`. + 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 migration state. +- `omarchy-first-run` enables `omarchy-migrate-notify.service`, which also + covers users created after install: their per-user migration markers are + missing, so their first login prompts them to run every shipped migration. +- The package ships `omarchy-update-user-notify.service` as a symlink onto + `omarchy-migrate-notify.service`. Users set up before the rename hold an + absolute `graphical-session.target.wants` symlink to the old path, and the + migration that repoints it only runs for users who run an update — the + opposite of who the notifier is for. The alias can be dropped once installs + have run migration `1785095882`. +- The notifier waits for a live notification server before sending, because + `graphical-session.target` can be reached before the shell claims + `org.freedesktop.Notifications`. - The notifier is only a prompt. It does not run migrations in the background. +- A session that is already open when another user updates is not re-checked; + it picks the migrations up at its next login, or whenever that user runs + `omarchy-migrate` or `omarchy update`. - Direct pacman updates do not run `omarchy-hook post-update` unless the user explicitly runs that hook; without a package-update marker, the only pending state we can derive is missing per-user migration markers. @@ -209,7 +226,7 @@ scripts. | `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` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **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` and shows notification only when this user has pending migrations. | **Keep internal/hidden.** Clear name now that the public command is `omarchy-migrate`. | +| `omarchy-migrate-notify` | Internal login-time notification helper. Uses `omarchy-migrate --pending` and shows a 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. | **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. | @@ -229,7 +246,8 @@ scripts. idempotent when they repair machine-wide state. 2. **Migration notification naming** - - The real helper is `omarchy-migrate-notify`. + - The real helper is `omarchy-migrate-notify`, started by + `omarchy-migrate-notify.service`. - `omarchy-update-user-notify` remains only as a hidden compatibility wrapper. 3. **Update pipeline ownership** diff --git a/install/user/first-run/enable-user-units.sh b/install/user/first-run/enable-user-units.sh index efc8123c..a5d7cb8e 100755 --- a/install/user/first-run/enable-user-units.sh +++ b/install/user/first-run/enable-user-units.sh @@ -16,5 +16,4 @@ systemctl --user enable --now \ bt-agent.service \ omarchy-recover-internal-monitor.service \ omarchy-sleep-lock.service \ - omarchy-update-user-notify.path \ - omarchy-update-user-notify.service + omarchy-migrate-notify.service diff --git a/migrations/1785095882.sh b/migrations/1785095882.sh new file mode 100644 index 00000000..cb633bac --- /dev/null +++ b/migrations/1785095882.sh @@ -0,0 +1,39 @@ +echo "Only check for pending migrations at login, not on every package update" + +# omarchy-update-user-notify.path watched /usr/share/omarchy/migrations, but +# pacman writes that directory during every update -- including the blessed +# `omarchy update`, which runs omarchy-migrate a step later. The watcher fired a +# critical notification for migrations that were already being applied in the +# visible update terminal. Retire the watcher and keep only the once-per-login +# check, now named after the command it runs. + +wants_dir="$HOME/.config/systemd/user/graphical-session.target.wants" + +systemctl --user daemon-reload >/dev/null 2>&1 || true + +# The watcher's unit file is already gone, but it stays loaded in a session that +# started before this update, so stop it before it can fire again. +systemctl --user stop omarchy-update-user-notify.path >/dev/null 2>&1 || true + +# Enable the replacement before dropping the old enablement, so a failure here +# can never leave a user with no notifier at all. Enable without --now: this +# usually runs from inside `omarchy update`, and starting the notifier here would +# pop a toast for the migrations running right after it -- the exact behavior +# being removed. `systemctl enable` also needs a live user manager, which +# `omarchy update` over SSH does not have, so fall back to writing precisely the +# symlink it would have written rather than silently doing nothing. +if ! systemctl --user enable omarchy-migrate-notify.service >/dev/null 2>&1; then + mkdir -p "$wants_dir" + ln -sfn /usr/lib/systemd/user/omarchy-migrate-notify.service \ + "$wants_dir/omarchy-migrate-notify.service" +fi + +# Drop the retired enablement by hand instead of through `systemctl disable`. +# The package ships omarchy-update-user-notify.service as a compatibility +# symlink onto the new unit, for users who have not reached this migration yet, +# so disabling that name here would disable the replacement along with it. +rm -f "$wants_dir/omarchy-update-user-notify.path" \ + "$wants_dir/omarchy-update-user-notify.service" + +systemctl --user reset-failed omarchy-update-user-notify.path >/dev/null 2>&1 || true +systemctl --user daemon-reload >/dev/null 2>&1 || true diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 905df673..ed1d7480 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -120,8 +120,7 @@ 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/systemd/user/omarchy-migrate-notify.service", "/usr/lib/systemd/user/omarchy-migrate-notify.service", "systemd/user/omarchy-migrate-notify.service"), ("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"), ("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"), ("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"), @@ -135,6 +134,16 @@ 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}") +# Existing users have an absolute wants symlink to the old unit path, and the +# migration that repoints it only runs for users who run an update -- the +# opposite of who the notifier is for. Dropping this alias strands them. +notify_alias = 'ln -sfn omarchy-migrate-notify.service "$pkgdir/usr/lib/systemd/user/omarchy-update-user-notify.service"' +if notify_alias not in pkgbuild: + errors.append( + "PKGBUILD does not ship the omarchy-update-user-notify.service compatibility " + "alias, so users who have not run migration 1785095882 lose the login notifier" + ) + alpm_hooks = [ "00-omarchy-update-guard.hook", "10-omarchy-hyprland-reload-pause.hook", diff --git a/test/shell.d/migrate-notify-test.sh b/test/shell.d/migrate-notify-test.sh index 9b182acb..a07888e9 100644 --- a/test/shell.d/migrate-notify-test.sh +++ b/test/shell.d/migrate-notify-test.sh @@ -33,6 +33,12 @@ bash -c "$command" SH chmod +x "$stub_bin/systemd-run" +cat >"$stub_bin/omarchy-notification-wait" <<'SH' +#!/bin/bash +exit 0 +SH +chmod +x "$stub_bin/omarchy-notification-wait" + cat >"$stub_bin/omarchy-notification-send" <<'SH' #!/bin/bash printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS" diff --git a/test/shell.d/systemd-test.sh b/test/shell.d/systemd-test.sh index cb9bd169..b6346495 100755 --- a/test/shell.d/systemd-test.sh +++ b/test/shell.d/systemd-test.sh @@ -28,12 +28,19 @@ grep -F 'ExecStart=/usr/bin/omarchy-system-sleep-monitor' "$upgrade_to_quattro" grep -F 'reset-failed omarchy-sleep-lock.service' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade repairs the legacy sleep lock unit path" -notify_path="$ROOT/default/systemd/user/omarchy-update-user-notify.path" -! grep -q 'PathExistsGlob' "$notify_path" -grep -Fx 'PathModified=/usr/share/omarchy/migrations' "$notify_path" >/dev/null -pass "migration watcher is edge-triggered so applied migrations on disk cannot re-trigger it" +[[ -e $ROOT/default/systemd/user/omarchy-update-user-notify.path ]] && + fail "the retired migration watcher is back; pacman writing the migration directory during omarchy update would notify about migrations that update is already applying" +grep -rlE '^(Path[A-Za-z]+|DirectoryNotEmpty)=.*/usr/share/omarchy/migrations' "$ROOT/default/systemd/user" >/dev/null 2>&1 && + fail "a user unit watches the migration directory again; the notifier must stay login-only" +pass "no unit watches the migration directory, so package updates cannot trigger the notifier" -notify_service="$ROOT/default/systemd/user/omarchy-update-user-notify.service" -! grep -q 'StartLimit' "$notify_service" +notify_service="$ROOT/default/systemd/user/omarchy-migrate-notify.service" +grep -Fx 'ExecStart=/usr/bin/omarchy-migrate-notify' "$notify_service" >/dev/null grep -Fx 'WantedBy=graphical-session.target' "$notify_service" >/dev/null -pass "migration notifier keeps its start-rate limit and still runs once per login" +pass "migration notifier only checks once per login" + +grep -F 'omarchy-migrate-notify.service' "$first_run_units" >/dev/null || + fail "first-run does not enable the login migration notifier" +grep -F 'omarchy-update-user-notify' "$first_run_units" >/dev/null && + fail "first-run still enables the retired notifier units" +pass "first-run enables the login-only migration notifier"