diff --git a/bin/rig b/bin/rig index 1f9b7ee..41a43af 100755 --- a/bin/rig +++ b/bin/rig @@ -152,10 +152,12 @@ commands: install/upgrade: curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash - Installs the latest RELEASE (RIG_REF= pins one, RIG_REF=main - tracks the development tree). Re-run any time: an installed version - converges (no-op), a new one installs side by side at - /versions/ and becomes the default. + Installs the latest RELEASE from GitHub (the default RIG_HOST). + RIG_REF= pins one, RIG_REF=main tracks the development tree. + From a Forgejo host: set RIG_HOST to that origin and fetch install.sh + from /heavy-duty/rig/raw/branch/main/install.sh (#111). + Re-run any time: an installed version converges (no-op), a new one + installs side by side at /versions/ and becomes the default. EOF } diff --git a/changelog.d/111.md b/changelog.d/111.md new file mode 100644 index 0000000..ffd0cb2 --- /dev/null +++ b/changelog.d/111.md @@ -0,0 +1,3 @@ +### Changed + +- `install.sh` and bootstrap's box fetch take `RIG_HOST` / `BOX_HOST` so a Forgejo origin can serve the install channel (#111) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index c8cae9e..ae5f934 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -723,7 +723,26 @@ if [ "$HOST" = "yes" ]; then BOX_RELEASE=0.9.0 BOX_REPO="${BOX_REPO:-heavy-duty/box}" BOX_REF="${BOX_REF:-$BOX_RELEASE}" - BOX_INSTALL_URL="https://raw.githubusercontent.com/${BOX_REPO}/${BOX_REF}/install.sh" + # BOX_HOST: which forge serves box (#111). Parallel to RIG_HOST / + # RIG_TEMPLATES_HOST — default GitHub keeps every existing bootstrap + # byte-unchanged; set BOX_HOST=https://forgejo.heavyduty.builders when + # box is installed from this instance. Raw-file grammar differs: + # GitHub raw.githubusercontent.com///install.sh + # Forgejo //raw/{tag|branch}//install.sh + # Version-shaped refs (BOX_RELEASE pins) use raw/tag/; anything else + # (BOX_REF=main) uses raw/branch/. + BOX_HOST="${BOX_HOST:-https://github.com}" + BOX_HOST="${BOX_HOST%/}" + case "$BOX_HOST" in + https://github.com|http://github.com|*//github.com) + BOX_INSTALL_URL="https://raw.githubusercontent.com/${BOX_REPO}/${BOX_REF}/install.sh" ;; + *) + case "$BOX_REF" in + [0-9]*|v[0-9]*) BOX_RAW_KIND=tag ;; + *) BOX_RAW_KIND=branch ;; + esac + BOX_INSTALL_URL="${BOX_HOST}/${BOX_REPO}/raw/${BOX_RAW_KIND}/${BOX_REF}/install.sh" ;; + esac BOX_MANUAL="curl -fsSL ${BOX_INSTALL_URL} | BOX_YES=1 BOX_REF=${BOX_REF} bash" if [ "${RIG_SKIP_BOX_INSTALL:-}" = "1" ]; then log "RIG_SKIP_BOX_INSTALL=1 — skipping box install; to prepare Incus by hand later: ${BOX_MANUAL}" diff --git a/commands/coolify-backup-install.sh b/commands/coolify-backup-install.sh index de17bca..b6a2669 100755 --- a/commands/coolify-backup-install.sh +++ b/commands/coolify-backup-install.sh @@ -206,7 +206,7 @@ log "writing ${UNIT_DIR}/coolify-dump.service" cat > "$UNIT_DIR/coolify-dump.service" < "$UNIT_DIR/coolify-dump.timer" < — so this returns 1 there instead of inventing a ref, -# and the CALLER owns the loud story. test/release.sh extracts this function -# (awk, the valid_version idiom) and drives it against a stubbed curl. +# and the CALLER owns the loud story. Host comes from RIG_HOST (default +# GitHub); both GitHub and Forgejo serve the same /releases/latest → +# /releases/tag/ redirect grammar, measured 2026-07-29 (#111). +# test/release.sh extracts this function (awk, the valid_version idiom) and +# drives it against a stubbed curl. resolve_latest_tag() { - local loc - loc="$(curl -fsSI -o /dev/null -w '%{redirect_url}' "https://github.com/$1/releases/latest")" || return 1 + # Default is inlined (not $RIG_HOST_DEFAULT) so test/release.sh's awk + # extract of this function stays self-contained — same discipline as + # valid_version. + local host="${RIG_HOST:-https://github.com}" loc + host="${host%/}" + loc="$(curl -fsSI -o /dev/null -w '%{redirect_url}' "$host/$1/releases/latest")" || return 1 case "$loc" in */releases/tag/?*) printf '%s\n' "${loc##*/releases/tag/}" ;; *) return 1 ;; @@ -113,12 +125,39 @@ resolve_latest_tag() { } # ref_candidate_urls — the download candidates for an -# explicit RIG_REF, in order: refs/tags first, so a tag always outranks a -# branch that happens to share its name (the pin must win), refs/heads as -# the fallback that keeps RIG_REF=main the dev channel. +# explicit RIG_REF, in order. Host comes from RIG_HOST; the two forges are +# not URL-compatible (#111 / #109): +# GitHub refs/tags first so a tag always outranks a same-named branch +# (the pin must win), then refs/heads for RIG_REF=main. +# Forgejo one form: /archive/.tar.gz resolves tags, branches and +# SHAs alike (same grammar as templates_archive_urls). ref_candidate_urls() { - printf 'https://github.com/%s/archive/refs/tags/%s.tar.gz\n' "$1" "$2" - printf 'https://github.com/%s/archive/refs/heads/%s.tar.gz\n' "$1" "$2" + local host="${RIG_HOST:-https://github.com}" + host="${host%/}" + case "$host" in + https://github.com|http://github.com|*//github.com) + printf '%s/%s/archive/refs/tags/%s.tar.gz\n' "$host" "$1" "$2" + printf '%s/%s/archive/refs/heads/%s.tar.gz\n' "$host" "$1" "$2" ;; + *) + printf '%s/%s/archive/%s.tar.gz\n' "$host" "$1" "$2" ;; + esac +} + +# install_script_url — the curl|bash entrypoint URL for this REPO on RIG_HOST. +# GitHub serves raw files at raw.githubusercontent.com; Forgejo at +# /raw/branch//. The refusal hint and bin/rig usage() both print +# this, so a Forgejo install never tells the operator to hit a 404 (#111). +# REPO is the installer's global (RIG_REPO); tests that extract this function +# must set it. +install_script_url() { + local host="${RIG_HOST:-https://github.com}" + host="${host%/}" + case "$host" in + https://github.com|http://github.com|*//github.com) + printf 'https://raw.githubusercontent.com/%s/main/install.sh\n' "${REPO:-heavy-duty/rig}" ;; + *) + printf '%s/%s/raw/branch/main/install.sh\n' "$host" "${REPO:-heavy-duty/rig}" ;; + esac } # The registry's candidate URLs, forge-aware — a byte-identical copy of @@ -223,12 +262,14 @@ else if [ -z "$REF" ]; then log "resolving the latest release of $REPO" if ! REF="$(resolve_latest_tag "$REPO")"; then - warn "could not resolve the latest release of $REPO — either no release exists yet, or GitHub was unreachable." - warn "(rig has no release until 0.1.0 is cut — rig#32. Until then, install the development tree explicitly.)" - die "set RIG_REF: e.g. curl -fsSL https://raw.githubusercontent.com/$REPO/main/install.sh | RIG_REF=main bash" + warn "could not resolve the latest release of $REPO — either no release exists yet, or ${RIG_HOST:-https://github.com} was unreachable." + warn "(install the development tree explicitly with RIG_REF=main when no release exists yet.)" + die "set RIG_REF: e.g. curl -fsSL $(install_script_url) | RIG_REF=main bash" fi log "latest release: $REF" - urls=("https://github.com/$REPO/archive/refs/tags/$REF.tar.gz") + # Same candidate grammar as an explicit pin: on GitHub the tag form wins + # first; on Forgejo the single bare archive URL is the whole list (#111). + mapfile -t urls < <(ref_candidate_urls "$REPO" "$REF") else mapfile -t urls < <(ref_candidate_urls "$REPO" "$REF") fi @@ -244,7 +285,7 @@ else fi done [ -n "$got" ] \ - || die "failed to download $REPO@$REF — not a tag and not a branch (tried refs/tags then refs/heads)" + || die "failed to download $REPO@$REF — no candidate URL worked (host ${RIG_HOST:-https://github.com}; tried tags then heads on GitHub, or the single archive URL on other forges)" log "extracting archive" tar -xzf "$TMPDIR/rig.tar.gz" -C "$TMPDIR" \ diff --git a/test/cli.sh b/test/cli.sh index 060c307..a01506a 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -249,6 +249,15 @@ check "bootstrap: manual box install carries the pinned ref" 0 "" \ grep -qF 'BOX_YES=1 BOX_REF=${BOX_REF} bash' "$ROOT/commands/bootstrap.sh" check "bootstrap: box repository remains pinnable" 0 "" \ grep -qF 'BOX_REPO:-heavy-duty/box' "$ROOT/commands/bootstrap.sh" +# BOX_HOST selects the forge that serves box's installer (#111). Default +# GitHub keeps the raw.githubusercontent.com grammar; a non-GitHub host +# uses Forgejo's /raw/{tag|branch}// form. +check "bootstrap: BOX_HOST defaults to GitHub" 0 "" \ + grep -qF 'BOX_HOST="${BOX_HOST:-https://github.com}"' "$ROOT/commands/bootstrap.sh" +check "bootstrap: GitHub box install uses raw.githubusercontent.com" 0 "" \ + grep -qF 'raw.githubusercontent.com/${BOX_REPO}/${BOX_REF}/install.sh' "$ROOT/commands/bootstrap.sh" +check "bootstrap: non-GitHub box install uses /raw/{tag|branch}/" 0 "" \ + grep -qF '/raw/${BOX_RAW_KIND}/${BOX_REF}/install.sh' "$ROOT/commands/bootstrap.sh" # Opt-out for rehearsals / offline / hand-managed hosts. check "bootstrap: box install honors RIG_SKIP_BOX_INSTALL opt-out" 0 "" \ grep -q "RIG_SKIP_BOX_INSTALL" "$ROOT/commands/bootstrap.sh" diff --git a/test/release.sh b/test/release.sh index 8e0103a..3608c5b 100644 --- a/test/release.sh +++ b/test/release.sh @@ -44,6 +44,7 @@ FAKEHOME="$WORK/home"; mkdir -p "$FAKEHOME" RL="$WORK/installer-fns.sh" awk '/^resolve_latest_tag\(\) \{/,/^\}/' "$ROOT/install.sh" > "$RL" awk '/^ref_candidate_urls\(\) \{/,/^\}/' "$ROOT/install.sh" >> "$RL" +awk '/^install_script_url\(\) \{/,/^\}/' "$ROOT/install.sh" >> "$RL" check "installer fns extracted (guards the awk)" 0 "redirect_url" cat "$RL" STUB="$WORK/stub"; mkdir -p "$STUB" @@ -93,14 +94,49 @@ check "resolve: a tagless releases/tag/ redirect fails" 1 "" \ check "resolve: a failing curl fails (network down is not a channel)" 1 "" \ rlt CURL_STUB_FAIL=1 -rcu_line() { # rcu_line — the nth candidate URL for an explicit ref - bash -c 'set -euo pipefail - . "$1"; ref_candidate_urls acme/widgets 1.2.3 | sed -n "${2}p"' _ "$RL" "$1" +rcu_line() { # rcu_line [VAR=val ...] — the nth candidate URL for an explicit ref + local n="$1"; shift + # shellcheck disable=SC2016 + env "$@" bash -c 'set -euo pipefail + . "$1"; ref_candidate_urls acme/widgets 1.2.3 | sed -n "${2}p"' _ "$RL" "$n" } check "candidates: refs/tags first — the pin outranks a same-named branch" 0 \ "https://github.com/acme/widgets/archive/refs/tags/1.2.3.tar.gz" rcu_line 1 check "candidates: refs/heads is the fallback" 0 \ "https://github.com/acme/widgets/archive/refs/heads/1.2.3.tar.gz" rcu_line 2 +# RIG_HOST selects the forge grammar (#111): non-GitHub hosts emit one bare +# /archive/.tar.gz form (Forgejo), never the refs/{tags,heads}/ pair. +check "candidates: Forgejo host emits the bare archive URL" 0 \ + "https://forgejo.example/acme/widgets/archive/1.2.3.tar.gz" \ + rcu_line 1 RIG_HOST=https://forgejo.example +check "candidates: Forgejo host emits exactly one candidate" 0 "1" \ + env RIG_HOST=https://forgejo.example bash -c 'set -euo pipefail + . "$1"; ref_candidate_urls acme/widgets 1.2.3 | grep -c .' _ "$RL" +check "candidates: trailing slash on RIG_HOST is stripped" 0 \ + "https://forgejo.example/acme/widgets/archive/1.2.3.tar.gz" \ + rcu_line 1 RIG_HOST=https://forgejo.example/ + +# install_script_url — the curl|bash hint must match the forge (#111). +isu() { + # shellcheck disable=SC2016 + env "$@" bash -c 'set -euo pipefail + REPO=heavy-duty/rig; . "$1"; install_script_url' _ "$RL" +} +check "install_script_url: GitHub default uses raw.githubusercontent.com" 0 \ + "https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh" isu +check "install_script_url: Forgejo uses /raw/branch/main/" 0 \ + "https://forgejo.example/heavy-duty/rig/raw/branch/main/install.sh" \ + isu RIG_HOST=https://forgejo.example + +# resolve_latest_tag follows RIG_HOST too — the probe URL must name the forge. +rlt_log="$WORK/rlt-log" +: > "$rlt_log" +check "resolve: RIG_HOST is the releases/latest origin" 0 "0.2.0" \ + rlt CURL_STUB_REDIRECT=https://forgejo.example/heavy-duty/rig/releases/tag/0.2.0 \ + RIG_HOST=https://forgejo.example CURL_STUB_LOG="$rlt_log" +check "resolve: the probe hit the Forgejo host" 0 \ + "https://forgejo.example/heavy-duty/rig/releases/latest" \ + cat "$rlt_log" # --- the three channels, driven through the REAL installer ------------------- # Full install.sh runs against throwaway roots with the stub curl on PATH: the @@ -167,8 +203,35 @@ check "channel dev: ...then the branch URL" 0 "refs/heads/feature-x" \ # Neither a tag nor a branch: both candidates miss, and the die says so. H5="$WORK/h5"; B5="$WORK/b5" -check "channel: a ref that is neither tag nor branch dies naming both tries" \ - 1 "not a tag and not a branch" rinst "$H5" "$B5" RIG_REF=no-such-ref +check "channel: a ref that is neither tag nor branch dies naming the tries" \ + 1 "no candidate URL worked" rinst "$H5" "$B5" RIG_REF=no-such-ref + +# Channel 4 — RIG_HOST=Forgejo: one archive URL, same redirect grammar for +# latest (#111). The stub only succeeds when the bare /archive/ form is +# requested — a regression that still emitted refs/tags would fail here. +H9="$WORK/h9"; B9="$WORK/b9"; LOG9="$WORK/log9" +check "channel forgejo latest: resolves and installs via bare archive URL" 0 "done" \ + rinst "$H9" "$B9" RIG_HOST=https://forgejo.example \ + CURL_STUB_REDIRECT=https://forgejo.example/heavy-duty/rig/releases/tag/7.7.7-relflow \ + CURL_STUB_OK='/archive/7.7.7-relflow.tar.gz' CURL_STUB_LOG="$LOG9" +check "channel forgejo latest: download URL is the bare archive form" 0 \ + "https://forgejo.example/heavy-duty/rig/archive/7.7.7-relflow.tar.gz" \ + cat "$LOG9" +check "channel forgejo latest: the tree landed" 0 "" \ + test -x "$H9/versions/7.7.7-relflow/bin/rig" +H10="$WORK/h10"; B10="$WORK/b10" +check "channel forgejo pinned: RIG_REF uses the bare archive URL" 0 "done" \ + rinst "$H10" "$B10" RIG_HOST=https://forgejo.example RIG_REF=main \ + CURL_STUB_OK='/archive/main.tar.gz' +check "channel forgejo pinned: the tree landed" 0 "" \ + test -x "$H10/versions/7.7.7-relflow/bin/rig" +# Refusal hint on a non-GitHub host must not send the operator to +# raw.githubusercontent.com (that 404s from a Forgejo-only tree). +H11="$WORK/h11"; B11="$WORK/b11" +check "channel forgejo latest: no-release hint uses the Forgejo raw URL" \ + 1 "https://forgejo.example/heavy-duty/rig/raw/branch/main/install.sh" \ + rinst "$H11" "$B11" RIG_HOST=https://forgejo.example \ + CURL_STUB_REDIRECT=https://forgejo.example/heavy-duty/rig/releases # --- the local channel: RIG_INSTALL_SOURCE (#106) ---------------------------- # A supported input, not test scaffolding — CI's `install:` job and test/cli.sh