diff --git a/scripts/cli/commands/updater/cli_updater_ladder.sh b/scripts/cli/commands/updater/cli_updater_ladder.sh index 601d8b7..fb24445 100644 --- a/scripts/cli/commands/updater/cli_updater_ladder.sh +++ b/scripts/cli/commands/updater/cli_updater_ladder.sh @@ -108,6 +108,57 @@ updaterOciTagList() { | tr ',' '\n' | grep -oE '"[^"]*"' | tr -d '"' | grep -v '^$' } +# When was the image behind : 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 diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 8c763be..c59df68 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -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 "$@"; } diff --git a/scripts/webui/data/generators/updater/webui_updater_scan.sh b/scripts/webui/data/generators/updater/webui_updater_scan.sh index ad353d9..3b760e1 100644 --- a/scripts/webui/data/generators/updater/webui_updater_scan.sh +++ b/scripts/webui/data/generators/updater/webui_updater_scan.sh @@ -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 \