From 3e1143f1d3a83925f1010bd24653203ed6f7bc8b Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Fri, 17 Jul 2026 13:34:53 +0000 Subject: [PATCH] Refuse to change versions under existing boxes; file the migration as #67 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per @danmt on #66: hatch first, the version-aware migration as its own issue. Building the host stack from the installer means an upgrade is no longer a tree swap — it reaches under every box attached to that stack. So the installer now declines to guess. Same version and ref: it says so and changes nothing. Version or ref change with boxes on the host: it refuses, lists them, and does so BEFORE $DEST is touched, so a refusal leaves the working install intact. No boxes: nothing to lose, proceed. BOX_FORCE_UPGRADE=1 overrides, and the drill sets it, because arriving on a dirty host and wiping it is the drill's job. Ref, not just VERSION: a branch and main carry the same VERSION string, so VERSION alone would call an install of this very branch "unchanged" and skip the hatch. Both tag generations count as boxes — a pre-rename user.claudebox=1 box is just as much someone's work as a current one. The box query runs unprivileged first and escalates only if the socket refuses: anyone who owns boxes is already in incus-admin, and an installer should not demand a sudo password merely to look. The error deliberately does not suggest snapshot -> rm -> restore --from: 'box rm' deletes a box AND every snapshot it has, so that path loses the data at the rm. It says to copy anything needed out of the box first. Raised on #67. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 15 ++++++++--- README.md | 15 ++++++++--- drill/drill.sh | 6 +++++ install.sh | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a5a7d..884c6e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,10 +48,17 @@ which records not just what changed but what each drill run proved. `install.sh`, since that is what triggers setup now. - **`install.sh` runs the host setup itself** (#64) — it printed a warning and left you a command to run, so the install reported success and `box new` - failed on a host with no Incus. Since `setup-host` is idempotent, doing this - on every install is also how an upgraded host picks up stack changes. - `BOX_SKIP_SETUP_HOST=1` opts out; if setup fails, the install still stands - and says what to re-run. + failed on a host with no Incus. `BOX_SKIP_SETUP_HOST=1` opts out; if setup + fails, the install still stands and says what to re-run. +- **`install.sh` refuses to change versions under existing boxes** — building + the host stack from the installer means an upgrade reaches under every box + attached to that stack, so it is no longer only a tree swap. Same version and + ref: says so, changes nothing. Version or ref change with boxes on the host + (either tag generation): refuses, loudly, listing them — and refuses *before* + `$DEST` is touched, so the working install survives the refusal. No boxes: + proceeds. `BOX_FORCE_UPGRADE=1` overrides; the drill sets it, since wiping + boxes is its job. The version-aware upgrade that migrates instead of refusing + is #67. ## 0.5.0 — 2026-07-15 diff --git a/README.md b/README.md index 8fbe035..8d422de 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,18 @@ curl -fsSL https://raw.githubusercontent.com/heavy-duty/box/main/install.sh | ba Installs the tree to `~/.local/share/box` and links `box` onto your `PATH`, then runs the host setup below for you (it may ask for `sudo`; set -`BOX_SKIP_SETUP_HOST=1` to opt out and run it yourself). Re-run any time to -upgrade — upgrading re-applies the host stack, and from a pre-0.4.0 install -also retires the old `claudebox` symlink. (No `git clone` needed.) +`BOX_SKIP_SETUP_HOST=1` to opt out and run it yourself). Upgrading from a +pre-0.4.0 install also retires the old `claudebox` symlink. (No `git clone` +needed.) + +**Upgrading, and boxes.** Because the installer builds the host stack, an +upgrade reaches under the boxes attached to it — so it is deliberate about it. +Re-running the same version changes nothing and says so. Changing version on a +host that has boxes **refuses**, lists them, and leaves your install exactly as +it was; deal with the boxes and re-run, or say `BOX_FORCE_UPGRADE=1` to upgrade +over them on purpose (boxes are not deleted, but the stack is rebuilt beneath +them). With no boxes on the host, it just upgrades. Migrating boxes across an +upgrade instead of refusing is [#67](https://github.com/heavy-duty/box/issues/67). ## One-time host setup (Ubuntu 24.04 / Debian 13) diff --git a/drill/drill.sh b/drill/drill.sh index 38e2d94..5de6e8a 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -202,6 +202,12 @@ EOF export BOX_SKIP_SETUP_HOST=1 fi + # The installer refuses to change versions on a host that still has boxes. + # That hatch is for humans with work to lose; the drill's whole job is to + # arrive on a dirty host, wipe every box it recognises (below) and re-prove + # the stack from there — so it opts out, deliberately and in one place. + export BOX_FORCE_UPGRADE=1 + BOX_REPO="$REPO" BOX_REF="$REF" \ bash -c "$(curl -fsSL "https://raw.githubusercontent.com/$REPO/$REF/install.sh")" \ || { echo "install failed"; exit 1; } diff --git a/install.sh b/install.sh index fae5cc6..fbc1170 100755 --- a/install.sh +++ b/install.sh @@ -47,6 +47,77 @@ EXTRACTED="$(find "$TMPDIR" -mindepth 1 -maxdepth 1 -type d | head -n1)" [ -n "$EXTRACTED" ] || die "could not find the extracted source directory in archive" [ -f "$EXTRACTED/bin/box" ] || die "archive does not contain bin/box — is $REPO@$REF correct?" +# --- upgrade hatch --------------------------------------------------------- +# This installer builds the host stack itself now, and every box on the host is +# attached to that stack — so a version change here is not just a tree swap, it +# reaches under running boxes. Until the version-aware migration exists (#67), +# refuse rather than guess: if this would change what is installed AND there are +# boxes on the host, stop and let a human decide. Checked BEFORE $DEST is +# touched, so a refusal leaves the working install exactly as it was. +# Same version + same ref = nothing to change: say so and carry on. +new_ver="$(cat "$EXTRACTED/VERSION" 2>/dev/null || echo unknown)" +old_ver="$(cat "$DEST/VERSION" 2>/dev/null || true)" +old_from="$(cat "$DEST/INSTALLED_FROM" 2>/dev/null || true)" + +if [ -n "$old_ver" ] && [ "$old_ver" = "$new_ver" ] && [ "$old_from" = "$REPO@$REF" ]; then + CHANGING=0 +else + CHANGING=1 +fi + +# How we ask incus about boxes. No incus => no boxes, and nothing to protect. +if [ "$(id -u)" -eq 0 ]; then PRIV="" +elif command -v sudo >/dev/null 2>&1; then PRIV="sudo" +else PRIV="" +fi + +# Unprivileged FIRST: anyone who owns boxes is already in incus-admin, so the +# plain query answers it without making the installer demand a sudo password +# just to look. Escalate only if the socket refuses us. +incus_names() { # $1 = tag filter + incus list "$1" --format csv --columns n 2>/dev/null && return 0 + [ -n "$PRIV" ] && $PRIV incus list "$1" --format csv --columns n 2>/dev/null + return 0 +} + +boxes_on_host() { + command -v incus >/dev/null 2>&1 || return 0 + # BOTH tags: a pre-rename box carries user.claudebox=1 and is just as much + # someone's work as a current one. + { incus_names "user.box=1"; incus_names "user.claudebox=1"; } | sed '/^$/d' | sort -u +} + +if [ "$CHANGING" = 0 ]; then + log "already at $new_ver ($REPO@$REF) — reinstalling the same tree, nothing to migrate" +elif [ -z "${BOX_FORCE_UPGRADE:-}" ]; then + found="$(boxes_on_host)" + if [ -n "$found" ]; then + printf 'box-install: ERROR: this host has boxes, and this install would change what runs them.\n' >&2 + printf '\n installed: %s\n incoming: %s\n\n boxes on this host:\n' \ + "${old_from:-} ${old_ver:-}" "$REPO@$REF $new_ver" >&2 + printf '%s\n' "$found" | sed 's/^/ · /' >&2 + cat >&2 <' / 'box exec -- ...'). + · Upgrade anyway, on purpose: + BOX_FORCE_UPGRADE=1 curl -fsSL | bash + Boxes are not deleted, but the stack is rebuilt underneath them. + + A version-aware upgrade that migrates boxes instead of refusing is #67. +EOF + exit 1 + fi +fi + # --- atomically replace $DEST --------------------------------------------- log "installing into $DEST" rm -rf "$DEST"