./install only ever adds. It copies over /, links dotfiles, installs packages -- and has no way to say "remove that file", "disable that unit", "that symlink points nowhere now". So a machine set up months ago keeps whatever this repository has since decided was wrong, and the only thing that ever fixes it is somebody reading a commit message. With a curl installer in the README, that stopped being hypothetical. A migration is one script that performs one repair, exactly once, on the machines that need it. Named by the commit timestamp that authored it, so glob order is chronological without a sequence number two branches could both pick. Marked in ~/.local/state on success and only on success, so a repair that failed stays pending rather than being recorded as done and hidden forever. Ordered, and stopped at the first failure, because a later repair may assume an earlier one landed. A fresh install marks everything without running it, the way Migrations.qml stamps a pre-versioning settings file at its baseline. The first real one removes the dangling ~/.config/forge symlink left behind when the GNOME session was cut: link-dotfiles could link it but never unlink it. Verified both ways -- a no-op on a machine that never had it, an actual repair on one that did. Root work goes through panama-sudo --reason so the password prompt names the repair, and the contract fails any migration reaching for bare sudo.
51 lines
1.9 KiB
Bash
Executable File
51 lines
1.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# "Panama has repairs waiting for this machine."
|
|
#
|
|
# A migration that nobody knows about is a migration that never runs. This
|
|
# checks once per session and, when work is pending, sends one notification
|
|
# whose action opens a terminal running `panama migrate` -- so the repair is
|
|
# always something the user chose, never something that happened to them.
|
|
#
|
|
# Waiting for the notification server first is not politeness. Quickshell owns
|
|
# org.freedesktop.Notifications, and a shell that has not started yet would
|
|
# swallow this silently -- which is precisely the session where a pending
|
|
# migration is most likely to matter.
|
|
|
|
set -uo pipefail
|
|
|
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
|
TERMINAL="${PANAMA_TERMINAL:-kitty}"
|
|
|
|
command -v notify-send >/dev/null 2>&1 || exit 1
|
|
|
|
count="$("$PANAMA_PATH/bin/panama-migrate" --pending)" || exit 1
|
|
[[ -n "$count" ]] || exit 1
|
|
|
|
# Up to ~15s for the shell to claim the bus name. Longer than a healthy start
|
|
# needs, short enough that a session without a shell gives up rather than
|
|
# lingering.
|
|
for _ in $(seq 1 30); do
|
|
if busctl --user status org.freedesktop.Notifications >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
if (( count == 1 )); then
|
|
body="One repair is waiting for this machine."
|
|
else
|
|
body="$count repairs are waiting for this machine."
|
|
fi
|
|
|
|
# Critical so it waits to be read: a notification that expires while the user
|
|
# is elsewhere has told nobody anything. The action is the whole point -- there
|
|
# is no instruction to remember and nothing to type.
|
|
action="$(notify-send --urgency=critical --icon=system-software-update \
|
|
--app-name=Panama \
|
|
--action=migrate="Apply now" --action=later="Later" \
|
|
"Panama updates" "$body" 2>/dev/null)" || exit 0
|
|
|
|
[[ "$action" == "migrate" ]] || exit 0
|
|
exec "$TERMINAL" --hold "$PANAMA_PATH/bin/panama-migrate" run
|