From 064abe42c349cf976a8e07a132b702428495d444 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Tue, 30 Jun 2026 21:10:33 +1000 Subject: [PATCH] fix: refresh image-managed dotfiles over the home volume on start (#89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pull/compose deployment path mounts only squarebox-home:/home/dev, so the named volume shadows the image-baked dotfiles (.bashrc, starship.toml). Docker seeds a volume from the image only at first creation, so upgraded containers silently keep whatever dotfile shipped when their volume was born — dotfile updates never reach them. This already bit rc1→rc2: PR #88's fzf keybindings stayed dead on upgraded shells. Fix: ship the managed dotfiles to a non-volume path (/usr/local/lib/squarebox/dotfiles/) and have squarebox-entrypoint run refresh-dotfiles.sh on every start to re-seed them over the volume copy. The refresh skips any path the operator bind-mounted (the desktop install path), so host-managed files are never clobbered, and is best-effort (never aborts boot). Done as root before the privilege drop so files get the resolved PUID:PGID. Verified in a real container: staled volume bashrc is restored from the image, and a read-only bind-mounted host bashrc is preserved untouched. - scripts/squarebox-refresh-dotfiles.sh: new, single source of truth - squarebox-entrypoint: call it at both exec points - Dockerfile: ship managed dotfiles to the non-volume source path - e2e: new `dotfiles` suite (5 tests), wired into the sqrbx-update CI job - uat-checklist: manual cross-version upgrade scenario - docs: CLAUDE.md + bashrc header explain the dual mechanism Closes #89 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e.yml | 6 +++ CLAUDE.md | 2 + Dockerfile | 15 ++++++- dotfiles/bashrc | 9 +++-- scripts/e2e-test.sh | 39 ++++++++++++++++++- scripts/squarebox-entrypoint.sh | 9 ++++- scripts/squarebox-refresh-dotfiles.sh | 56 +++++++++++++++++++++++++++ uat-checklist.md | 9 +++++ 8 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 scripts/squarebox-refresh-dotfiles.sh diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a59674c..3a18809 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -299,6 +299,12 @@ jobs: -v "$PWD/scripts:/workspace/scripts:ro" \ squarebox:test bash -c 'scripts/e2e-test.sh update' + - name: "Section 10: dotfile refresh (#89)" + run: | + docker run --rm \ + -v "$PWD/scripts:/workspace/scripts:ro" \ + squarebox:test bash -c 'scripts/e2e-test.sh dotfiles' + # ── Dev Container ────────────────────────────────────────────────────── devcontainer: needs: build-amd64 diff --git a/CLAUDE.md b/CLAUDE.md index b1f10e2..3bf72c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,8 @@ docker start -ai squarebox The `squarebox-home` named volume holds per-user state. Bind mounts at sub-paths inside `/home/dev` (`.bashrc`, `.config/starship.toml`, `.config/lazygit`, `.config/git`) override the volume so repo-managed files stay in lockstep with the image. +**Pull/compose path (no bind mounts):** the `docker-compose.yml` / GHCR install mounts only `squarebox-home:/home/dev`, so the named volume would otherwise shadow the image-baked dotfiles and they'd go stale on upgrade (issue #89). To prevent this, the managed dotfiles also ship to a non-volume path (`/usr/local/lib/squarebox/dotfiles/`), and `squarebox-entrypoint` runs `refresh-dotfiles.sh` on every start to re-seed `.bashrc`/`starship.toml` from there over the volume copy. The refresh skips any path the operator bind-mounted (the desktop path above), so it never clobbers host-managed files. + Replace `docker` with `podman` above if using Podman. The `install.sh` script auto-detects the runtime; override with `SQUAREBOX_RUNTIME=docker|podman`. The `install.sh` script automates initial setup (clone, build, create container, add `sqrbx` shell alias). A `.devcontainer/devcontainer.json` is also provided for VS Code Dev Containers / Codespaces. diff --git a/Dockerfile b/Dockerfile index 0dffd8b..b56dfa1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -142,10 +142,21 @@ COPY scripts/squarebox-setup.sh /usr/local/bin/sqrbx-setup COPY scripts/sqrbx-learn /usr/local/bin/sqrbx-learn COPY scripts/squarebox-help.sh /usr/local/bin/sqrbx-help COPY scripts/squarebox-entrypoint.sh /usr/local/bin/squarebox-entrypoint +COPY scripts/squarebox-refresh-dotfiles.sh /usr/local/lib/squarebox/refresh-dotfiles.sh COPY scripts/lib/tools.yaml /usr/local/lib/squarebox/tools.yaml COPY scripts/lib/tool-lib.sh /usr/local/lib/squarebox/tool-lib.sh + +# Image-managed dotfiles also live under a non-volume path so the entrypoint can +# refresh them into the squarebox-home volume on every start. Without this the +# volume shadows the /home/dev image layer and dotfile updates never reach +# upgraded containers (issue #89). The /home/dev copies below still seed a fresh +# volume; these are the source of truth the refresh re-applies thereafter. +COPY dotfiles/bashrc /usr/local/lib/squarebox/dotfiles/bashrc +COPY starship.toml /usr/local/lib/squarebox/dotfiles/starship.toml + RUN chmod +x /usr/local/lib/squarebox/setup.sh \ /usr/local/lib/squarebox/motd.sh \ + /usr/local/lib/squarebox/refresh-dotfiles.sh \ /usr/local/bin/sqrbx-update \ /usr/local/bin/sqrbx-setup \ /usr/local/bin/sqrbx-learn \ @@ -168,7 +179,9 @@ ENV PGID=1000 # The .bashrc lives in dotfiles/ on the host so install.sh can bind-mount it # into the container — keeping it in sync with the repo while shell history # and per-user state stay in the squarebox-home named volume. The COPY here -# is what seeds a fresh volume; subsequent runs see the bind-mounted version. +# seeds a fresh volume; the desktop install path then bind-mounts the host copy, +# and the pull/compose path keeps it current via the entrypoint dotfile refresh +# (issue #89) since there is no bind-mount in that path. COPY --chown=dev:dev dotfiles/bashrc /home/dev/.bashrc diff --git a/dotfiles/bashrc b/dotfiles/bashrc index 4f0e365..2a9e827 100644 --- a/dotfiles/bashrc +++ b/dotfiles/bashrc @@ -1,7 +1,8 @@ -# squarebox /home/dev/.bashrc — managed by the repo, bind-mounted read-only -# from $INSTALL_DIR/dotfiles/bashrc by install.sh / install.ps1. Edits to this -# file inside the container will not persist across rebuilds; edit the copy in -# ~/squarebox/dotfiles/bashrc on the host instead. +# squarebox /home/dev/.bashrc — managed by the repo. On the desktop install path +# it is bind-mounted read-only from $INSTALL_DIR/dotfiles/bashrc by install.sh / +# install.ps1; on the pull/compose path the entrypoint re-seeds it from the image +# on every start (refresh-dotfiles.sh, issue #89). Either way, edits made inside +# the container do not persist — edit ~/squarebox/dotfiles/bashrc on the host. # Bail out if not running interactively. case $- in diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index 4f9c73b..8e42ed3 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -376,11 +376,46 @@ suite_setup_rerun() { run_test "9.6 setup.sh --rerun parses cleanly" bash -c '/usr/local/lib/squarebox/setup.sh --rerun git /dev/null \ + && printf '\n# __e2e_stale_marker__\n' >> "$HOME/.bashrc" \ + && "$refresh" \ + && ! grep -q '__e2e_stale_marker__' "$HOME/.bashrc" \ + && cmp -s "$HOME/.bashrc" "$src"; then + PASS_COUNT=$((PASS_COUNT + 1)) + echo "ok ${TEST_NUM} - 10.4 refresh restores a staled bashrc" + else + FAIL_COUNT=$((FAIL_COUNT + 1)) + echo "not ok ${TEST_NUM} - 10.4 refresh restores a staled bashrc" + cp -f /tmp/bashrc.e2e.bak "$HOME/.bashrc" 2>/dev/null || true + fi + + # 10.5 the refresh never exits non-zero (must not be able to abort boot) + run_test "10.5 refresh-dotfiles.sh exits 0" "$refresh" +} + # ── Main ───────────────────────────────────────────────────────────────── usage() { echo "Usage: $0 " - echo "Suites: tools, shell, setup, setup-editors, update, devcontainer, setup-rerun" + echo "Suites: tools, shell, setup, setup-editors, update, devcontainer, setup-rerun, dotfiles" exit 1 } @@ -398,6 +433,7 @@ main() { update) suite_update ;; devcontainer) suite_devcontainer ;; setup-rerun) suite_setup_rerun ;; + dotfiles) suite_dotfiles ;; all) suite_tools suite_shell @@ -405,6 +441,7 @@ main() { suite_update suite_devcontainer suite_setup_rerun + suite_dotfiles ;; *) usage ;; esac diff --git a/scripts/squarebox-entrypoint.sh b/scripts/squarebox-entrypoint.sh index 5ee6463..4ee363d 100755 --- a/scripts/squarebox-entrypoint.sh +++ b/scripts/squarebox-entrypoint.sh @@ -46,10 +46,17 @@ if [ "$(id -u)" = "0" ]; then chown "$PUID:$PGID" /workspace 2>/dev/null || true fi + # Re-seed image-managed dotfiles over the (volume-shadowed) home so image + # updates reach upgraded containers — issue #89. Done as root, before the + # privilege drop, so refreshed files can be chowned to the resolved dev user. + /usr/local/lib/squarebox/refresh-dotfiles.sh "$PUID:$PGID" || true + # Drop to dev. --init-groups picks up dev's supplementary groups; numeric # ids resolve back to the (now-remapped) dev passwd entry. exec setpriv --reuid "$PUID" --regid "$PGID" --init-groups -- "$@" fi -# Already unprivileged (rootless Podman, or --user override): run as-is. +# Already unprivileged (rootless Podman, or --user override): run as-is, but +# still refresh managed dotfiles (owned by the running user; no chown needed). +/usr/local/lib/squarebox/refresh-dotfiles.sh || true exec "$@" diff --git a/scripts/squarebox-refresh-dotfiles.sh b/scripts/squarebox-refresh-dotfiles.sh new file mode 100644 index 0000000..7654eae --- /dev/null +++ b/scripts/squarebox-refresh-dotfiles.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# squarebox-refresh-dotfiles — re-seed image-managed dotfiles into /home/dev. +# +# Why: /home/dev is the persisted `squarebox-home` named volume. Docker seeds a +# volume from the image only when the volume is first created, so the dotfiles +# baked into /home/dev (.bashrc, starship.toml) are shadowed by the volume on +# every later start. An upgraded container therefore keeps whatever dotfile +# shipped when its volume was born — image updates never reach it (issue #89). +# +# Fix: the managed copies also live under /usr/local/lib/squarebox/dotfiles/ — a +# plain image path the volume cannot shadow. The entrypoint runs this on every +# start to copy the current managed dotfile back over the (possibly stale) volume +# copy, so dotfile changes ship with the image like every other tool. +# +# Skips any target the operator bind-mounted (the desktop install.sh path mounts +# dotfiles/bashrc and starship.toml from the host repo, often read-only — the +# host owns those and keeps them in sync itself). +# +# Arg 1 (optional): uid:gid to chown refreshed files to. The entrypoint passes +# the resolved PUID:PGID when it runs this as root before dropping privileges; +# omit it when running unprivileged and the copying user owns the result. +# +# Deliberately NOT `set -e`: refreshing a convenience dotfile must never abort +# container boot. Every step is best-effort and the script always exits 0. +set -uo pipefail + +owner="${1:-}" + +# src:dest pairs — src is the non-volume image copy, dest is the volume path. +pairs=( + "/usr/local/lib/squarebox/dotfiles/bashrc:/home/dev/.bashrc" + "/usr/local/lib/squarebox/dotfiles/starship.toml:/home/dev/.config/starship.toml" +) + +for pair in "${pairs[@]}"; do + src="${pair%%:*}" + dest="${pair#*:}" + + [ -f "$src" ] || continue + + # Operator bind-mounted this path — host-managed, leave it alone. + if mountpoint -q -- "$dest" 2>/dev/null; then + continue + fi + + # Already current — don't churn the mtime on every start. + if cmp -s "$src" "$dest" 2>/dev/null; then + continue + fi + + mkdir -p "$(dirname "$dest")" 2>/dev/null + cp -f "$src" "$dest" 2>/dev/null || continue + [ -n "$owner" ] && chown "$owner" "$dest" 2>/dev/null +done + +exit 0 diff --git a/uat-checklist.md b/uat-checklist.md index bc9af8e..14e6af3 100644 --- a/uat-checklist.md +++ b/uat-checklist.md @@ -28,6 +28,15 @@ - [ ] After rebuild: `/workspace/.squarebox/` selections reused (no re-prompts) - [ ] After rebuild: GH CLI stays authenticated +## Pull/Compose Upgrade (cross-version, #89) +> Real cross-version test: start a container on an **older** image so the +> `squarebox-home` volume is seeded with that image's dotfiles, then upgrade the +> image and restart. The e2e `dotfiles` suite only simulates this within one +> image; this verifies it across an actual version bump. +- [ ] Start old image → stop → `docker compose pull` newer image → up: `~/.bashrc` matches the new image (entrypoint refresh defeated the volume shadow) +- [ ] fzf keybindings (Ctrl+R / Ctrl+T / Alt+C) and git tab-completion work in the upgraded container +- [ ] Desktop install path: host-edited `~/squarebox/dotfiles/bashrc` (bind-mounted) is NOT clobbered by the refresh + ## Dev Container - [ ] VS Code "Reopen in Container" builds and connects - [ ] Manual `sqrbx-setup` works in VS Code integrated terminal