fix(updater): find newer versions the tag listing cannot see
Two holes that together left a versioned app reporting "up to date" while a newer release was published. Newer-version discovery enumerated a repo's newest 100 tags. Projects that push a tag per commit drown their own releases in that window — matrixdotorg/synapse's newest 100 hold five version tags, about ten days of history. Once the release we need is older than the window it is simply absent, and the app reports current forever. The failure is silent and lands hardest on the apps furthest behind. Discovery now falls back to PROBING exact tags, most-significant component first, which has no window at all. Listing still runs first, so the common case stays at one call; probing is bounded at 40 lookups. Same reasoning the version ladder already uses, for the same reason. Registry lookups were also throttled purely per-run, so an app installed just after a window carried an empty available_digest until the next one — up to CFG_UPDATER_REGISTRY_INTERVAL (6h) later. Empty means update_available=false, which the UI renders as "up to date", so a new app claimed to be current on no evidence. Seen live: seven apps installed the evening after a 19:31 window all sat at update_available=false, one of them two releases behind. Apps with no prior registry answer are now looked up regardless of the throttle — once each, and interval 0 still means manual-only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
741edfdeb1
commit
f747083115
@ -187,9 +187,9 @@ updaterTagGreater() {
|
||||
return 1 # identical -> not newer
|
||||
}
|
||||
|
||||
# The newest tag for $2 (repo) that shares $1's (current tag) shape and is
|
||||
# numerically greater. Empty when there is nothing newer — the common case.
|
||||
updaterNewerVersionTag() {
|
||||
# The newest same-shape, numerically-greater tag found by ENUMERATING the newest
|
||||
# 100 tags. One cheap call, and right whenever the release we want is recent.
|
||||
updaterNewerVersionByList() {
|
||||
local cur="$1" repo="$2"
|
||||
local shape; shape="$(updaterTagShape "$cur")"
|
||||
local best="" t
|
||||
@ -202,6 +202,82 @@ updaterNewerVersionTag() {
|
||||
printf '%s' "$best"
|
||||
}
|
||||
|
||||
# Bump the Nth (0-based) numeric component of a tag by one and zero every
|
||||
# component after it, so a bump means what a version bump means:
|
||||
# "v1.158.2", 1 -> "v1.159.0" "31-fpm-alpine", 0 -> "32-fpm-alpine"
|
||||
# Shape is preserved by construction (only the digits change), which is what
|
||||
# lets the caller compare shapes to reject nonsense candidates.
|
||||
updaterTagBumpAt() {
|
||||
printf '%s' "$1" | awk -v idx="$2" '{
|
||||
out=""; n=0; s=$0
|
||||
while (match(s, /[0-9]+/)) {
|
||||
pre = substr(s, 1, RSTART-1)
|
||||
num = substr(s, RSTART, RLENGTH) + 0
|
||||
s = substr(s, RSTART+RLENGTH)
|
||||
if (n == idx) num = num + 1; else if (n > idx) num = 0
|
||||
out = out pre num
|
||||
n++
|
||||
}
|
||||
print out s
|
||||
}'
|
||||
}
|
||||
|
||||
# The newest tag reachable from $1 by PROBING for exact tags, most-significant
|
||||
# component first, climbing each as far as it goes.
|
||||
#
|
||||
# This exists because enumeration has a blind spot that gets worse the more
|
||||
# actively a project publishes. The listing returns the newest 100 tags, and
|
||||
# repos that push a tag per commit drown their own releases in them —
|
||||
# matrixdotorg/synapse's newest 100 hold just 5 version tags, roughly ten days
|
||||
# of history. Once the release we need is older than that window it is simply
|
||||
# absent, updaterNewerVersionByList returns nothing, and the app reports "up to
|
||||
# date" forever. The failure is silent and it lands exactly on the apps that are
|
||||
# furthest behind, which are the ones that most need to be told.
|
||||
#
|
||||
# Probing has no window: an exact tag lookup either exists or it does not. This
|
||||
# is the same reasoning (and the same endpoint) as the version ladder, which
|
||||
# refuses to build a rung list from a listing for precisely this reason — see
|
||||
# cli_updater_ladder.sh. Bounded at 40 lookups so a strange tag scheme cannot
|
||||
# turn one app's scan into a crawl.
|
||||
updaterNewerVersionByProbe() {
|
||||
local cur="$1" repo="$2"
|
||||
declare -F updaterTagExists >/dev/null 2>&1 || return 0 # ladder unavailable
|
||||
local shape; shape="$(updaterTagShape "$cur")"
|
||||
local ncomp; ncomp="$(printf '%s' "$cur" | grep -oE '[0-9]+' | wc -l | tr -d ' ')"
|
||||
[ "${ncomp:-0}" -gt 0 ] 2>/dev/null || return 0
|
||||
|
||||
local best="$cur" probes=0 i cand
|
||||
for ((i=0; i<ncomp; i++)); do
|
||||
# Climb this component until it runs out, THEN move to a finer one. Going
|
||||
# most-significant first means v2.0.0 is preferred over v1.159.0, and
|
||||
# never re-probing a component that already missed keeps the call count
|
||||
# proportional to how far behind we actually are.
|
||||
while (( probes < 40 )); do
|
||||
cand="$(updaterTagBumpAt "$best" "$i")"
|
||||
[ "$(updaterTagShape "$cand")" = "$shape" ] || break
|
||||
probes=$((probes + 1))
|
||||
updaterTagExists "$repo" "$cand" || break
|
||||
best="$cand"
|
||||
done
|
||||
done
|
||||
[ "$best" != "$cur" ] && printf '%s' "$best"
|
||||
return 0
|
||||
}
|
||||
|
||||
# The newest tag for $2 (repo) that shares $1's (current tag) shape and is
|
||||
# numerically greater. Empty when there is nothing newer — the common case.
|
||||
#
|
||||
# Listing first because it is one call and answers most repos outright; probing
|
||||
# only when it comes back empty, which is both the "genuinely current" case
|
||||
# (a few wasted lookups, no answer either way) and the "release fell out of the
|
||||
# listing window" case that enumeration alone gets silently wrong.
|
||||
updaterNewerVersionTag() {
|
||||
local cur="$1" repo="$2"
|
||||
local best; best="$(updaterNewerVersionByList "$cur" "$repo")"
|
||||
[ -n "$best" ] && { printf '%s' "$best"; return 0; }
|
||||
updaterNewerVersionByProbe "$cur" "$repo"
|
||||
}
|
||||
|
||||
# Human-readable version for display: OCI label → tag (versioned) → tag·shortdigest.
|
||||
updaterDisplayVersion() {
|
||||
local oci="$1" channel="$2" vtype="$3" digest="$4"
|
||||
@ -270,9 +346,29 @@ webuiUpdaterScan() {
|
||||
# local running identity of the anchor image
|
||||
local li dig oci; li="$(updaterInspectLocal "$anchor")"; dig="${li%%|*}"; oci="${li#*|}"
|
||||
|
||||
# Has this app EVER had a registry answer? A just-installed app has no
|
||||
# previous available_digest to reuse, so between windows it would carry
|
||||
# an empty one forward — and empty means update_available=false, which
|
||||
# the UI renders as "up to date". An app installed minutes after a
|
||||
# window would therefore claim to be current, on no evidence, until the
|
||||
# next one up to CFG_UPDATER_REGISTRY_INTERVAL (default 6h) later.
|
||||
# Observed live: seven apps installed the evening after a 19:31 window
|
||||
# all sat at update_available=false with an empty digest, one of them
|
||||
# two releases behind. Look those up regardless of the throttle — it is
|
||||
# the difference between "checked and current" and "never checked", and
|
||||
# it costs one lookup per app exactly once. Interval 0 still means
|
||||
# manual-only, so an explicit "never check on its own" is honoured.
|
||||
local app_registry="$do_registry"
|
||||
if [ "$app_registry" = "0" ] && [ "$reg_interval" != "0" ] && [ "$have_jq" = "1" ]; then
|
||||
local _seen=""
|
||||
[ -f "$prev_json" ] && _seen="$(jq -r --arg n "$app" '(.apps[]?|select(.name==$n)|.available_digest)//""' "$prev_json" 2>/dev/null)"
|
||||
[ "$_seen" = "null" ] && _seen=""
|
||||
[ -z "$_seen" ] && app_registry=1 # never answered (new app, or first scan)
|
||||
fi
|
||||
|
||||
# available (registry) digest for the anchor's channel
|
||||
local avail_dig=""
|
||||
if [ "$do_registry" = "1" ]; then
|
||||
if [ "$app_registry" = "1" ]; then
|
||||
avail_dig="$(updaterRegistryDigest "$(updaterRepoTag "$anchor")")"
|
||||
fi
|
||||
# reuse the prior value when we didn't (or couldn't) reach the registry
|
||||
@ -287,7 +383,7 @@ webuiUpdaterScan() {
|
||||
# rest of the registry work: look it up live inside the window, otherwise
|
||||
# carry the previous answer forward so the field never blinks off.
|
||||
local img_updated=""
|
||||
if [ "$do_registry" = "1" ]; then
|
||||
if [ "$app_registry" = "1" ]; then
|
||||
img_updated="$(updaterTagLastUpdated "$(updaterRepoTag "$anchor")" "$channel")"
|
||||
fi
|
||||
if [ -z "$img_updated" ] && [ "$have_jq" = "1" ] && [ -f "$prev_json" ]; then
|
||||
@ -301,7 +397,7 @@ webuiUpdaterScan() {
|
||||
# field does not flicker off on a throttled scan.
|
||||
local newer_ver=""
|
||||
if [ "$vtype" = "versioned" ]; then
|
||||
if [ "$do_registry" = "1" ]; then
|
||||
if [ "$app_registry" = "1" ]; then
|
||||
newer_ver="$(updaterNewerVersionTag "$channel" "$(updaterRepoTag "$anchor")")"
|
||||
newer_ver="${newer_ver%%:*}"
|
||||
elif [ "$have_jq" = "1" ] && [ -f "$prev_json" ]; then
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user