262 lines
11 KiB
Markdown
262 lines
11 KiB
Markdown
# 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:
|
||
|
||
```sh
|
||
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
|
||
|
||
1. **`panama update` becomes the machine-update command.** The git workflow
|
||
moves to `panama sync`, unchanged in intent. One verb per job; nothing
|
||
branches on state the user cannot see.
|
||
2. **`install` gains `--upgrade`**, which skips the interview and trims `STAGES`
|
||
to the five that need no answers. One stage list in the repository, so the
|
||
two paths cannot drift.
|
||
3. **`install-packages` runs when a content hash of `setup/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.
|
||
4. **A dirty tree is stashed, pulled over, and popped.** The machine update is
|
||
never blocked by uncommitted work.
|
||
5. **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.
|
||
6. **The migrate notification is untouched.** It says "repairs are waiting" and
|
||
it will keep doing exactly that.
|
||
7. **`panama sync` commits 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. No `PANAMA_ANSWERS` file is
|
||
created, so every `PANAMA_*` answer is unset and each stage takes its
|
||
documented empty-answer path.
|
||
- `hostnamectl` is skipped — there is no answer to apply.
|
||
- `STAGES` drops `setup-identity` and `install-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
|
||
|
||
```sh
|
||
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 pop` **keeps** the stash entry.
|
||
- Conflict markers are written into the working file. On this repository that
|
||
means directly into a live `~/.config` path via the symlink — a broken `.qml`
|
||
can cost the running shell.
|
||
- `git reset --hard HEAD` clears the markers, and `stash@{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 runs `panama-migrate run` in 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:
|
||
|
||
1. **`install --upgrade` never invokes the interview.** The regression that
|
||
started this. Assert `setup/scripts/interview` is not executed.
|
||
2. **`--upgrade` runs exactly the five answer-free stages**, and never
|
||
`setup-identity` or `install-hardware`.
|
||
3. **The packages hash gates the stage**, and is not written when the stage
|
||
exits non-zero. Note this is a narrow guarantee: `install-packages` logs 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.
|
||
4. **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:
|
||
|
||
```sh
|
||
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.
|