11 KiB
Panama: one command to update a machine
Why now
Updating a machine currently takes three commands, and one of them asks the full first-install questionnaire every time:
panama update # actually: commit & push my edits, or pull if clean
panama migrate # the repairs
panama upgrade # ./install, interview and all
Nobody will do that routinely, which means machines drift. The interview in particular is pure ceremony on an already-configured machine: it asks about NVIDIA, Secure Boot, git identity and extras categories, none of which have changed since the machine was built.
What is broken
1. The interview runs on every upgrade, and nothing needs it.
Five of the seven stages read no interview answer at all:
| Stage | Reads from the interview |
|---|---|
install-packages |
PANAMA_EXTRAS only, for the optional categories. Empty means base lists still install. |
link-dotfiles |
nothing |
link-user |
PANAMA_USER_CONTENT, and falls back to the decision recorded at $XDG_STATE_HOME/panama/user-content |
change-settings |
nothing |
link-vicinae-scripts |
nothing |
setup-identity |
git name/email/editor, gh login, ssh key — genuinely first-run |
install-hardware |
NVIDIA, MOK hash, debloat, firmware — genuinely first-run |
link-user already anticipates this in its own comment: "Running this stage by
hand outside an install honours the recorded answer instead, so panama upgrade
on an already-configured machine does not need re-asking." The capability is
there; install:91 just calls the interview unconditionally with no way past it.
2. panama update is named for the wrong job.
It means "review, commit & sync local changes" — a git workflow command. README
and config/dot/quickshell/manual/05-making-it-yours.md both document it that
way. The command a person reaches for to update their machine is therefore the
one command that does not.
3. Most updates need no stages at all, and there is no way to tell.
Dotfiles are symlinks into the checkout, so an edit to any existing
config/dot/** file is live the instant git pull returns. A pull only needs
stages when it brings something structurally new: a new config/dot/<name>/
directory, anything under config/copy/, a new package, or a gsettings change.
Today the only way to be sure is to run everything.
Decisions
panama updatebecomes the machine-update command. The git workflow moves topanama sync, unchanged in intent. One verb per job; nothing branches on state the user cannot see.installgains--upgrade, which skips the interview and trimsSTAGESto the five that need no answers. One stage list in the repository, so the two paths cannot drift.install-packagesruns when a content hash ofsetup/packages/**differs from the recorded one. Not a git range: Panama is developed in place, and a hand-edited uncommitted package list must still trigger an install.- A dirty tree is stashed, pulled over, and popped. The machine update is never blocked by uncommitted work.
- A conflicted pop resets the tree, keeps the stash, warns, and continues. Conflict markers must never reach a live config; the update proceeds against the clean upstream checkout.
- The migrate notification is untouched. It says "repairs are waiting" and it will keep doing exactly that.
panama synccommits before pulling, so it needs no stash at all.
Design
Command surface
| Command | Job |
|---|---|
panama update |
Bring this machine up to date. The routine command. |
panama sync |
Commit & push my edits. Today's cmd_update, renamed and simplified. |
panama upgrade |
Full ./install, interview included. New machine, or to change an answer. |
panama migrate |
Repairs only. Unchanged; still what the login notification runs. |
panama update
1. Record pre-pull HEAD.
2. Dirty tree? git stash push --include-untracked -m "panama-update-<epoch>"
3. git pull --ff-only (failure is reported, never fatal)
4. Stashed? git stash pop
on conflict: git reset --hard HEAD
leave the stash in place
record a warning for the summary
5. exec install --upgrade
Steps 1–4 are the only new logic in panama update; the packages gate below is
the only new logic in install. Everything after step 5 is install's existing
tail.
The pull is --ff-only. A diverged branch is a thing to resolve deliberately,
not something an update command should paper over; it reports and continues to
the stages, because the machine should still be brought in line with what is
checked out.
install --upgrade
install has no argument parsing today, so this adds the first. cmd_upgrade
already forwards "$@", so panama upgrade --upgrade would work but is
pointless and undocumented.
--upgrade changes exactly four things:
- The interview block (
install:91-98) is skipped. NoPANAMA_ANSWERSfile is created, so everyPANAMA_*answer is unset and each stage takes its documented empty-answer path. hostnamectlis skipped — there is no answer to apply.STAGESdropssetup-identityandinstall-hardware.- The closing message says the machine was updated rather than "Panama installed. Log out and choose the Hyprland session to start it."
Everything else is inherited unchanged and deliberately so: the sudo -v
keepalive (change-settings and install-packages both need root), the
per-stage failure collection, the migrations block, panama-doctor --summary,
the post-upgrade hook, and the exit code.
One correction to the migrations block is required. It currently baselines when
the marker directory is absent, on the reasoning that a machine with no markers
was just built from this checkout. That inference is only sound during a real
install. Under --upgrade it must always take run; the migrations are all
self-guarding and a no-op on a machine that does not need them.
The packages hash
STATE="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
hash_packages() {
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \
| sort | sha256sum | cut -d' ' -f1
}
install-packages is skipped when the hash matches $STATE/packages-hash.
-maxdepth 1 is deliberate and excludes setup/packages/extras/. An
answer-free run has an empty PANAMA_EXTRAS and installs no optional category,
so hashing those files would flip the hash, run the stage, install nothing extra,
and record the new hash as though it had. Optional categories cannot be
re-applied by panama update at all, because which ones this machine chose is
never recorded — the interview's answers are transient by design. Adding to a
category you already have is panama apps, which is what it is for. This is a
known limitation, not an oversight.
The hash is written only after the stage succeeds, mirroring the rule
panama-migrate already documents for its markers: a step that did not complete
has not happened, and recording it as done hides it forever.
Two consequences worth stating: a full ./install always runs the stage and
writes the hash regardless of any recorded value, and the first panama update
after this ships will run install-packages once, because no hash exists yet.
Both are correct.
panama update --packages forces the stage regardless of the hash, forwarding
--packages to install --upgrade. This is the one flag worth adding, because
the hash cannot see a package that dnf removed behind Panama's back.
panama sync
Today's cmd_update with the stash dance removed. Because the changes are
committed before the pull, there is nothing to stash:
1. Clean tree? Say so and stop. Pulling is `panama update`'s job now.
2. Show status + diff, including untracked files as new-file diffs.
3. Confirm, read a commit message, git add -A && git commit.
4. Behind upstream? git pull --rebase
5. Offer to push.
A rebase conflict lands on committed work, which is an ordinary and recoverable place to be, rather than on a stash.
Conflict handling
Verified against a throwaway repository rather than assumed:
- A conflicted
git stash popkeeps the stash entry. - Conflict markers are written into the working file. On this repository that
means directly into a live
~/.configpath via the symlink — a broken.qmlcan cost the running shell. git reset --hard HEADclears the markers, andstash@{0}survives it.
So on conflict: reset, keep the stash, and report it in the final summary rather than only in scrollback, where twenty minutes of dnf output will bury it.
! Your local changes conflicted with what was pulled.
They are safe at stash@{0}: panama-update-1787680000
Restore with: git stash pop
What does not change
bin/panama-migrate— no change of any kind.bin/panama-migrate-notify— still runspanama-migrate runin a terminal.- The seven stage scripts — none of them are edited. This works precisely because they already handle absent answers.
panama upgrade— same behaviour, same interview.
Files touched
| File | Change |
|---|---|
install |
Argument parsing; --upgrade skips interview + hostname, trims STAGES, forces migrate run, alters the closing message. Packages-hash gate around install-packages. |
bin/panama |
cmd_update rewritten; cmd_sync added (the old body, minus the stash); dispatcher, usage block and header comment updated. |
README.md |
The upgrade-path paragraph and the command table. |
config/dot/quickshell/manual/05-making-it-yours.md |
The line telling the reader panama update will offer to commit. |
tests/setup/ |
New contracts, below. |
Tests
Four contracts, each pinning a decision that would otherwise rot silently:
install --upgradenever invokes the interview. The regression that started this. Assertsetup/scripts/interviewis not executed.--upgraderuns exactly the five answer-free stages, and neversetup-identityorinstall-hardware.- The packages hash gates the stage, and is not written when the stage
exits non-zero. Note this is a narrow guarantee:
install-packageslogs and steps over individual package failures internally and still exits 0, so a single missing package does not hold the hash back. Only a stage-level failure does. - A conflicted pop leaves no conflict markers in the tree and keeps the stash. Runs against a temporary repository, as the verification above did.
Contract 2 is the one that matters most over time: it fails the moment a stage
is added to STAGES without a decision about which path owns it.
Verification
On this machine, after implementation:
panama test update # the new contracts
panama update # expect: no questions, one sudo prompt, packages skipped
panama update # expect: idempotent, still no questions
The second run is the real check. Everything must be a no-op, and
install-packages must be skipped on both.
Out of scope
- Any change to how migrations work. They are correct.
- A GUI entry point for updates. The notification covers repairs; a full update is a terminal action because it needs a password and prints a summary worth reading.
- Rewriting the seven stage scripts. Their empty-answer behaviour is what makes this design a flag rather than a refactor.