diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index c7cce46..14cf9d4 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -747,25 +747,43 @@ if [ "$HOST" = "yes" ]; then printf '%s/%s/raw/branch/%s/install.sh\n' "$BOX_HOST" "$BOX_REPO" "$BOX_REF" ;; esac } - # Operator-facing recovery command. On multi-candidate hosts (Forgejo) - # list every URL — taking only the first would hand BOX_REF=main a - # /raw/tag/main URL that 404s (#111 review: "tells the operator to run a - # command that 404s"). After a live probe succeeds, the install loop - # rewrites BOX_MANUAL to the URL that actually worked. + # One pasteable recovery command per candidate URL. Never join candidates + # with English prose or shell metacharacters — "curl A | bash; if that + # 404s: curl B | bash" is not valid shell (`bash -n` exits 2) and is the + # same class of operator-facing failure #111 exists to remove (#125 / + # codex REQUEST_CHANGES on !114). Display uses separate prefixed lines + # (try: / or:); after a live probe succeeds the install loop rewrites + # BOX_MANUAL to the single URL that worked. + box_manual_cmd() { # box_manual_cmd — one pasteable install line + printf 'curl -fsSL %s | BOX_YES=1 BOX_REF=%s bash\n' "$1" "$BOX_REF" + } + box_manual_text() { + local _n=0 _url _cmd + while IFS= read -r _url; do + _cmd="$(box_manual_cmd "$_url")" + _cmd="${_cmd%$'\n'}" + _n=$((_n + 1)) + if [ "$_n" -eq 1 ]; then + printf 'try: %s\n' "$_cmd" + else + printf 'or: %s\n' "$_cmd" + fi + done < <(box_install_urls) + } BOX_INSTALL_URL="$(box_install_urls | head -n1)" - BOX_MANUAL="" - while IFS= read -r _box_manual_url; do - _box_manual_cmd="curl -fsSL ${_box_manual_url} | BOX_YES=1 BOX_REF=${BOX_REF} bash" - if [ -z "$BOX_MANUAL" ]; then - BOX_MANUAL="$_box_manual_cmd" - else - BOX_MANUAL="${BOX_MANUAL}; if that 404s: ${_box_manual_cmd}" - fi - done < <(box_install_urls) + # Newline-separated try:/or: lines — each command after the prefix is + # independently pasteable (never one shell-looking string with prose). + BOX_MANUAL="$(box_manual_text)" if [ "${RIG_SKIP_BOX_INSTALL:-}" = "1" ]; then - log "RIG_SKIP_BOX_INSTALL=1 — skipping box install; to prepare Incus by hand later: ${BOX_MANUAL}" + log "RIG_SKIP_BOX_INSTALL=1 — skipping box install; to prepare Incus by hand later:" + while IFS= read -r _line; do [ -n "$_line" ] && log " ${_line}"; done </dev/null 2>&1; then - warn "curl not found — skipping box install; once curl is present, prepare Incus with: ${BOX_MANUAL}" + warn "curl not found — skipping box install; once curl is present, prepare Incus with:" + while IFS= read -r _line; do [ -n "$_line" ] && warn " ${_line}"; done < "$BIM" +check "bootstrap: box_manual helpers extracted" 0 "try:" cat "$BIM" +# shellcheck disable=SC2016 +bmanual() { # bmanual HOST REF — render BOX_MANUAL text + env BOX_HOST="$1" BOX_REPO=heavy-duty/box BOX_REF="$2" \ + bash -c 'set -euo pipefail; . "$1"; box_manual_text' _ "$BIM" +} +# shellcheck disable=SC2016 +bmanual_cmds_ok() { # every pasteable command after try:/or: passes bash -n + env BOX_HOST="$1" BOX_REPO=heavy-duty/box BOX_REF="$2" \ + bash -c 'set -euo pipefail + . "$1" + while IFS= read -r line; do + [ -n "$line" ] || continue + cmd="$line" + cmd="${cmd#try: }" + cmd="${cmd#or: }" + cmd="${cmd#or: }" + bash -n <<<"$cmd" + done < <(box_manual_text)' _ "$BIM" +} +check "bootstrap: BOX_MANUAL GitHub is a single try: raw.githubusercontent.com line" 0 \ + "try: curl -fsSL https://raw.githubusercontent.com/heavy-duty/box/0.9.0/install.sh | BOX_YES=1 BOX_REF=0.9.0 bash" \ + bmanual https://github.com 0.9.0 +check "bootstrap: BOX_MANUAL Forgejo lists raw/tag first" 0 \ + "try: curl -fsSL https://forgejo.example/heavy-duty/box/raw/tag/main/install.sh | BOX_YES=1 BOX_REF=main bash" \ + bmanual https://forgejo.example main +check "bootstrap: BOX_MANUAL Forgejo lists raw/branch as or:" 0 \ + "or: curl -fsSL https://forgejo.example/heavy-duty/box/raw/branch/main/install.sh | BOX_YES=1 BOX_REF=main bash" \ + bmanual https://forgejo.example main +check "bootstrap: BOX_MANUAL GitHub commands pass bash -n" 0 "" \ + bmanual_cmds_ok https://github.com 0.9.0 +check "bootstrap: BOX_MANUAL Forgejo commands pass bash -n" 0 "" \ + bmanual_cmds_ok https://forgejo.example main +# Regression: the old prose join must not return. +check "bootstrap: BOX_MANUAL does not use prose 'if that 404s'" 1 "" \ + grep -qF 'if that 404s' "$ROOT/commands/bootstrap.sh" rm -rf "$BIU_DIR" # Opt-out for rehearsals / offline / hand-managed hosts. check "bootstrap: box install honors RIG_SKIP_BOX_INSTALL opt-out" 0 "" \