Repository navigation
upgrade --check: report when a newer RigForge release exists (on-demand, no phone-home) #148
Description
Why
An operator running a fleet from release tags has no way to learn a new RigForge shipped short of watching the GitHub repo. The obvious fix — a periodic version ping — is off the table: SECURITY.md:10-11 and README.md:132 promise "no telemetry, ever… no version ping", and that promise is a feature. The compatible shape is on-demand only: upgrade --check asks GitHub's releases API exactly when the operator types it, prints the answer, and exits 0 no matter what the network did (an update hint must never break a script that calls it).
Scope
upgrade --check: compare the localVERSIONfile against the latest GitHub release tag via one unauthenticatedcurl -fsS --max-time 5 https://api.github.com/repos/p2pool-starter-stack/rigforge/releases/latest | jq -r .... Noghdependency, no token — 60 unauthenticated requests/hour is plenty for a human-invoked check.- Output, three cases: up to date → one ok line; newer available → the version, the release URL, and the upgrade recipe (
git pullin$SCRIPT_DIR, orgit fetch --tags && git checkout <tag>for tag-pinned fleets, thensudo $0 upgrade— matching usage()'s "redeploy after a 'git pull'" wording); local ahead of the latest release (a develop checkout) → "ahead of the latest release" ok line, not a downgrade prompt. - Offline / rate-limited / API-shape-changed →
warnone line andexit 0. Never nonzero, never a hang past the 5s timeout. - Version compare, decided:
sort -V. Verified: GNU coreutils has it, and macOS's shippedsort(2.3-Apple) supports-Vand orders1.9.0 < 1.10.0correctly (checked on the dev Mac — the "BSD sort lacks -V" folklore does not apply to Apple's sort, and Linux+macOS are the only supported platforms per the script'sOS_TYPEhandling). No pure-bash comparator; equality is checked with=before sort ever runs, so sort only breaks ties in one direction. doctorgets NO network call — decided: doctor is the deterministic offline health check (it runs in CI/tests and on rigs mid-incident; a 5s DNS stall inside a health check is worse than a stale hint). Instead, append one static offline line to doctor's existing all-clear closing summary ("doctor: all critical checks passed.", rigforge.sh:3028):Check for a newer RigForge any time: '$0 upgrade --check'.Text only, on the success path only (a failing doctor should not advertise upgrades).- Docs: SECURITY.md:10-11 and README.md:132 must gain one clarifying sentence — the "no version ping" promise stands because the check is explicit and on-demand, never scheduled or piggybacked on another verb. This is a hard requirement, not polish: shipping a GitHub call without touching that paragraph makes the security doc wrong.
- No new config keys —
config.minimal.jsonstays minimal.
Out of scope: auto-update, a check timer/cron, checking from setup/apply/doctor (all would violate the on-demand posture), checking XMRig upstream versions (RigForge pins XMRig deliberately — #18).
Acceptance criteria
-
./rigforge.sh upgrade --checkwith local == latest prints one up-to-date line (including the version) and exits 0. - With latest > local: prints the new version, the release html_url, and the exact upgrade commands; exits 0.
- With local > latest (develop checkout): says so, exits 0, does not suggest downgrading.
- Offline / curl failure / rate-limit 403 /
tag_namemissing: onewarnline ("couldn't reach GitHub — try again later"), exit 0. - Plain
upgradebehavior is byte-for-byte unchanged (it makes no new network calls); unknown flags to upgrade error out with the standard "Run '$0 help'" message. -
doctormakes zero network calls (assert via CURL_LOG in tests that doctor triggers no curl) and its all-clear output mentionsupgrade --check. - Tag formats
v1.1.0and1.1.0both compare correctly against the bare-semver VERSION file (strip a leadingvbefore comparing). - SECURITY.md + README.md "no version ping" paragraphs updated with the on-demand clarification;
usage()upgrade line mentions--check.make lint+make testpass (macOS bash 3.2 and Linux).
Implementation notes
Numbered, mechanical. All design decisions above (sort -V, exit-0-always, doctor stays offline) are made — do not reopen them.
- Dispatch (rigforge.sh:3086):
upgrade) upgrade ;;→upgrade) shift; upgrade "$@" ;;(upgrade currently takes no args — verified; the shift is safe because the verb matched, so$#≥ 1). - upgrade() (rigforge.sh:1038): prepend an arg loop (copy backup()'s shape, rigforge.sh:2368-2374):
--check) _upgrade_check; return 0 ;;*) error "Unexpected argument for upgrade: $arg. Run '$0 help'." ;;. The existing pipeline (1039-1052) runs only with no args, unchanged. _upgrade_check()(place directly aboveupgrade()):local local_v remote_v url body;local_v=$(tr -d '[:space:]' <"$SCRIPT_DIR/VERSION" 2>/dev/null)— same read ascmd_version(rigforge.sh:2661); empty → warn "no VERSION file" + return 0.body=$(curl -fsS --max-time 5 "https://api.github.com/repos/p2pool-starter-stack/rigforge/releases/latest" 2>/dev/null) || { warn "…couldn't reach GitHub…"; return 0; }—curl -fturns the rate-limit 403 into exit 22, so that path funnels into the same warn.remote_v=$(printf '%s' "$body" | jq -r '.tag_name // empty' 2>/dev/null);url=$(printf '%s' "$body" | jq -r '.html_url // empty' 2>/dev/null);remote_v="${remote_v#v}"; empty remote_v → the same warn + return 0 (covers API shape changes and rate-limit JSON bodies that slip past-f).- Compare:
[ "$local_v" = "$remote_v" ]→log "RigForge $local_v is the latest release."; elsehighest=$(printf '%s\n%s\n' "$local_v" "$remote_v" | sort -V | tail -n1);highest = remote_v→ newer:logthe version + URL +"Update: cd $SCRIPT_DIR && git pull && sudo $0 upgrade (tag-pinned fleets: git fetch --tags && git checkout v$remote_v)"; else →log "RigForge $local_v is ahead of the latest release ($remote_v) — development build.". Always return 0. - No parse_config, no root, no other side effects — the function reads one file and one URL.
- doctor (rigforge.sh:3026-3031): inside the
issues -eq 0branch only, after the existinglog "doctor: all critical checks passed.", addlog "Check for a newer RigForge any time: '$0 upgrade --check'.". Static string — no curl, no version read needed. - usage() (rigforge.sh:3036): extend the upgrade line:
upgrade redeploy after a 'git pull': rebuild + restart if the pinned XMRig changed ('--check' just reports whether a newer RigForge release exists). - Tests (tests/run.sh): the harness already has the pieces — the curl stub records every call to CURL_LOG (tests/run.sh:175-179) and the version assertion pattern lives at tests/run.sh:1037. Add a unit block that sources
$SCRIPTin a subshell (the_RIGFORGE_SOURCEDpattern used throughout) and drives_upgrade_checkwith a test-local curl stub, since the shared stub emits an XMRig summary body:- stub printing
{"tag_name":"v9.9.9","html_url":"https://github.com/p2pool-starter-stack/rigforge/releases/tag/v9.9.9"}→ output contains9.9.9, the URL, andupgrade; rc 0. - stub printing
{"tag_name":"v$(tr -d '[:space:]' <VERSION)"}→ "latest" line; rc 0. - stub printing
{"tag_name":"v0.0.1"}→ "ahead" line, no downgrade suggestion; rc 0. - stub
exit 22(curl -f on 403/offline) and stub printing{}→ one warn line each, rc 0 both. - Ordering sanity: assert
printf '1.9.0\n1.10.0\n' | sort -V | tail -n1is1.10.0— a canary that fails loudly if some exotic host's sort lacks-V, instead of silently mis-comparing. - Black-box:
bash "$SCRIPT" upgrade --bogus→ rc 1 + "Unexpected argument";bash "$SCRIPT" doctorwith CURL_LOG exported → CURL_LOG has no releases-API line (doctor stayed offline).
- stub printing
- Docs: SECURITY.md:10-11 — after "no version ping", add: "(
upgrade --checkqueries the GitHub releases API, but only when you run it — nothing is scheduled and nothing else calls it)"; README.md:132 — same clause;docs/operations.mdupgrade section — two lines showing--checkbeforeupgrade.
Pitfalls: bash 3.2 — no associative arrays, no ${var,,}; set -Eeuo pipefail — the curl/jq/tr failures are all handled with || { warn; return 0; } or 2>/dev/null so the check can NEVER kill the shell or exit nonzero; strip the leading v from tags before comparing (VERSION is bare semver — tests/run.sh:1037 asserts that format); no gh, no token, nothing written to disk; do not log the response body (keep output to the three decided lines); jq --arg if any JSON is ever written (none is here); make lint = pinned shellcheck 0.11 + shfmt -i 4; no new config keys.
Related
- SECURITY.md:10-11 / README.md:132 — the no-telemetry promise this feature is designed around (on-demand only), and the paragraphs this change must update.
- Pin & verify the XMRig build (v6.26.0) #18 — pinned-commit verification for the XMRig clone; same supply-chain posture (we tell you an update exists; we never fetch-and-run it).
- setup: dependency install fails on a fresh boot when apt/dpkg is locked by unattended-upgrades #74 — headless/scripted setup; exit-0-always is what makes
upgrade --checksafe to sprinkle into an operator's own cron/scripts at THEIR choice. cmd_version(rigforge.sh:2659-2663) — the local-version read this reuses.
Activity