#!/usr/bin/env bash # # update-umbrel-mempool.sh # # Interactively update the Mempool Umbrel app's mempool/frontend and # mempool/backend Docker images. # # What it does: # 1. Picks (or prompts for) the SSH target for your Umbrel. # 2. Reads the current versions from the app's docker-compose.yml (over SSH). # 3. Shows the last 10 tags for each image from Docker Hub and lets you choose. # 4. Shows a before/after diff (warns if frontend/backend versions differ). # 5. After you confirm, edits the compose file in place (with a .bak backup) and # runs `umbreld client apps.update.mutate --appId mempool` to apply. # # Usage: # ./scripts/update-umbrel-mempool.sh [ssh-target] # # With no argument it prompts for the target (default umbrel@umbrel.local). # e.g. ./scripts/update-umbrel-mempool.sh pi@192.168.1.50 # # Re-exec under bash if started another way (e.g. `sh script`), and leave POSIX # mode so arrays and other bash features work regardless of how it was launched. if [ -z "${BASH_VERSION:-}" ]; then exec bash "$0" "$@" fi set +o posix 2>/dev/null || true set -euo pipefail DEFAULT_TARGET="umbrel@umbrel.local" FRONTEND_REPO="mempool/frontend" BACKEND_REPO="mempool/backend" TAG_LIST_COUNT=10 # Candidate locations for the app's docker-compose.yml on the device. The first # one that exists is used. The bare relative path matches the umbrel user's home. COMPOSE_REL="umbrel/app-stores/getumbrel-umbrel-apps-github-53f74447/mempool/docker-compose.yml" COMPOSE_CANDIDATES=( "\$HOME/$COMPOSE_REL" "/home/umbrel/$COMPOSE_REL" "$COMPOSE_REL" ) err() { printf 'error: %s\n' "$*" >&2; } die() { err "$*"; exit 1; } info() { printf '%s\n' "$*"; } # --- Step 1: preflight ------------------------------------------------------ command -v jq >/dev/null 2>&1 || die "jq is required (install: brew install jq)" command -v ssh >/dev/null 2>&1 || die "ssh is required" command -v curl >/dev/null 2>&1 || die "curl is required" [ -e /dev/tty ] || die "this script is interactive and needs a terminal (/dev/tty)" # --- Step 2: choose SSH target ---------------------------------------------- if [ "$#" -ge 1 ] && [ -n "$1" ]; then SSH_TARGET="$1" info "Using SSH target: ${SSH_TARGET}" else printf 'SSH target [%s]: ' "$DEFAULT_TARGET" >/dev/tty read -r SSH_TARGET # then: the matching `image: mempool/...` lines REMOTE_READ=$(cat <&2; exit 3; fi echo "COMPOSE_PATH=\$compose" grep -E 'image:[[:space:]]*mempool/(frontend|backend):' "\$compose" REMOTE ) info "Connecting to ${SSH_TARGET} to read current versions..." REMOTE_OUT="$(ssh "$SSH_TARGET" "$REMOTE_READ")" || { err "Could not read the compose file on ${SSH_TARGET}." err "Checked: ${COMPOSE_CANDIDATES[*]}" die "Verify the SSH target and that the Mempool app is installed." } COMPOSE_PATH="$(printf '%s\n' "$REMOTE_OUT" | sed -n 's/^COMPOSE_PATH=//p')" [ -n "$COMPOSE_PATH" ] || die "could not resolve compose path on device" # Extract current "repo:tag@sha256:digest" tokens and the bare current tag. cur_ref() { # $1 = repo printf '%s\n' "$REMOTE_OUT" | grep -oE "$1:[^[:space:]\"']+" | head -1 } cur_tag() { # $1 = full ref (repo:tag@sha256:...) printf '%s' "$1" | sed -E 's#^[^:]+:([^@]+)@.*#\1#' } FE_CUR="$(cur_ref "$FRONTEND_REPO" || true)" BE_CUR="$(cur_ref "$BACKEND_REPO" || true)" FE_CUR_TAG="$(cur_tag "$FE_CUR")" BE_CUR_TAG="$(cur_tag "$BE_CUR")" info "" info "Currently installed on ${SSH_TARGET}:" info " frontend: ${FE_CUR_TAG:-}" info " backend: ${BE_CUR_TAG:-}" # --- Step 4: interactive tag selection from Docker Hub ---------------------- # Choose a tag filter once; it applies to both images for consistency. echo "" >/dev/tty echo "Tag filter:" >/dev/tty echo " 1) Stable releases only (vX.Y.Z)" >/dev/tty echo " 2) All tags (including -dev / -rc / -beta)" >/dev/tty while :; do printf "Enter number [1-2] (default 1): " >/dev/tty read -r FILTER_SEL /dev/tty ;; esac done # jq selector for the chosen filter. Stable = plain semver vX.Y.Z (no suffix). # Both modes require a non-null top-level digest (needed for the @sha256: pin; # some old tags lack one). case "$FILTER_MODE" in stable) JQ_SELECT='select(.digest != null and (.name | test("^v[0-9]+\\.[0-9]+\\.[0-9]+$")))' ;; all) JQ_SELECT='select(.digest != null and .name != "latest")' ;; esac # Prints the chosen " " to stdout; menu/prompts go to /dev/tty # so they don't pollute the captured value. $3 = current tag (annotated as such). # Paginates through Docker Hub (newest first) until TAG_LIST_COUNT matches are # collected or pages run out — needed for stable tags buried behind many dev builds. choose_image() { local repo="$1" label="$2" current="${3:-}" local url="https://hub.docker.com/v2/repositories/${repo}/tags?page_size=100&ordering=last_updated" local tags=() digests=() t d resp parsed page=0 while [ -n "$url" ] && [ "$page" -lt 10 ] && [ "${#tags[@]}" -lt "$TAG_LIST_COUNT" ]; do page=$((page + 1)) resp="$(curl -fsSL "$url")" || { err "Docker Hub query failed for ${repo}"; return 1; } parsed="$(printf '%s' "$resp" \ | jq -r ".results | map(${JQ_SELECT}) | .[] | \"\(.name)\t\(.digest)\"")" # heredoc (not process substitution) so the loop runs in this shell and the # array appends persist, and so it works even under bash POSIX mode. while IFS=$'\t' read -r t d; do [ -n "$t" ] || continue tags+=("$t"); digests+=("$d") [ "${#tags[@]}" -ge "$TAG_LIST_COUNT" ] && break done </dev/tty local sel idx while :; do printf "Enter number [1-%d] (default 1): " "${#tags[@]}" >/dev/tty read -r sel /dev/tty; continue ;; esac if [ "$sel" -ge 1 ] && [ "$sel" -le "${#tags[@]}" ]; then idx=$((sel - 1)); break; fi echo " Out of range." >/dev/tty done printf '%s %s\n' "${tags[$idx]}" "${digests[$idx]}" } read -r FE_TAG FE_DIGEST <<<"$(choose_image "$FRONTEND_REPO" frontend "$FE_CUR_TAG")" [ -n "${FE_TAG:-}" ] && [ -n "${FE_DIGEST:-}" ] && [ "$FE_DIGEST" != "null" ] \ || die "frontend selection failed" read -r BE_TAG BE_DIGEST <<<"$(choose_image "$BACKEND_REPO" backend "$BE_CUR_TAG")" [ -n "${BE_TAG:-}" ] && [ -n "${BE_DIGEST:-}" ] && [ "$BE_DIGEST" != "null" ] \ || die "backend selection failed" FE_NEW="${FRONTEND_REPO}:${FE_TAG}@${FE_DIGEST}" BE_NEW="${BACKEND_REPO}:${BE_TAG}@${BE_DIGEST}" # --- Step 5: show diff + confirm -------------------------------------------- info "" info "Compose file: ${COMPOSE_PATH} (on ${SSH_TARGET})" info "" info "frontend:" info " current: ${FE_CUR:-}" info " new: ${FE_NEW}" info "backend:" info " current: ${BE_CUR:-}" info " new: ${BE_NEW}" info "" if [ "$FE_TAG" != "$BE_TAG" ]; then info "WARNING: selected frontend (${FE_TAG}) and backend (${BE_TAG}) versions differ." info "" fi if [ "$FE_CUR" = "$FE_NEW" ] && [ "$BE_CUR" = "$BE_NEW" ]; then info "Both images already match the selected versions. Nothing to do." exit 0 fi printf 'Apply update and restart mempool? [y/N] ' >/dev/tty read -r REPLY ${FE_TAG}" info " backend -> ${BE_TAG}" info "Backup of previous compose: ${COMPOSE_PATH}.bak"