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 '^$'
|
| 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.
|
# 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
|
# 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
|
# 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"
|
[updaterNewerVersionByProbe]="webui/data/generators/updater/webui_updater_scan.sh"
|
||||||
[updaterNewerVersionTag]="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"
|
[updaterNextRung]="cli/commands/updater/cli_updater_ladder.sh"
|
||||||
|
[updaterOciCreated]="cli/commands/updater/cli_updater_ladder.sh"
|
||||||
[_updaterOciSplit]="cli/commands/updater/cli_updater_ladder.sh"
|
[_updaterOciSplit]="cli/commands/updater/cli_updater_ladder.sh"
|
||||||
[updaterOciTagExists]="cli/commands/updater/cli_updater_ladder.sh"
|
[updaterOciTagExists]="cli/commands/updater/cli_updater_ladder.sh"
|
||||||
[updaterOciTagList]="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"
|
[updaterNewerVersionByProbe]="scripts"
|
||||||
[updaterNewerVersionTag]="scripts"
|
[updaterNewerVersionTag]="scripts"
|
||||||
[updaterNextRung]="scripts"
|
[updaterNextRung]="scripts"
|
||||||
|
[updaterOciCreated]="scripts"
|
||||||
[_updaterOciSplit]="scripts"
|
[_updaterOciSplit]="scripts"
|
||||||
[updaterOciTagExists]="scripts"
|
[updaterOciTagExists]="scripts"
|
||||||
[updaterOciTagList]="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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
_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 "$@"; }
|
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 "$@"; }
|
updaterOciTagList() { unset -f updaterOciTagList; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterOciTagList "$@"; }
|
||||||
|
|||||||
@ -190,7 +190,21 @@ updaterRegistryTags() {
|
|||||||
updaterTagLastUpdated() {
|
updaterTagLastUpdated() {
|
||||||
local repo="${1%%:*}" tag="${2:-latest}"
|
local repo="${1%%:*}" tag="${2:-latest}"
|
||||||
repo="${repo#docker.io/}"; repo="${repo#index.docker.io/}"
|
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
|
case "$repo" in */*) : ;; *) repo="library/$repo" ;; esac
|
||||||
command -v curl >/dev/null 2>&1 || return 0
|
command -v curl >/dev/null 2>&1 || return 0
|
||||||
curl -fsS --connect-timeout 5 --max-time 12 \
|
curl -fsS --connect-timeout 5 --max-time 12 \
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user