diff --git a/README.md b/README.md index 8960b0c..a68587f 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,15 @@ By default that installs the **latest release** — the installer resolves the release tag off GitHub's `releases/latest` redirect (no API, no token) and downloads exactly that tree, so two operators running it get the same box. If the resolution fails it says so and stops — it never silently hands out -`main`. `BOX_REF` picks another channel (a set ref is tried as a tag first, -then as a branch — [#83](https://github.com/heavy-duty/box/issues/83)): +`main`. `BOX_REF` picks another channel: a named ref is tried as a tag first, +then as a branch ([#83](https://github.com/heavy-duty/box/issues/83)), while a +full 40-hex commit SHA selects GitHub's immutable commit archive: ```sh curl -fsSL .../install.sh | bash # the latest release (default) curl -fsSL .../install.sh | BOX_REF=0.6.0 bash # pin a release curl -fsSL .../install.sh | BOX_REF=main bash # the development tip +curl -fsSL .../install.sh | BOX_REF= bash # exact tree ``` (A dev tree's `VERSION` carries a `-dev` suffix, so it lands beside your diff --git a/bin/box b/bin/box index d50d8de..cc603cb 100755 --- a/bin/box +++ b/bin/box @@ -1096,36 +1096,48 @@ load_template() { T_DISK="${disk:-${BOX_DISK:-${T_DISK:-60GiB}}}" } -# The ONE substitution a template gets — user-data.yaml is otherwise passed to -# Incus verbatim. The tenant seeds preinstall rig, which inverts the rig→box -# install edge (rig#28: rig installs box on hosts; now box guests install rig), -# and that edge needs a pin point (#81): the seed carries @RIG_REPO@ / -# @RIG_REF@ tokens, resolved here from the mint environment — RIG_REPO -# (default heavy-duty/rig) and RIG_REF (default main). Both directions track -# main unpinned today, said honestly (the same treatment rig#29 gave box's own -# unpinned install) until a release flow exists (rig#32 / #83). The values are +# The ONE substitution a template gets — @RIG_INSTALL@ — while user-data.yaml +# is otherwise passed to Incus verbatim. The tenant seeds preinstall rig, which +# inverts the rig→box install edge (rig#28: rig installs box on hosts; now box +# guests install rig), and that edge needs a pin point (#81). The renderer +# resolves the mint environment's RIG_REPO (default heavy-duty/rig) and RIG_REF +# (default main) into the whole installer command. Named refs keep the normal +# tag/branch channel; a drill-pinned commit carries its immutable archive as +# RIG_INSTALL_SOURCE (#152). Both values are # allowlist-validated BEFORE touching the YAML: they land inside a runcmd # shell line, so a quote, a space or a newline smuggled through the # environment must die on the host, never execute in the guest. bash's =~ # anchors to the whole string — a multi-line value cannot sneak one clean # line past it the way a line-oriented grep would. # The rig pin, resolved from the mint environment, in ONE place: render_userdata -# substitutes it into the seed, and the mint stamp (#103) records it onto the +# builds it into the seed, and the mint stamp (#103) records it onto the # instance. Two spellings of the same default would eventually disagree, and a # stamp that disagrees with the seed is worse than no stamp at all. rig_repo() { printf '%s\n' "${RIG_REPO:-heavy-duty/rig}"; } rig_ref() { printf '%s\n' "${RIG_REF:-main}"; } render_userdata() { - local f="$1" repo data ref + local f="$1" repo data ref install repo="$(rig_repo)"; ref="$(rig_ref)" [[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] \ || die "RIG_REPO must look like owner/repo: $repo" [[ "$ref" =~ ^[A-Za-z0-9._/-]+$ ]] \ || die "RIG_REF must be a plain ref name (letters, digits, . _ / -): $ref" + if [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then + # rig's installer currently resolves named tags/branches. A release drill + # pins the moving ref before mint, so hand that installer the immutable + # commit archive as a local source instead of asking it to resolve again + # inside the guest (#152). + printf -v install '%s' \ + "rig_source=/tmp/rig-$ref.tar.gz; rig_installer=/tmp/rig-install-$ref.sh; curl -fsSL https://github.com/$repo/archive/$ref.tar.gz -o \"\$rig_source\" && curl -fsSL https://raw.githubusercontent.com/$repo/$ref/install.sh -o \"\$rig_installer\" && HOME=/root RIG_REPO=\"$repo\" RIG_REF=\"$ref\" RIG_INSTALL_SOURCE=\"\$rig_source\" bash \"\$rig_installer\"; rig_rc=\$?; rm -f \"\$rig_source\" \"\$rig_installer\"; exit \"\$rig_rc\"" + else + printf -v install 'curl -fsSL https://raw.githubusercontent.com/%s/%s/install.sh | HOME=/root RIG_REPO="%s" RIG_REF="%s" bash' \ + "$repo" "$ref" "$repo" "$ref" + fi data="$(cat "$f")" data="${data//@RIG_REPO@/$repo}" data="${data//@RIG_REF@/$ref}" + data="${data//@RIG_INSTALL@/$install}" printf '%s\n' "$data" } diff --git a/install.sh b/install.sh index 3522d45..d673ebb 100755 --- a/install.sh +++ b/install.sh @@ -24,10 +24,11 @@ set -euo pipefail # downloading — for CI and the drill, so what lands is the code under review. REPO="${BOX_REPO:-heavy-duty/box}" -# Three install channels, one knob (#83): BOX_REF unset installs the LATEST +# Four install channels, one knob (#83, #152): BOX_REF unset installs the LATEST # RELEASE (the tag resolved from GitHub's releases/latest redirect, below); # BOX_REF= pins a release; BOX_REF= (say, main) is the dev -# channel. A set ref is tried as a tag first, then as a branch. +# channel; and a full 40-hex commit SHA is fetched immutably. A named ref is +# tried as a tag first, then as a branch. REF="${BOX_REF:-}" # Root installs GLOBALLY, non-root installs per-user. box's install tree is # EXECUTED by other users (the multi-user host path: rig installs box once, every @@ -232,17 +233,27 @@ else fi INSTALLED_FROM="$REPO@$REF" log "installing box from $REPO@$REF" - # A ref is a TAG first (the pinned-release channel), a branch second (the - # dev channel, BOX_REF=main) — and the fallback only exists for a ref the - # OPERATOR named: a resolved latest tag has no branch to fall through to. - URL="https://github.com/$REPO/archive/refs/tags/$REF.tar.gz" - log "downloading $URL" - if ! curl -fsSL "$URL" -o "$TMPDIR/box.tar.gz"; then - [ -n "${BOX_REF:-}" ] || die "failed to download $URL" - URL="https://github.com/$REPO/archive/refs/heads/$REF.tar.gz" - log "no tag '$REF' — trying it as a branch: $URL" + if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then + # The drill resolves moving candidate refs before it mutates a host, then + # installs the exact object it measured (#152). GitHub's commit archive is + # outside refs/{tags,heads}; guessing either namespace makes every SHA 404. + URL="https://github.com/$REPO/archive/$REF.tar.gz" + log "downloading immutable commit $URL" curl -fsSL "$URL" -o "$TMPDIR/box.tar.gz" \ - || die "failed to download it as either — '$REF' is neither a tag nor a branch of $REPO" + || die "failed to download commit $REF from $REPO" + else + # A named ref is a TAG first (the pinned-release channel), a branch second + # (the dev channel, BOX_REF=main). The fallback only exists for a ref the + # OPERATOR named: a resolved latest tag has no branch to fall through to. + URL="https://github.com/$REPO/archive/refs/tags/$REF.tar.gz" + log "downloading $URL" + if ! curl -fsSL "$URL" -o "$TMPDIR/box.tar.gz"; then + [ -n "${BOX_REF:-}" ] || die "failed to download $URL" + URL="https://github.com/$REPO/archive/refs/heads/$REF.tar.gz" + log "no tag '$REF' — trying it as a branch: $URL" + curl -fsSL "$URL" -o "$TMPDIR/box.tar.gz" \ + || die "failed to download it as either — '$REF' is neither a tag nor a branch of $REPO" + fi fi log "extracting archive" diff --git a/templates/claude-box/user-data.yaml b/templates/claude-box/user-data.yaml index 9a7baf8..9a8bcee 100644 --- a/templates/claude-box/user-data.yaml +++ b/templates/claude-box/user-data.yaml @@ -30,4 +30,4 @@ runcmd: # HOME=/root: cloud-init runs runcmd as root but with NO $HOME in the # environment, and the rig installer (set -u) reads $HOME for its DEST — # measured live: the mint died with "HOME: unbound variable" without it. - - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | HOME=/root RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash + - '@RIG_INSTALL@' diff --git a/templates/codex-box/user-data.yaml b/templates/codex-box/user-data.yaml index 32fa730..1599fb0 100644 --- a/templates/codex-box/user-data.yaml +++ b/templates/codex-box/user-data.yaml @@ -30,4 +30,4 @@ runcmd: # HOME=/root: cloud-init runs runcmd as root but with NO $HOME in the # environment, and the rig installer (set -u) reads $HOME for its DEST — # measured live: the mint died with "HOME: unbound variable" without it. - - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | HOME=/root RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash + - '@RIG_INSTALL@' diff --git a/templates/grok-box/user-data.yaml b/templates/grok-box/user-data.yaml index 28c2ab3..fd2d840 100644 --- a/templates/grok-box/user-data.yaml +++ b/templates/grok-box/user-data.yaml @@ -30,4 +30,4 @@ runcmd: # HOME=/root: cloud-init runs runcmd as root but with NO $HOME in the # environment, and the rig installer (set -u) reads $HOME for its DEST — # measured live: the mint died with "HOME: unbound variable" without it. - - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | HOME=/root RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash + - '@RIG_INSTALL@' diff --git a/templates/kimi-box/user-data.yaml b/templates/kimi-box/user-data.yaml index e86ba31..a4f1519 100644 --- a/templates/kimi-box/user-data.yaml +++ b/templates/kimi-box/user-data.yaml @@ -30,4 +30,4 @@ runcmd: # HOME=/root: cloud-init runs runcmd as root but with NO $HOME in the # environment, and the rig installer (set -u) reads $HOME for its DEST — # measured live: the mint died with "HOME: unbound variable" without it. - - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | HOME=/root RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash + - '@RIG_INSTALL@' diff --git a/templates/staging-box/user-data.yaml b/templates/staging-box/user-data.yaml index 3a230d3..76cd330 100644 --- a/templates/staging-box/user-data.yaml +++ b/templates/staging-box/user-data.yaml @@ -31,4 +31,4 @@ runcmd: # HOME=/root: cloud-init runs runcmd as root but with NO $HOME in the # environment, and the rig installer (set -u) reads $HOME for its DEST — # measured live: the mint died with "HOME: unbound variable" without it. - - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | HOME=/root RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash + - '@RIG_INSTALL@' diff --git a/test/cli.sh b/test/cli.sh index 6dc71e0..6804fca 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -266,15 +266,28 @@ check "render_userdata: the pin's defaults came with it (guards the grep)" 0 "he check "render_userdata: the extracted function is valid bash" 0 "" bash -n "$RUFN" SEED="$(mktemp)" -printf '#cloud-config\nruncmd:\n - curl -fsSL https://raw.githubusercontent.com/@RIG_REPO@/@RIG_REF@/install.sh | RIG_REPO="@RIG_REPO@" RIG_REF="@RIG_REF@" bash\n' > "$SEED" +printf "#cloud-config\nruncmd:\n - '@RIG_INSTALL@'\n" > "$SEED" # shellcheck disable=SC2016 # $0/$1 expand in the child shell, by design rud() { # rud [VAR=val ...] — render the fixture seed through the real function env "$@" bash -c 'die() { echo "box: $*" >&2; exit 1; }; . "$0"; render_userdata "$1"' "$RUFN" "$SEED" } check "render_userdata: defaults pin heavy-duty/rig" 0 "githubusercontent.com/heavy-duty/rig/main/install.sh" rud check "render_userdata: defaults feed the installer's own env too" 0 'RIG_REPO="heavy-duty/rig" RIG_REF="main"' rud +check "render_userdata: the rig install pins HOME=/root (runcmd has no \$HOME)" 0 "HOME=/root " rud check "render_userdata: RIG_REPO/RIG_REF override at mint" 0 "dan-claude-bot/rig/feat/bootstrap-roles/install.sh" \ rud RIG_REPO=dan-claude-bot/rig RIG_REF=feat/bootstrap-roles +RIG_COMMIT=89abcdef0123456789abcdef0123456789abcdef +check "render_userdata: a rig commit downloads its immutable archive" 0 "archive/$RIG_COMMIT.tar.gz" \ + rud RIG_REPO=heavy-duty/rig RIG_REF="$RIG_COMMIT" +# $rig_source belongs to the rendered guest shell. +# shellcheck disable=SC2016 +check "render_userdata: a rig commit feeds the archive to the installer" 0 'RIG_INSTALL_SOURCE="$rig_source"' \ + rud RIG_REPO=heavy-duty/rig RIG_REF="$RIG_COMMIT" +# Expansion belongs to the bash -c fixture. +# shellcheck disable=SC2016 +check "render_userdata: a named rig ref keeps the normal channel" 1 "" \ + bash -c 'die() { exit 1; }; RIG_REPO=heavy-duty/rig RIG_REF=main; . "$1"; render_userdata "$2" | grep -q RIG_INSTALL_SOURCE' \ + _ "$RUFN" "$SEED" # shellcheck disable=SC2016 # $0/$1 expand in the child shells, by design check "render_userdata: no token survives the render" 1 "" \ bash -c 'env bash -c "die() { echo box: \$*; exit 1; }; . \"\$0\"; render_userdata \"\$1\"" "$1" "$2" | grep -q @RIG_' _ "$RUFN" "$SEED" @@ -326,26 +339,13 @@ for d in "$ROOT"/templates/*/; do # The thin-template contract (#81), both halves per template: # # THE SEED — a template that names a tenant role (BOX_BOOTSTRAP_ROLE) must - # preinstall rig carrying BOTH pin tokens, on the installer URL and on the - # installer's own env, or the pin is a half-truth: a mint would fetch one - # ref's installer and install another ref's tree. + # delegate its rig install to render_userdata. That one renderer owns both + # the named-ref channel and the immutable-commit archive channel. # ------------------------------------------------------------------------ trole="$(tpl "$ROOT" "$t" | sed -n 's/.*ROLE=\([^ ]*\).*/\1/p')" if [ -n "$trole" ]; then - check "template '$t': the seed installs rig (role '$trole')" 0 "" \ - grep -q 'install.sh' "$d/user-data.yaml" - # shellcheck disable=SC2016 # $1 expands in the child shell, by design - check "template '$t': the rig install carries the @RIG_REPO@ pin token" 0 "" \ - bash -c 'grep "install.sh" "$1" | grep -q "@RIG_REPO@/@RIG_REF@"' _ "$d/user-data.yaml" - # shellcheck disable=SC2016 - check "template '$t': the pin reaches the installer's env too" 0 "" \ - bash -c 'grep "install.sh" "$1" | grep -q "RIG_REPO=\"@RIG_REPO@\" RIG_REF=\"@RIG_REF@\""' _ "$d/user-data.yaml" - # HOME=/root: a scar found live — cloud-init's runcmd has no $HOME and - # rig's installer (set -u) dies on it (rig#39). The pin must survive - # every seed rewrite. - # shellcheck disable=SC2016 - check "template '$t': the rig install pins HOME=/root (runcmd has no \$HOME)" 0 "" \ - bash -c 'grep "install.sh" "$1" | grep -q "HOME=/root "' _ "$d/user-data.yaml" + check "template '$t': the seed installs rig through the renderer (role '$trole')" 0 "" \ + grep -q "^[[:space:]]*- '@RIG_INSTALL@'\$" "$d/user-data.yaml" fi # ------------------------------------------------------------------------ # THE ABSENCE — no tenant content in ANY template, ever again. Everything a diff --git a/test/release.sh b/test/release.sh index d47372d..62461ad 100644 --- a/test/release.sh +++ b/test/release.sh @@ -109,6 +109,16 @@ check "dev channel: tries the tag first" 0 "refs/tags/main.tar.gz" head -1 "$L3" check "dev channel: then downloads the branch" 0 "" \ grep -qF "archive/refs/heads/main.tar.gz" "$L3" +COMMIT_REF=0123456789abcdef0123456789abcdef01234567 +H5="$WORK/h5"; B5="$WORK/b5"; L5="$WORK/c5.log" +check "commit channel: installs an immutable full SHA" 0 "installing box from heavy-duty/box@$COMMIT_REF" \ + ninst "$H5" "$B5" BOX_REF="$COMMIT_REF" FAKE_CURL_LOG="$L5" \ + FAKE_SERVE_URL="https://github.com/heavy-duty/box/archive/$COMMIT_REF.tar.gz" +check "commit channel: downloads the commit archive directly" 0 "archive/$COMMIT_REF.tar.gz" cat "$L5" +check "commit channel: never guesses tag or branch namespaces" 1 "" grep -Eq 'refs/(tags|heads)' "$L5" +check "commit channel: records the immutable source" 0 "heavy-duty/box@$COMMIT_REF" \ + cat "$H5/versions/9.9.9/INSTALLED_FROM" + H4="$WORK/h4"; B4="$WORK/b4"; L4="$WORK/c4.log" check "resolution failure: names the latest-release probe" 1 "could not resolve the latest release" \ ninst "$H4" "$B4" FAKE_CURL_RC=6 FAKE_CURL_LOG="$L4" @@ -122,6 +132,7 @@ check "unknown ref names both attempted channels" 1 "neither a tag nor a branch" check "README documents the latest-release channel" 0 "" grep -qF 'latest release' "$ROOT/README.md" check "README documents the pinned channel" 0 "" grep -qF 'BOX_REF=0.6.0' "$ROOT/README.md" check "README documents the dev channel" 0 "" grep -qF 'BOX_REF=main' "$ROOT/README.md" +check "README documents the immutable commit channel" 0 "" grep -qF 'BOX_REF=' "$ROOT/README.md" echo "---" echo "$PASS passed, $FAIL failed"