Files
Panama/tests/setup/migrations-contract
T
Gabriel Brown e446a1072c Give an installed machine a way to catch up
./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.
2026-08-21 21:01:31 -04:00

150 lines
6.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Migrations: the half of an upgrade `./install` cannot do.
#
# The installer only ever adds. Migrations are the one mechanism that can
# remove a file, disable a unit, or repair a symlink on a machine that already
# exists -- which means they run, unattended, against machines nobody testing
# them has ever seen. The properties that make that safe:
#
# 1. Applied exactly once. A marker is written on success and never again.
# 2. A FAILURE LEAVES NO MARKER. A repair that could not complete has not
# happened, and recording it as done would hide it forever.
# 3. Ordered, and stopped at the first failure. Later migrations may assume
# earlier ones landed; running them anyway turns one stuck repair into an
# unpredictable machine.
# 4. A fresh install runs none of them. The machine was just built from this
# checkout, so every repair they describe is already true of it.
# 5. Root work goes through `panama-sudo --reason`, never bare sudo, so the
# password prompt names the repair.
#
# Driven against fixture migrations in a throwaway state directory. The real
# migrations are only checked for the properties every one of them must have.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
runner="$repo_dir/bin/panama-migrate"
authoring="$repo_dir/bin/panama-dev-migration"
installer="$repo_dir/install"
findings=()
note() { findings+=("$1"); }
[[ -x "$runner" ]] || { printf 'migrations contract: %s is not executable\n' "$runner" >&2; exit 1; }
[[ -x "$authoring" ]] || note 'panama-dev-migration is missing or not executable'
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
fake_repo="$work/repo"
state="$work/state/panama/migrations"
mkdir -p "$fake_repo/bin" "$fake_repo/migrations"
cp "$runner" "$fake_repo/bin/panama-migrate"
# Fixture migrations, named the way the real ones are: epoch seconds, so glob
# order is chronological.
ran="$work/ran"
write_migration() {
local stamp="$1" body="$2"
cat >"$fake_repo/migrations/$stamp.sh" <<MIGRATION
#!/usr/bin/env bash
printf '%s\n' "$stamp" >>"$ran"
$body
MIGRATION
}
run() {
PANAMA_PATH="$fake_repo" XDG_STATE_HOME="$work/state" \
"$fake_repo/bin/panama-migrate" "$@" 2>&1
}
# ── 1. Applied exactly once, in order ────────────────────────────────────────
: >"$ran"
write_migration 1000000001 'exit 0'
write_migration 1000000002 'exit 0'
run run >/dev/null || note 'a run of two good migrations failed'
[[ "$(cat "$ran")" == $'1000000001\n1000000002' ]] \
|| note "migrations did not run oldest-first (got: $(tr '\n' ' ' <"$ran"))"
: >"$ran"
run run >/dev/null || note 'a second run failed'
[[ -s "$ran" ]] && note 'a migration ran twice'
# ── 2 & 3. A failure leaves no marker, and stops the ones after it ───────────
: >"$ran"
write_migration 1000000003 'exit 1'
write_migration 1000000004 'exit 0'
run run >/dev/null && note 'a failing migration reported success'
grep -qx 1000000003 "$ran" || note 'the failing migration did not run at all'
grep -qx 1000000004 "$ran" && note 'a migration after a failure still ran'
[[ -e "$state/1000000003.sh" ]] && note 'a failed migration was marked applied'
# It retries, and the one behind it is still waiting.
: >"$ran"
write_migration 1000000003 'exit 0'
run run >/dev/null || note 'a repaired migration did not succeed on retry'
grep -qx 1000000003 "$ran" || note 'the previously failed migration was not retried'
grep -qx 1000000004 "$ran" || note 'the migration behind the failure never ran'
# ── 4. Fresh installs run nothing ────────────────────────────────────────────
rm -rf "$work/state"
: >"$ran"
run --baseline >/dev/null || note 'baseline failed'
[[ -s "$ran" ]] && note 'baseline ran a migration instead of marking it'
: >"$ran"
run run >/dev/null || note 'a run after baseline failed'
[[ -s "$ran" ]] && note 'a migration ran on a machine that was baselined'
# ── --pending is the notifier's question ─────────────────────────────────────
run --pending >/dev/null && note '--pending reported work when everything is applied'
write_migration 1000000005 'exit 0'
run --pending >/dev/null || note '--pending reported nothing when a migration is waiting'
# ── 5. Everything the repository actually ships ──────────────────────────────
shopt -s nullglob
for migration in "$repo_dir"/migrations/*.sh; do
name="$(basename "$migration")"
[[ -x "$migration" ]] || note "$name is not executable"
[[ "$name" =~ ^[0-9]{10,}\.sh$ ]] \
|| note "$name is not named with an epoch timestamp, so its order is undefined"
# Bare sudo would prompt with polkit's generic text, in a script the user
# did not run by hand. panama-sudo makes the prompt name the repair.
if grep -qE '(^|[^-[:alnum:]])sudo ' "$migration" && ! grep -q 'panama-sudo' "$migration"; then
note "$name calls bare sudo; root work goes through panama-sudo --reason"
fi
done
# ── The installer wires it in ────────────────────────────────────────────────
grep -q 'panama-migrate' "$installer" \
|| note 'install never runs or baselines migrations, so a fresh machine is never marked'
grep -q -- '--baseline' "$installer" \
|| note 'install does not baseline a fresh machine, so it would run repairs it never needed'
notifier="$repo_dir/bin/panama-migrate-notify"
if [[ ! -x "$notifier" ]]; then
note 'panama-migrate-notify is missing, so pending repairs announce themselves to nobody'
else
grep -q 'org.freedesktop.Notifications' "$notifier" \
|| note 'the notifier does not wait for the notification server, so an early session swallows it'
fi
grep -q 'panama-migrate-notify' "$repo_dir/config/dot/hypr/autostart.lua" \
|| note 'nothing starts the migration notifier at login'
if (( ${#findings[@]} > 0 )); then
printf 'migrations contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'migrations contract: PASS\n'