From e99ad6409f17bcadbda2f94341417b24445ae8c4 Mon Sep 17 00:00:00 2001 From: MickLesk Date: Fri, 19 Jun 2026 22:51:53 +0200 Subject: [PATCH] feat(build): standardize in-container update backup recovery Auto-restore on update failure via create_backup ERR trap and clear backup stores after successful updates inside the LXC start() flow. Co-authored-by: Cursor --- misc/build.func | 28 ++++++++++++++++-- misc/tools.func | 20 +++++++++++++ tools/ci/check-update-backup.sh | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100755 tools/ci/check-update-backup.sh diff --git a/misc/build.func b/misc/build.func index b90c89901..ed2270991 100644 --- a/misc/build.func +++ b/misc/build.func @@ -3692,6 +3692,28 @@ runtime_script_status_guard() { return 0 } +# ------------------------------------------------------------------------------ +# _run_update_script() +# +# - Runs update_script inside the LXC with standardized backup recovery +# - create_backup() arms an ERR trap; successful updates clear the backup store +# - Works even when update_script ends with exit (EXIT trap) +# ------------------------------------------------------------------------------ +_run_update_script() { + local _update_rc=0 + _on_update_script_exit() { + local _exit_rc=$? + trap - EXIT + if [[ $_exit_rc -eq 0 ]] && declare -f clear_update_backup &>/dev/null; then + clear_update_backup + fi + exit "$_exit_rc" + } + trap '_on_update_script_exit' EXIT + update_script || _update_rc=$? + return "$_update_rc" +} + # ------------------------------------------------------------------------------ # start() # @@ -3713,7 +3735,7 @@ start() { ensure_profile_loaded get_lxc_ip runtime_script_status_guard || return 0 - update_script + _run_update_script run_addon_updates update_motd_ip cleanup_lxc @@ -3724,7 +3746,7 @@ start() { ensure_profile_loaded get_lxc_ip runtime_script_status_guard || return 0 - update_script + _run_update_script run_addon_updates update_motd_ip cleanup_lxc @@ -3754,7 +3776,7 @@ start() { ensure_profile_loaded get_lxc_ip runtime_script_status_guard || return 0 - update_script + _run_update_script run_addon_updates update_motd_ip cleanup_lxc diff --git a/misc/tools.func b/misc/tools.func index 6a0c3ed88..72183c842 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -1162,6 +1162,7 @@ create_backup() { if [[ -f "$manifest" ]]; then msg_ok "Existing backup found at ${store}, skipping backup" + trap '_restore_update_backup_on_error' ERR return 0 fi @@ -1186,6 +1187,24 @@ create_backup() { echo "$path" >>"$manifest" done msg_ok "Backed up data to ${store}" + trap '_restore_update_backup_on_error' ERR +} + +_restore_update_backup_on_error() { + local _err=$? + trap - ERR + if [[ -f "${BACKUP_DIR:-/opt/${NSAPP:-app}.backup}/.manifest" ]]; then + msg_error "Update failed (exit ${_err}) – restoring backup" + restore_backup + fi + exit "${_err:-1}" +} + +clear_update_backup() { + local store="${BACKUP_DIR:-/opt/${NSAPP:-app}.backup}" + [[ -d "$store" ]] || return 0 + rm -rf "$store" + trap - ERR } restore_backup() { @@ -1208,6 +1227,7 @@ restore_backup() { cp -a "$src" "$path" done <"$manifest" rm -rf "$store" + trap - ERR msg_ok "Restored data" } diff --git a/tools/ci/check-update-backup.sh b/tools/ci/check-update-backup.sh new file mode 100755 index 000000000..fe84750a7 --- /dev/null +++ b/tools/ci/check-update-backup.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Copyright (c) 2021-2026 community-scripts ORG +# License: MIT +# +# Flags ct/*.sh update_script blocks that mutate config/data destructively +# without calling create_backup. Used in CI / local review before merge. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CT_DIR="${ROOT}/ct" +FAIL=0 +CHECKED=0 +FLAGGED=0 + +check_file() { + local file="$1" + local base content block + base="$(basename "$file")" + content="$(<"$file")" + [[ "$content" == *"function update_script"* ]] || return 0 + CHECKED=$((CHECKED + 1)) + + block="$(python3 - "$file" <<'PY' +import re, sys +text = open(sys.argv[1]).read() +m = re.search(r'function update_script\(\).*?(?=^function |\Z)', text, re.S | re.M) +print(m.group() if m else "") +PY +)" + + [[ -n "$block" ]] || return 0 + [[ "$block" == *"create_backup"* ]] && return 0 + + if echo "$block" | grep -qE 'sed -i|\.env|settings\.(py|json)|config\.(json|yml|yaml)|/etc/[^ ]+\.(conf|env)'; then + if echo "$block" | grep -qE 'rm -rf|find .* -delete|mv .*\.(bak|old)'; then + echo "MISSING create_backup: ct/${base}" + FLAGGED=$((FLAGGED + 1)) + FAIL=1 + fi + fi +} + +for f in "$CT_DIR"/*.sh; do + [[ -f "$f" ]] || continue + check_file "$f" +done + +echo "Checked ${CHECKED} update scripts, flagged ${FLAGGED} without create_backup" +exit "$FAIL"