Files
command-center/omp-docs/tools/dotfiles-deploy.md
Frank 3022277167 Stop the deploy tier from reverting live theme state
Three findings from verifying the #41 merge against the live box with
`deploy.sh --check`. One commit because they share the deploy tier, its
documentation, and the first test file that tier ever had.

**1. Generated palette output was tracked, and sync would have restored it over a
live theme.** `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl` (`b3d3df4`,
2026-10-18) sat byte-identical to the live file — harmless while the palette held, and
`--check` was already proposing `ADOPT … noctalia.kdl`. The moment the theme changed, the
next sync would have quarantined the live file and reinstated 2026-10-18's colours.
`deploy.sh` now refuses the class outright (`SKIP … (generated theme output)`), `.gitignore`
carries the matching patterns so it cannot be re-added, and the file is untracked.

**2. A `zz` override older than the file it overrides.** `dotfiles/zz-blankpad/niri/…/
layout.kdl` held focus ring `width 2` / `#e01b24` (`7b7dbf5`, 2026-10-17) while base
absorbed `b3d3df4` (2026-10-18): `width 4`, `#ff8c00`. Stow order makes `zz-*` win, and a
cross-package owner change relinks *silently* — no quarantine, no warning. Live was linked
to base, the newer intent, so `--check` printed `RELINK … layout.kdl` every run and one
unattended `deploy.sh` would have reverted the amber focus ring. The duplicate is deleted;
base owns the file.

**3. Two topologies I had asserted and not measured.** The media law and the `.gitignore`
comment both said `~/.local/share/noctalia/wallpapers` symlinks into the repo, so deleting
the tracked copies would blank the desktop. Measured: it is a real directory of real files
that Noctalia owns; the repo held a duplicate, and the #41 merge deleted that duplicate
without the desktop flickering. The correct general law replaces the false one — untracking
a path deletes the file in every other checkout, `git rm --cached` on the branch
notwithstanding — and the restore procedure now points at the app's directory, which
`deploy.sh` does not manage.

The guard is `tests/deploy.bats`, black-box over exit status, stdout, the symlink shape
under `$HOME`, and the quarantine directory. Before the change: test 1 and test 2 fail,
test 2 because the live palette really is quarantined and reverted. After: 4/4, including
the control that an ordinary identical config is still adopted — an exclusion nobody can
turn off is just a broken tool.

Verification: `bats tests/` 66/66, `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0
issues, and live `deploy.sh --check` on blankpad reports `SKIP … (generated theme output)`
with `actions=0` instead of `ADOPT … noctalia.kdl`. The real `deploy.sh` run that clears the
remaining `RELINK` lines is deliberately left until this is in main — running it from a
worktree would repoint live symlinks into a checkout the heartbeat reaps.

Refs #42
2026-10-20 17:08:06 -07:00

7.4 KiB

type, title, description, tags, timestamp, verified
type title description tags timestamp verified
reference Dotfiles deployment tier How dotfiles/ packages become live configuration — stow-equivalent symlinks, host override packages, and the two laws about theme media and systemd enable symlinks.
dotfiles
deployment
systemd
noctalia
2026-10-20 2026-10-20

Dotfiles deployment tier

dotfiles/ holds home-relative trees and scripts/deploy.sh is a stow-equivalent: the repo checkout is the deployment, and deploying replaces live files with symlinks into the repo, so git pull + deploy.sh is the whole propagation story. One directory per tool (niri, ghostty, gtk-3.0, systemd-user, …); a package named zz-$(hostname) is stowed last and its files replace symlinks coming from base packages.

Mode Effect
deploy.sh --check dry run — adopts identicals, reports what would be linked, lists divergents, writes nothing. Run this first, always
deploy.sh (sync) adopt identicals, link new, quarantine divergents into .quarantine/ and replace them
deploy.sh --adopt like sync, but divergents are reported and skipped rather than quarantined

Two package shapes are not symlinks: dconf/dump.txt is loaded with dconf load /, and systemd-user units get daemon-reload plus enable --now only when their ExecStart binary resolves on the current machine, so a unit belonging to another box stays inert instead of crash-looping.

The tracked unit is the file, not the directory. A live directory can be a real directory and still be fully tracked, because every file inside it is a symlink into the repo (niri is 9 tracked / 1 box-only; gtk-3.0 is 3/0). Any audit that classifies by directory mislabels the tree — classify per file with readlink -f. The live census and its classifier command are in home-config-census.

Law: media the theme pipeline consumes is content, not config

Wallpapers live at dotfiles/noctalia/.local/share/noctalia/wallpapers/ and are gitignored. They were tracked until 2026-10-20 — 23 files, 80 MiB — which meant every clone of a configuration repo carried a media library no diff can review, and the payload only ever grows because deleting a tracked blob from the tree does not delete it from history. The topology is not what one would guess: Noctalia keeps its own library at ~/.local/share/noctalia/wallpapers/ as a real directory of real files, so the tracked tree was a duplicate of it and never its source — measured 2026-10-20, when the merge of #41 deleted the repo copies and the desktop did not flicker. Two consequences to know before untracking media: the copy that survives belongs to the application, not to the repo; and untracking deletes the file in every other checkout, git rm --cached on the branch notwithstanding.

Restore on a fresh box: the library lives in ~/.local/share/noctalia/wallpapers/, a path deploy.sh does not manage. Every filename is wallhaven_<id>.<ext> and the ID is the whole address — curl -L -o <name> "https://wallhaven.cc/w/<id>" into that directory puts the set back. Losing it is an acceptable outcome; 80 MB per clone forever is not.

Law: never systemctl --user enable a unit from a worktree

enable writes ~/.config/systemd/user/default.target.wants/<unit> as a symlink to the unit path it was handed, and that path is absolute. The heartbeat reaps wt/* worktrees older than seven days (live-sync), so a unit enabled from a worktree turns into a dangling symlink while systemctl --user is-enabled still answers enabled — the unit is dead and the status lies about it. Found on 2026-10-20: blankpad-desk-autoswitch.service pointed into the reaped wt/de-redesign-20261009, so desk auto-switching had been silently off since the reap.

Check the link target before the unit file:

ls -l ~/.config/systemd/user/default.target.wants/
# every target must resolve; a dangling one is a reaped worktree, not a config bug

Fix is to relink into the deployment checkout (~/.omp/dotfiles/…), systemctl --user daemon-reload, then confirm with is-active — is-enabled proves nothing.

Law: a generated file must never be a required include

~/.config/niri/config.kdl ends with include "noctalia.kdl" — no optional=true. That file is noctalia's palette output (#ff8c00, #f43841, one re-stamp 2026-10-18 17:36:27), so it is machine state and correctly untracked. But on a fresh box deploy.sh links config.kdl first, and niri then refuses to start until noctalia has run once and written the file — the compositor is bricked by the order of operations, not by a bad config. Close it at the include, which survives someone skipping the theme pipeline: mark generated includes optional=true, as the display.kdl and switch-events.kdl lines in the same file already are.

Desk choice is live state, never config

~/.config/niri/cfg/display.kdl is a relative symlink to whichever profile is active (desk-1-home.kdl, desk-2-portable.kdl), written by desk.sh. Both profiles are tracked in dotfiles/zz-blankpad/; the pointer is not, because which desk the box sits on is not a fact about the repo. A fresh box therefore starts with no output configuration — config.kdl includes display.kdl with optional=true, so niri comes up usable but with default monitors until desk.sh is run once.

Law: generated theme output is never deployed

noctalia rewrites palette output in place — noctalia.*, themes/noctalia*, colors/noctalia*, noctalia-* (the classes the census proves by same-millisecond mtime). A tracked copy of one is a stale snapshot of a colour scheme, and the failure is quiet: dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl (commit b3d3df4, 2026-10-18) sat byte-identical to the live file for two days, harmless, while --check cheerfully proposed ADOPT … noctalia.kdl. The moment the palette changed, the next sync would have quarantined the live file and restored 2026-10-18's colours — a tool silently reverting a user-visible theme choice.

deploy.sh now refuses the class (SKIP … (generated theme output)), .gitignore carries the matching patterns so it cannot be re-added, and tests/deploy.bats pins both directions: generated output is never adopted, and an ordinary identical config still is. Untracked in #43, Refs #42.

Law: a zz override older than the file it overrides is a time bomb

Stow order means zz-$HOST wins, and when both packages carry the same path, sync relinks it silently — the script's own comment: "managed file changing owners between packages: silent relink, nothing to quarantine". dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl held focus ring width 2 / active-color "#e01b24" from 7b7dbf5 (2026-10-17) while the base package absorbed b3d3df4 (2026-10-18): width 4, "#ff8c00". Live was linked to base, the newer intent, so --check printed RELINK .config/niri/cfg/layout.kdl on every run and one unattended deploy.sh would have reverted the amber focus ring with no quarantine and no warning. The duplicate is deleted in #43.

Rule: a zz file exists only to differ. When base absorbs the change, delete the zz copy in the same commit — and never read RELINK lines from --check as cosmetic noise; they are the sound of a stale override waiting for someone to run the tool for real.