<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archsetup/scripts, branch main</title>
<subtitle>Builds a full dev workstation from a bare Arch Linux install.
</subtitle>
<id>https://git.cjennings.net/archsetup/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/archsetup/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/'/>
<updated>2026-07-25T20:00:44+00:00</updated>
<entry>
<title>refactor(test): share runner and scenario contracts</title>
<updated>2026-07-25T20:00:44+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-25T20:00:44+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=d52da959bc416e09bc0f3e54994563bd80e19c64'/>
<id>urn:sha1:d52da959bc416e09bc0f3e54994563bd80e19c64</id>
<content type='text'>
</content>
</entry>
<entry>
<title>fix(installer): eight fixes from an overnight bug-hunt and its review</title>
<updated>2026-07-24T17:19:04+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-24T17:19:04+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=3becfac66dc569e71d0f6085fb56e92cfa6d626a'/>
<id>urn:sha1:3becfac66dc569e71d0f6085fb56e92cfa6d626a</id>
<content type='text'>
I squashed these because the per-bug reasoning lives in todo.org, which this commit carries.

Two could cost a machine. configure_initramfs_hook swapped the udev hook for systemd on a LUKS root, leaving a standalone encrypt hook under an init that never runs it. The rebuild succeeds and the installer exits clean, then the root won't unlock at the next boot. trim_firmware ran pacman -Rdd against twelve firmware packages behind a DMI gate reading product_name, where "Framework" never appears. That left it dead on the hardware it targets, and dangerous to fix the obvious way: this machine is a Framework Desktop whose Ryzen iGPU needs the amdgpu firmware. It refuses on PCI modalias evidence now.

wipedisk discarded before it checked. blkdiscard ran with -f, which disables the exclusive open, so picking the wrong disk destroyed a live filesystem and then reported that nothing had happened.

Four more are smaller. The NVIDIA preflight aborted dwm and headless installs over a driver floor they never need. zfs-replicate exited 0 after every dataset failed. Unattended installs blocked on two prompts, and the first fix for that inherited a [Y/n] default into passwordless console login. A fresh install left the dotfiles repo permanently dirty.

The review found a pattern worth more than any single fix. Helpers had thorough tests and none proved they were called. Deleting the call left five suites green, including the guard on that pacman -Rdd. CALL_SITES now pins nine caller/callee pairs.

The suite runs 341 tests at exit 0, with no new shellcheck findings. I proved every guard by deleting it and watching the intended test go red.
</content>
</entry>
<entry>
<title>test: fix the weak assertions from the S6/S7 audit</title>
<updated>2026-07-21T04:44:35+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-21T04:44:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=179fbd55795c27ed388860e3c6b3d8c0651422ac'/>
<id>urn:sha1:179fbd55795c27ed388860e3c6b3d8c0651422ac</id>
<content type='text'>
The Hyprland socket check passed a shell glob to `test -S`, which breaks at both edges: zero matches leaves the literal pattern and several instances pass multiple args, failing for reasons unrelated to the socket. It now resolves one socket with find -type s.

The gen_tokens degenerate-marker test asserted properties loose enough to pass on garbled output. It now pins the exact reconstruction as a characterization, with the impossible-input caveat stated, so any change to the branch surfaces.

The gallery tick count leaned on split-string omit-nulls arithmetic that happened to equal the match count. It now counts occurrences directly. gallery-widget-write-svg, the last uncovered public entry point, gets a Normal-case test.
</content>
</entry>
<entry>
<title>fix(test): harden four VM-framework paths from the S5 audit</title>
<updated>2026-07-21T04:40:56+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-21T04:40:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=866d327f0bad3a0730801a22654bac45e02ee14b'/>
<id>urn:sha1:866d327f0bad3a0730801a22654bac45e02ee14b</id>
<content type='text'>
init_vm_paths suffixed the disk and NVRAM by FS_PROFILE but left PID_FILE, MONITOR_SOCK, and SERIAL_LOG shared. A concurrent btrfs and zfs run read each other's PID file, so the second run's stop logic could kill the first run's VM. All runtime paths now carry the suffix.

kill_qemu sent kill -9 and returned without waiting, so the force-kill fallback could run qemu-img snapshot against a qcow2 the dying qemu still held locked (the restore failed silently behind || true, leaving the base image dirty). It now reaps or polls the process to death before returning.

debug-vm.sh hardcoded the btrfs base-disk name, so FS_PROFILE=zfs booted the wrong base or fatalled. It now takes DISK_PATH from init_vm_paths.

Both runners reported a completion-marker grep as "ArchSetup Exit Code," but the installer runs detached with set -e off, so its real exit status was never captured -- a run that errored mid-way and still reached its last line reported exit 0. The flag is now ARCHSETUP_COMPLETED with the report labeled honestly; Testinfra stays the pass/fail authority.
</content>
</entry>
<entry>
<title>fix(test): count a diagnose miss as a failed net scenario</title>
<updated>2026-07-21T04:33:02+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-21T04:33:02+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=cf211cd8dd8dd0ff18673c88a3eef2c63549d6d5'/>
<id>urn:sha1:cf211cd8dd8dd0ff18673c88a3eef2c63549d6d5</id>
<content type='text'>
The scenario_diagnose_expect else-branch printed its FAIL line but never forced a non-zero subshell exit. A scenario whose only failure was the diagnose check counted as passed, so the run printed "all scenarios passed" and exited 0 over a real net-doctor diagnosis regression.

The miss now sets a per-scenario rc that carries to the subshell exit. The fix and assert still run first, since the repair result is worth having either way.

The new harness drives the real script with stubbed ssh/rsync/jq and a controllable fake scenario, pinning all four check outcomes.
</content>
</entry>
<entry>
<title>fix(sounds): make normalize-notify-sounds write atomically and clean up</title>
<updated>2026-07-20T20:56:26+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-20T20:56:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=a29769e0f46aa2e2b9ed3662d58d1395a665b5d5'/>
<id>urn:sha1:a29769e0f46aa2e2b9ed3662d58d1395a665b5d5</id>
<content type='text'>
The script re-encoded each notify sound with `cat "$tmp" &gt; "$f"`, which truncates the target first -- a short or empty encode left a corrupt, repo-tracked sound file. The mktemp also had no trap, so an interrupted encode leaked a temp.

Now it resolves the real target with `readlink -f` (SOUND_DIR is usually the stow-symlinked ~/.local copy), stages the temp beside it, guards on a non-empty encode, and moves it into place. The mv is atomic on one filesystem and replaces the real file, so the stow symlink still points at it. An EXIT trap removes the in-flight temp when a failed encode aborts the run.
</content>
</entry>
<entry>
<title>fix(wireguard): bring the tunnel down before the rename modify</title>
<updated>2026-07-20T20:46:09+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-20T20:46:09+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=36daf763d4007eccbb5608de0e77278c2f5a1135'/>
<id>urn:sha1:36daf763d4007eccbb5608de0e77278c2f5a1135</id>
<content type='text'>
`nmcli connection import` activates a full-tunnel (0.0.0.0/0) Proton profile immediately. The import loop renamed and set autoconnect off, then brought the tunnel down. Under `set -euo pipefail`, a failed modify aborted the script before that down ran, leaving a live full-tunnel VPN routing all traffic through Proton -- the exact unasked-for VPN the script exists to prevent. The next-run guard catches the leftover profile but not the still-active tunnel.

Moved the down ahead of the modify. It targets the UUID, which is stable across the rename, so downing first is safe and no failure between import and modify can leave the tunnel up.
</content>
</entry>
<entry>
<title>fix(security): decrypt the cmail password under a tight umask</title>
<updated>2026-07-20T17:44:01+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-20T17:44:01+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=dffecf528cd20af2bd4437df567896ce03cabe2f'/>
<id>urn:sha1:dffecf528cd20af2bd4437df567896ce03cabe2f</id>
<content type='text'>
gpg writes its output at the process umask (often 0644), so the plaintext mail password was world-readable between the decrypt and the chmod. I extracted decrypt_to_secure, which runs the decrypt in a 0077 umask subshell so the file is 0600 from creation. A fake-gpg test pins that the write runs under the tight umask.
</content>
</entry>
<entry>
<title>fix: order pacman safety hooks</title>
<updated>2026-07-19T00:46:05+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-19T00:46:05+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=80017365527b612ca8763b29c5771fc41d58def5'/>
<id>urn:sha1:80017365527b612ca8763b29c5771fc41d58def5</id>
<content type='text'>
Install snapshot and live-update hooks before mkinitcpio removal, migrate legacy paths, and cover the ordering invariant.
</content>
</entry>
<entry>
<title>test(net): add the doctor control-plane repair scenarios and runner</title>
<updated>2026-07-11T11:01:17+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-11T11:01:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=4c1b9811927aee572624aa03527dab9ec5f3a12a'/>
<id>urn:sha1:4c1b9811927aee572624aa03527dab9ec5f3a12a</id>
<content type='text'>
net Phase 1's three privileged fixes (unmask-nm, disable-rival, chmod-keyfile) can't be verified against fakes past the point where real sudo and a running NetworkManager matter, and the broken states they repair are too dangerous to stage on a daily driver. I added the live-verification harness they need.

Three scenario files encode the break/fix/assert for each: mask NetworkManager, enable a rival dhcpcd, and chmod a profile keyfile to 0644, then run the real net doctor --fix and assert the repair. Each also names the read-only diagnose verdict it expects, so a run that breaks catches whether the fault is in the diagnosis or the fix. run-net-scenarios.sh rsyncs the net and panelkit trees into a target VM over ssh and drives the scenarios there.

The target is a booted VM, not a container: NetworkManager needs a real kernel and network stack to actually run, so an nspawn container can only show the filesystem-level half (the mask symlink, the keyfile mode) and not "NM is active". A disposable VM you revert is the right environment.

The scenario break/fix/assert logic is the reviewed part. The ssh transport plumbing hasn't been exercised against a live target yet, so it's marked first-draft and may need a shakeout on the first real run. The by-hand equivalent already lives in the manual-testing checklist.
</content>
</entry>
</feed>
