feat(updater): build dates for off-Hub images, from the config blob
The unmaintained warning runs on one field — when upstream last rebuilt the image — and off-Hub apps had no value for it. Hub answers in a single call; the OCI API does not expose it at all, so an app on ghcr.io, quay.io or lscr.io simply could not be assessed for staleness, which is the one signal a user cannot work out for themselves. It is in the image, just further down: manifest -> (if a multi-arch index) a platform manifest -> config blob, whose "created" is the build time. Three requests instead of Hub's one, once per registry window, and only for the apps Hub cannot answer for — which is why Hub keeps its cheap path rather than being routed through this. Index and single-arch manifests are distinguished explicitly rather than by position: in an index the first digest is a CHILD manifest, in an image manifest it is the config itself, so reading "the first digest" would silently fetch the wrong blob for one of the two shapes. Live: stoat 2026-08-08, bookstack 2026-08-17, speedtest 2026-08-16, invidious 2026-08-05 — all previously null. Hub unchanged, navidrome still answered by the single-call path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0a6ea95b08
commit
9c8f0782e1
@ -108,6 +108,57 @@ updaterOciTagList() {
|
||||
| tr ',' '\n' | grep -oE '"[^"]*"' | tr -d '"' | grep -v '^$'
|
||||
}
|
||||
|
||||
# When was the image behind <image-ref>:<tag> built? ISO8601, empty if unknown.
|
||||
#
|
||||
# Hub answers this in one call (updaterTagLastUpdated); the OCI API does not
|
||||
# expose it at all, so it has to be walked out of the image itself:
|
||||
# manifest -> (if a multi-arch index) a platform manifest -> config blob
|
||||
# and the config blob's "created" is the build time. Three requests, once per
|
||||
# registry window, and only for the apps Hub cannot answer for.
|
||||
#
|
||||
# Worth the walk because this single field is what the unmaintained warning
|
||||
# runs on. Without it an off-Hub app cannot be assessed at all, and "upstream
|
||||
# stopped rebuilding this a year ago" is exactly the thing a user cannot work
|
||||
# out for themselves.
|
||||
updaterOciCreated() {
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
local ref="${1%%@*}" tag="$2"
|
||||
[ -n "$tag" ] || tag="${ref##*:}"
|
||||
_updaterOciSplit "$ref"
|
||||
local tok; tok="$(_updaterOciToken "$_oci_reg" "$_oci_repo")"
|
||||
local -a acc=(
|
||||
-H 'Accept: application/vnd.oci.image.index.v1+json'
|
||||
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json'
|
||||
-H 'Accept: application/vnd.oci.image.manifest.v1+json'
|
||||
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json'
|
||||
)
|
||||
local base="https://${_oci_reg}/v2/${_oci_repo}"
|
||||
local man
|
||||
man="$(curl -fsS --connect-timeout 5 --max-time 15 ${tok:+-H "Authorization: Bearer $tok"} \
|
||||
"${acc[@]}" "${base}/manifests/${tag}" 2>/dev/null | tr -d ' \n')"
|
||||
[ -n "$man" ] || return 0
|
||||
|
||||
# A multi-arch index lists child manifests; descend into the first. A single
|
||||
# manifest has no "manifests" key and already carries the config reference —
|
||||
# the distinction matters because in an index the first digest is a CHILD,
|
||||
# while in an image manifest it is the config itself.
|
||||
if printf '%s' "$man" | grep -q '"manifests":\['; then
|
||||
local child
|
||||
child="$(printf '%s' "$man" | grep -oE '"digest":"sha256:[0-9a-f]{64}"' | head -1 | grep -oE 'sha256:[0-9a-f]{64}')"
|
||||
[ -n "$child" ] || return 0
|
||||
man="$(curl -fsS --connect-timeout 5 --max-time 15 ${tok:+-H "Authorization: Bearer $tok"} \
|
||||
"${acc[@]}" "${base}/manifests/${child}" 2>/dev/null | tr -d ' \n')"
|
||||
[ -n "$man" ] || return 0
|
||||
fi
|
||||
|
||||
local cfg
|
||||
cfg="$(printf '%s' "$man" | sed -E 's/.*"config":\{//; s/\}.*//' | grep -oE 'sha256:[0-9a-f]{64}' | head -1)"
|
||||
[ -n "$cfg" ] || return 0
|
||||
curl -fsSL --connect-timeout 5 --max-time 20 ${tok:+-H "Authorization: Bearer $tok"} \
|
||||
"${base}/blobs/${cfg}" 2>/dev/null \
|
||||
| grep -oE '"created":"[^"]*"' | head -1 | cut -d'"' -f4
|
||||
}
|
||||
|
||||
# Does this exact tag exist? One cheap lookup, and the ONLY reliable way to ask.
|
||||
# Listing cannot answer it: Docker Hub pages at 100 and orders by recency, so an
|
||||
# older intermediate rung falls off the end — mastodon's v4.3 exists but is
|
||||
|
||||
@ -1062,6 +1062,7 @@ declare -gA LP_FN_MAP=(
|
||||
[updaterNewerVersionByProbe]="webui/data/generators/updater/webui_updater_scan.sh"
|
||||
[updaterNewerVersionTag]="webui/data/generators/updater/webui_updater_scan.sh"
|
||||
[updaterNextRung]="cli/commands/updater/cli_updater_ladder.sh"
|
||||
[updaterOciCreated]="cli/commands/updater/cli_updater_ladder.sh"
|
||||
[_updaterOciSplit]="cli/commands/updater/cli_updater_ladder.sh"
|
||||
[updaterOciTagExists]="cli/commands/updater/cli_updater_ladder.sh"
|
||||
[updaterOciTagList]="cli/commands/updater/cli_updater_ladder.sh"
|
||||
@ -2239,6 +2240,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[updaterNewerVersionByProbe]="scripts"
|
||||
[updaterNewerVersionTag]="scripts"
|
||||
[updaterNextRung]="scripts"
|
||||
[updaterOciCreated]="scripts"
|
||||
[_updaterOciSplit]="scripts"
|
||||
[updaterOciTagExists]="scripts"
|
||||
[updaterOciTagList]="scripts"
|
||||
@ -3452,6 +3454,7 @@ updaterNewerVersionByList() { unset -f updaterNewerVersionByList; __lpAutoload "
|
||||
updaterNewerVersionByProbe() { unset -f updaterNewerVersionByProbe; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterNewerVersionByProbe "$@"; }
|
||||
updaterNewerVersionTag() { unset -f updaterNewerVersionTag; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterNewerVersionTag "$@"; }
|
||||
updaterNextRung() { unset -f updaterNextRung; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterNextRung "$@"; }
|
||||
updaterOciCreated() { unset -f updaterOciCreated; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterOciCreated "$@"; }
|
||||
_updaterOciSplit() { unset -f _updaterOciSplit; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; _updaterOciSplit "$@"; }
|
||||
updaterOciTagExists() { unset -f updaterOciTagExists; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterOciTagExists "$@"; }
|
||||
updaterOciTagList() { unset -f updaterOciTagList; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterOciTagList "$@"; }
|
||||
|
||||
@ -190,7 +190,21 @@ updaterRegistryTags() {
|
||||
updaterTagLastUpdated() {
|
||||
local repo="${1%%:*}" tag="${2:-latest}"
|
||||
repo="${repo#docker.io/}"; repo="${repo#index.docker.io/}"
|
||||
case "$repo" in *.*/*|localhost/*) return 0 ;; esac # non-Hub: unknown, stay quiet
|
||||
# Non-Hub: no equivalent endpoint exists, so dig the build time out of the
|
||||
# image's own config blob instead of returning "unknown". Costs three
|
||||
# requests against one for Hub, which is why Hub keeps its cheap path — but
|
||||
# without it an off-Hub app can never be assessed for staleness at all, and
|
||||
# the unmaintained warning is the one signal a user cannot derive alone.
|
||||
case "$repo" in
|
||||
*.*/*|localhost/*)
|
||||
if ! declare -F updaterOciCreated >/dev/null 2>&1; then
|
||||
[ -f "$install_scripts_dir/cli/commands/updater/cli_updater_ladder.sh" ] \
|
||||
&& source "$install_scripts_dir/cli/commands/updater/cli_updater_ladder.sh" 2>/dev/null
|
||||
fi
|
||||
declare -F updaterOciCreated >/dev/null 2>&1 && updaterOciCreated "$1" "$tag"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
case "$repo" in */*) : ;; *) repo="library/$repo" ;; esac
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
curl -fsS --connect-timeout 5 --max-time 12 \
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user