From 6438fbc406797c3f218ebc6730e57b3e0b22a56c Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 31 May 2026 14:59:33 +0900 Subject: [PATCH] Add mempool umbrel update script --- scripts/update-umbrel-mempool.sh | 260 +++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100755 scripts/update-umbrel-mempool.sh diff --git a/scripts/update-umbrel-mempool.sh b/scripts/update-umbrel-mempool.sh new file mode 100755 index 000000000..893111757 --- /dev/null +++ b/scripts/update-umbrel-mempool.sh @@ -0,0 +1,260 @@ +#!/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"