diff --git a/configs/webui/webui_updater b/configs/webui/webui_updater index 3a7391c..d9f7697 100644 --- a/configs/webui/webui_updater +++ b/configs/webui/webui_updater @@ -6,6 +6,7 @@ CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - CFG_UPDATER_REGISTRY_INTERVAL=360 # Registry Check Interval - Minutes between registry lookups for new image builds. 0 = only on Check now. CFG_HOTFIX_AUTO=security-breakage # Hotfix Auto-Apply - Which signed hotfix severities apply automatically on the update check [security-breakage|all|off] CFG_UPDATER_AUTO=true # Automatic App Updates - Master switch for per-app automatic updates; off makes every app manual [true:On|false:Off] +CFG_UPDATER_LADDER_AUTO=true # Automatic Version Steps - Whether automatic updates may also move an app to the NEXT published version, not just rebuild the one it is on. One step per app per day, each snapshotted and verified before the next is considered, so an app is never more than one version from a state that worked. Off keeps version moves to the Upgrade button. [true:On|false:Off] CFG_UPDATER_STALE_DAYS=365 # Unmaintained Warning After - Days without an upstream rebuild before an app is flagged as possibly unmaintained. 0 disables. CFG_UPDATER_UPGRADE_PRUNE=true # Clean Up After Version Upgrades - After a stepped upgrade, delete the intermediate images it stepped through [true:On|false:Off] CFG_UPDATER_WINDOW=06:00-08:00 # Automatic Update Window - When automatic updates may install, in host local time (HH:MM-HH:MM, or 'always') diff --git a/scripts/cli/commands/updater/cli_updater_auto.sh b/scripts/cli/commands/updater/cli_updater_auto.sh index f93b2a9..ef57814 100644 --- a/scripts/cli/commands/updater/cli_updater_auto.sh +++ b/scripts/cli/commands/updater/cli_updater_auto.sh @@ -162,3 +162,87 @@ updaterApplyAuto() fi return 0 } + +# --------------------------------------------------------------------------- +# Automatic version LADDERING — at most one rung, at most once a day. +# +# updaterApplyAuto above moves an app WITHIN its version line (a rebuild of the +# tag it already tracks). This moves it BETWEEN lines, and it is the deliberately +# cautious half of that: it never plans a climb, only ever the single next +# published release, and it does that at most once per calendar day per app. +# +# Why one rung and not a ladder: a ladder run unattended can be several +# migrations deep before anyone looks, and "restore the snapshot from sixty +# seconds ago" stops being a comfort once four of them have stacked. One rung a +# day means the app is never more than a single version from a state that +# verified, and there is a day in which to notice. It crosses a major boundary +# if that is genuinely the next release, because refusing would strand an app +# on the last version of a line forever — but it gets there one step at a time, +# never as a leap. +# +# Every rung still goes through updaterUpgradeApp, so the contract is unchanged +# from the button: snapshot (fail-closed) -> set version -> pull -> up -> verify, +# and on any failure that rung is restored and the climb stops. +# +# Two stamps, both one-shot in different ways: +# .rung the target last attempted — a rung that failed is not retried +# until a NEWER one is published (mirrors the .digest stamp). +# .rungday the day a rung was last attempted — the once-a-day bound. +_updaterAutoRungStamp() { echo "$(_updaterAutoDir)/$1.rung"; } +_updaterAutoRungDay() { echo "$(_updaterAutoDir)/$1.rungday"; } + +updaterUpgradeAuto() +{ + [[ "${CFG_UPDATER_AUTO:-true}" == "true" ]] || return 0 + # Its own switch as well as the master one: "keep my apps patched" and "move + # my apps between versions on their own" are different appetites for risk, + # and someone should be able to want the first without the second. + [[ "${CFG_UPDATER_LADDER_AUTO:-true}" == "true" ]] || return 0 + command -v jq >/dev/null 2>&1 || return 0 + + local upd; upd="$(_updaterAutoGenDir)/updates.json" + [[ -f "$upd" ]] || return 0 + updaterInWindow || return 0 + + local _f + for _f in cli_updater_ladder cli_updater_verify cli_updater_upgrade; do + declare -F updaterNextRung >/dev/null 2>&1 && break + source "$install_scripts_dir/cli/commands/updater/${_f}.sh" 2>/dev/null + done + declare -F updaterNextRung >/dev/null 2>&1 || return 0 + + local auto_dir; auto_dir="$(_updaterAutoDir)" + [[ -d "$auto_dir" ]] || runFileOp mkdir -p "$auto_dir" 2>/dev/null + + local today; today="$(date +%F)" + local app channel image repo next stamp dayf enq=0 + while IFS=$'\t' read -r app channel image; do + [[ -n "$app" && -n "$channel" && -n "$image" ]] || continue + [[ "$(updaterAppPolicy "$app")" == "auto" ]] || continue + # An in-flight updater task for this app: leave it alone rather than + # stacking a version move on top of an update that is still running. + updaterAutoTaskPending "$app" && continue + + dayf="$(_updaterAutoRungDay "$app")" + [[ -f "$dayf" && "$(cat "$dayf" 2>/dev/null)" == "$today" ]] && continue + + repo="$(updaterRepoTag "$image")"; repo="${repo%:*}" + next="$(updaterNextRung "$channel" "$repo")" + [[ -n "$next" ]] || continue + + stamp="$(_updaterAutoRungStamp "$app")" + [[ -f "$stamp" && "$(cat "$stamp" 2>/dev/null)" == "$next" ]] && continue + + # Stamp BEFORE enqueueing, same reasoning as the digest stamp: a crash + # between the two costs one skipped upgrade, never a loop. + printf '%s' "$next" | runFileWrite "$stamp" 2>/dev/null || true + printf '%s' "$today" | runFileWrite "$dayf" 2>/dev/null || true + cliTaskRun "libreportal updater upgrade $app $next --auto" "updater_upgrade" "$app" "--detach" + enq=$((enq + 1)) + isNotice "$app: queued the next version step $channel → $next (snapshotted and verified; one step only)." + done < <(jq -r '.apps[]? | select((.type // "") == "versioned") + | "\(.name)\t\(.channel // "")\t\(.current_image // "")"' "$upd" 2>/dev/null) + + (( enq > 0 )) && isSuccessful "Queued $enq automatic version step(s) — one rung each, and no more today." + return 0 +} diff --git a/scripts/cli/commands/updater/cli_updater_commands.sh b/scripts/cli/commands/updater/cli_updater_commands.sh index ee5a54e..1ae9102 100644 --- a/scripts/cli/commands/updater/cli_updater_commands.sh +++ b/scripts/cli/commands/updater/cli_updater_commands.sh @@ -77,6 +77,10 @@ cliHandleUpdaterCommands() source "$install_scripts_dir/cli/commands/updater/cli_updater_auto.sh" 2>/dev/null fi declare -F updaterApplyAuto >/dev/null 2>&1 && updaterApplyAuto + # App versions: step apps set to auto onto the next published + # release, one rung and once a day. Runs after the in-line updates + # so an app that just took a rebuild is already skipped as pending. + declare -F updaterUpgradeAuto >/dev/null 2>&1 && updaterUpgradeAuto ;; "apply"|"now") @@ -107,8 +111,15 @@ cliHandleUpdaterCommands() # and refused with "no safe path to --detach" — it failed safe, but # blaming the version for a misplaced flag is a poor way to say # "that flag isn't supported here". + # --auto marks a climb the updater scheduled for itself, so History + # can say so. It is the ONLY difference from the button: same + # engine, same snapshot-and-verify per rung, same rollback. + case "$upgrade_mode" in + --auto) export UPDATER_UPGRADE_TRIGGER=auto; upgrade_mode="" ;; + esac case "$upgrade_target" in --dry-run) upgrade_mode="--dry-run"; upgrade_target="" ;; + --auto) export UPDATER_UPGRADE_TRIGGER=auto; upgrade_target="" ;; --*) isError "Unknown option '$upgrade_target'. Usage: libreportal updater upgrade [version] [--dry-run]"; return 1 ;; esac for _f in cli_updater_ladder cli_updater_verify cli_updater_upgrade; do diff --git a/scripts/cli/commands/updater/cli_updater_upgrade.sh b/scripts/cli/commands/updater/cli_updater_upgrade.sh index d8005cf..90b792e 100644 --- a/scripts/cli/commands/updater/cli_updater_upgrade.sh +++ b/scripts/cli/commands/updater/cli_updater_upgrade.sh @@ -127,14 +127,14 @@ updaterUpgradeApp() { isNotice "Snapshotting $app before $rung…" if ! backupAppStart "$app" >/dev/null 2>&1; then isError "Pre-step snapshot failed — stopping with $app on $from." - updaterRecordHistory "$app" "upgrade" "$from" "$rung" "aborted-no-snapshot" "" "" "" "manual" + updaterRecordHistory "$app" "upgrade" "$from" "$rung" "aborted-no-snapshot" "" "" "" "${UPDATER_UPGRADE_TRIGGER:-manual}" return 1 fi # 2. Move the version. if ! updaterSetAnchorVersion "$app" "$rung"; then isError "Could not set $app to $rung — stopping, nothing changed." - updaterRecordHistory "$app" "upgrade" "$from" "$rung" "aborted-set-version" "" "" "" "manual" + updaterRecordHistory "$app" "upgrade" "$from" "$rung" "aborted-set-version" "" "" "" "${UPDATER_UPGRADE_TRIGGER:-manual}" return 1 fi @@ -152,7 +152,7 @@ updaterUpgradeApp() { return 1 fi - updaterRecordHistory "$app" "upgrade" "$from" "$rung" "ok" "" "" "" "manual" + updaterRecordHistory "$app" "upgrade" "$from" "$rung" "ok" "" "" "" "${UPDATER_UPGRADE_TRIGGER:-manual}" isSuccessful "$app is verified on $rung." from="$rung"; done_n=$((done_n + 1)) done @@ -209,12 +209,12 @@ _updaterUpgradeRollbackStep() { updaterSetAnchorVersion "$app" "$from" || isError "Could not put $app's version back to $from — check its compose file." if restoreAppStart "$app" latest "" >/dev/null 2>&1; then dockerComposeUp "$app" >/dev/null 2>&1 || true - updaterRecordHistory "$app" "upgrade" "$from" "$failed" "rolled-back" "" "" "" "manual" + updaterRecordHistory "$app" "upgrade" "$from" "$failed" "rolled-back" "" "" "" "${UPDATER_UPGRADE_TRIGGER:-manual}" isSuccessful "$app restored to $from from its pre-step snapshot." isNotice "The ladder stopped here. Read $failed's release notes before trying again." return 0 fi - updaterRecordHistory "$app" "upgrade" "$from" "$failed" "rollback-failed" "" "" "" "manual" + updaterRecordHistory "$app" "upgrade" "$from" "$failed" "rollback-failed" "" "" "" "${UPDATER_UPGRADE_TRIGGER:-manual}" isError "Could not restore $app automatically. Its data snapshot is intact — restore it from the Backups page." return 1 } diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 267a217..373182f 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -1029,6 +1029,8 @@ declare -gA LP_FN_MAP=( [updaterAppPolicy]="cli/commands/updater/cli_updater_auto.sh" [_updaterAutoDir]="cli/commands/updater/cli_updater_auto.sh" [_updaterAutoGenDir]="cli/commands/updater/cli_updater_auto.sh" + [_updaterAutoRungDay]="cli/commands/updater/cli_updater_auto.sh" + [_updaterAutoRungStamp]="cli/commands/updater/cli_updater_auto.sh" [_updaterAutoStamp]="cli/commands/updater/cli_updater_auto.sh" [updaterAutoTaskPending]="cli/commands/updater/cli_updater_auto.sh" [updaterClassifyTag]="webui/data/generators/updater/webui_updater_scan.sh" @@ -1044,6 +1046,7 @@ declare -gA LP_FN_MAP=( [updaterNewerVersionByList]="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" + [updaterNextRung]="cli/commands/updater/cli_updater_ladder.sh" [_updaterPrimaryContainer]="cli/commands/updater/cli_updater_verify.sh" [updaterPrimaryImage]="webui/data/generators/updater/webui_updater_scan.sh" [updaterRecordHistory]="cli/commands/updater/cli_updater_commands.sh" @@ -1054,7 +1057,7 @@ declare -gA LP_FN_MAP=( [updaterRollbackApp]="cli/commands/updater/cli_updater_commands.sh" [updaterSetAnchorRef]="cli/commands/updater/cli_updater_commands.sh" [updaterSetAnchorVersion]="cli/commands/updater/cli_updater_upgrade.sh" - [updaterTagBumpAt]="webui/data/generators/updater/webui_updater_scan.sh" + [updaterTagBumpAt]="cli/commands/updater/cli_updater_ladder.sh" [updaterTagExists]="cli/commands/updater/cli_updater_ladder.sh" [updaterTagGreater]="webui/data/generators/updater/webui_updater_scan.sh" [updaterTagIncrement]="cli/commands/updater/cli_updater_ladder.sh" @@ -1064,6 +1067,7 @@ declare -gA LP_FN_MAP=( [updaterTagShape]="webui/data/generators/updater/webui_updater_scan.sh" [updaterTagSortKey]="cli/commands/updater/cli_updater_ladder.sh" [updaterUpgradeApp]="cli/commands/updater/cli_updater_upgrade.sh" + [updaterUpgradeAuto]="cli/commands/updater/cli_updater_auto.sh" [_updaterUpgradeGenDir]="cli/commands/updater/cli_updater_upgrade.sh" [_updaterUpgradePruneImages]="cli/commands/updater/cli_updater_upgrade.sh" [_updaterUpgradeRollbackStep]="cli/commands/updater/cli_updater_upgrade.sh" @@ -2180,6 +2184,8 @@ declare -gA LP_FN_ROOT=( [updaterAppPolicy]="scripts" [_updaterAutoDir]="scripts" [_updaterAutoGenDir]="scripts" + [_updaterAutoRungDay]="scripts" + [_updaterAutoRungStamp]="scripts" [_updaterAutoStamp]="scripts" [updaterAutoTaskPending]="scripts" [updaterClassifyTag]="scripts" @@ -2195,6 +2201,7 @@ declare -gA LP_FN_ROOT=( [updaterNewerVersionByList]="scripts" [updaterNewerVersionByProbe]="scripts" [updaterNewerVersionTag]="scripts" + [updaterNextRung]="scripts" [_updaterPrimaryContainer]="scripts" [updaterPrimaryImage]="scripts" [updaterRecordHistory]="scripts" @@ -2215,6 +2222,7 @@ declare -gA LP_FN_ROOT=( [updaterTagShape]="scripts" [updaterTagSortKey]="scripts" [updaterUpgradeApp]="scripts" + [updaterUpgradeAuto]="scripts" [_updaterUpgradeGenDir]="scripts" [_updaterUpgradePruneImages]="scripts" [_updaterUpgradeRollbackStep]="scripts" @@ -3367,6 +3375,8 @@ updaterApplyAuto() { unset -f updaterApplyAuto; __lpAutoload "${install_scripts_ updaterAppPolicy() { unset -f updaterAppPolicy; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; updaterAppPolicy "$@"; } _updaterAutoDir() { unset -f _updaterAutoDir; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoDir "$@"; } _updaterAutoGenDir() { unset -f _updaterAutoGenDir; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoGenDir "$@"; } +_updaterAutoRungDay() { unset -f _updaterAutoRungDay; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoRungDay "$@"; } +_updaterAutoRungStamp() { unset -f _updaterAutoRungStamp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoRungStamp "$@"; } _updaterAutoStamp() { unset -f _updaterAutoStamp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoStamp "$@"; } updaterAutoTaskPending() { unset -f updaterAutoTaskPending; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; updaterAutoTaskPending "$@"; } updaterClassifyTag() { unset -f updaterClassifyTag; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterClassifyTag "$@"; } @@ -3382,6 +3392,7 @@ updaterLastUpdateFrom() { unset -f updaterLastUpdateFrom; __lpAutoload "${instal updaterNewerVersionByList() { unset -f updaterNewerVersionByList; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterNewerVersionByList "$@"; } 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 "$@"; } _updaterPrimaryContainer() { unset -f _updaterPrimaryContainer; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_verify.sh"; _updaterPrimaryContainer "$@"; } updaterPrimaryImage() { unset -f updaterPrimaryImage; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterPrimaryImage "$@"; } updaterRecordHistory() { unset -f updaterRecordHistory; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterRecordHistory "$@"; } @@ -3392,7 +3403,7 @@ updaterRepoTag() { unset -f updaterRepoTag; __lpAutoload "${install_scripts_dir} updaterRollbackApp() { unset -f updaterRollbackApp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterRollbackApp "$@"; } updaterSetAnchorRef() { unset -f updaterSetAnchorRef; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterSetAnchorRef "$@"; } updaterSetAnchorVersion() { unset -f updaterSetAnchorVersion; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_upgrade.sh"; updaterSetAnchorVersion "$@"; } -updaterTagBumpAt() { unset -f updaterTagBumpAt; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterTagBumpAt "$@"; } +updaterTagBumpAt() { unset -f updaterTagBumpAt; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterTagBumpAt "$@"; } updaterTagExists() { unset -f updaterTagExists; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterTagExists "$@"; } updaterTagGreater() { unset -f updaterTagGreater; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterTagGreater "$@"; } updaterTagIncrement() { unset -f updaterTagIncrement; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterTagIncrement "$@"; } @@ -3402,6 +3413,7 @@ updaterTagOf() { unset -f updaterTagOf; __lpAutoload "${install_scripts_dir}webu updaterTagShape() { unset -f updaterTagShape; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterTagShape "$@"; } updaterTagSortKey() { unset -f updaterTagSortKey; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_ladder.sh"; updaterTagSortKey "$@"; } updaterUpgradeApp() { unset -f updaterUpgradeApp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_upgrade.sh"; updaterUpgradeApp "$@"; } +updaterUpgradeAuto() { unset -f updaterUpgradeAuto; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; updaterUpgradeAuto "$@"; } _updaterUpgradeGenDir() { unset -f _updaterUpgradeGenDir; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_upgrade.sh"; _updaterUpgradeGenDir "$@"; } _updaterUpgradePruneImages() { unset -f _updaterUpgradePruneImages; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_upgrade.sh"; _updaterUpgradePruneImages "$@"; } _updaterUpgradeRollbackStep() { unset -f _updaterUpgradeRollbackStep; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_upgrade.sh"; _updaterUpgradeRollbackStep "$@"; } diff --git a/scripts/webui/data/generators/updater/webui_updater_scan.sh b/scripts/webui/data/generators/updater/webui_updater_scan.sh index f8ae189..a22f2fc 100644 --- a/scripts/webui/data/generators/updater/webui_updater_scan.sh +++ b/scripts/webui/data/generators/updater/webui_updater_scan.sh @@ -242,15 +242,18 @@ updaterNewerVersionByList() { # turn one app's scan into a crawl. updaterNewerVersionByProbe() { local cur="$1" repo="$2" - # updaterTagExists lives in the ladder, and a cross-file function is not - # reliably already loaded here — same situation updaterAppPolicy handles - # below, so use the same remedy rather than assuming. Without the explicit - # source a missing function would make this whole fallback silently do - # nothing, which is precisely the failure mode it exists to remove. - if ! declare -F updaterTagExists >/dev/null 2>&1; then + # updaterTagExists and updaterTagBumpAt both live in the ladder, and a + # cross-file function is not reliably already loaded here — same situation + # updaterAppPolicy handles below, so use the same remedy rather than + # assuming. Without the explicit source a missing function would make this + # whole fallback silently do nothing, which is precisely the failure mode it + # exists to remove. Check BOTH: they arrive together, but a guard that only + # names one is a guard that stops being true the day the other moves. + if ! declare -F updaterTagExists >/dev/null 2>&1 || ! declare -F updaterTagBumpAt >/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 declare -F updaterTagExists >/dev/null 2>&1 || return 0 + declare -F updaterTagBumpAt >/dev/null 2>&1 || return 0 fi local shape; shape="$(updaterTagShape "$cur")" local ncomp; ncomp="$(printf '%s' "$cur" | grep -oE '[0-9]+' | wc -l | tr -d ' ')"