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.
This commit is contained in:
Gabriel Brown
2026-08-21 21:01:31 -04:00
parent 3c45d63085
commit e446a1072c
11 changed files with 557 additions and 1 deletions
+22
View File
@@ -106,6 +106,28 @@ for stage in "${STAGES[@]}"; do
fi
done
# ── Migrations ───────────────────────────────────────────────────────────────
#
# Repairs for machines that installed an older Panama: removing a file this
# repository stopped shipping, disabling a unit it stopped wanting. The
# installer itself cannot do any of that, because it only ever adds.
#
# A machine that has never seen migrations before is one of two things, and
# the difference matters. If it has no marker directory at all it was just
# built from THIS checkout, so every repair those migrations describe is
# already true of it -- they are marked applied without running, exactly as
# Migrations.qml stamps a pre-versioning settings file at its baseline rather
# than replaying upgrades it never needed. Otherwise the pending ones run.
migrate="$PANAMA_PATH/bin/panama-migrate"
if [[ -x "$migrate" ]]; then
printf '\n=== migrations ===\n'
if [[ -d "${XDG_STATE_HOME:-$HOME/.local/state}/panama/migrations" ]]; then
"$migrate" run || failed+=(migrations)
else
"$migrate" --baseline || true
fi
fi
# ── Did it actually work? ────────────────────────────────────────────────────
#
# A failed-stage count only reports what exited non-zero. It says nothing about a