panama update: one command, no questions, and no gap it cannot see
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user