From cc7eb1500dcda3df66ff3b73f035e651372007fb Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:39:19 +0200 Subject: [PATCH 1/2] Redo an update whose run did not finish after the deploy The stamp fetch_and_deploy_release writes only says "deployed"; a build that fails afterwards (NPM, Immich, Paperclip) left the new stamp behind and the next check said "No update available". A pending marker now stays next to the stamp until a run that deployed exits 0, and check_for_release redoes the update while it exists (ProxmoxVE #18050). --- .github/workflows/verify-split-files.yml | 5 ++ core/error_handler.func | 6 ++ lib/forge.func | 16 +++-- tests/release-stamp-pending.sh | 78 ++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 tests/release-stamp-pending.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index c2f8727..7b5f10a 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -36,6 +36,7 @@ on: - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" + - "tests/release-stamp-pending.sh" - ".github/workflows/verify-split-files.yml" push: branches: [main] @@ -63,6 +64,7 @@ on: - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" - "tests/install-cleanup.sh" + - "tests/release-stamp-pending.sh" - ".github/workflows/verify-split-files.yml" workflow_dispatch: @@ -126,6 +128,9 @@ jobs: - name: Install cleanup and uv trap tests run: bash tests/install-cleanup.sh + - name: Release stamp pending marker tests + run: bash tests/release-stamp-pending.sh + - name: Prefetch lists cover every engine file run: | set -euo pipefail diff --git a/core/error_handler.func b/core/error_handler.func index 88a0e71..fc06edd 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -769,6 +769,12 @@ on_exit() { cleanup_lxc || true fi + # fetch_and_deploy_release leaves a pending marker next to the stamp; only a + # run that deployed and exits 0 may remove it. + if [[ $exit_code -eq 0 && "${_CS_DEPLOYED:-}" == "1" ]]; then + rm -f "${HOME:-/root}"/.*.pending 2>/dev/null || true + fi + if _is_container_context; then if [[ $exit_code -ne 0 ]]; then _container_write_failure "$exit_code" "${FAILED_COMMAND:-}" "${FAILED_LINE:-}" diff --git a/lib/forge.func b/lib/forge.func index d53c9c6..df05be5 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -1794,6 +1794,8 @@ check_for_release() { fi fi current="$(_forge_strip_v "$current")" + local unfinished="" + [[ -f "${current_file}.pending" ]] && unfinished=" (the last run did not finish, doing it again)" if [[ -n "$pinned_version_in" ]]; then local pin_clean match_raw="" i @@ -1810,9 +1812,9 @@ check_for_release() { return 250 fi - if [[ "$current" != "$pin_clean" ]]; then + if [[ "$current" != "$pin_clean" || -n "$unfinished" ]]; then CHECK_UPDATE_RELEASE="$match_raw" - msg_ok "Update available: ${app} ${current:-not installed} -> ${pin_clean}" + msg_ok "Update available: ${app} ${current:-not installed} -> ${pin_clean}${unfinished}" return 0 fi @@ -1824,13 +1826,13 @@ check_for_release() { return 1 fi - if _forge_is_current "$current" "$latest_clean"; then + if [[ -z "$unfinished" ]] && _forge_is_current "$current" "$latest_clean"; then msg_ok "No update available: ${app} (${current})" return 1 fi CHECK_UPDATE_RELEASE="$latest_raw" - msg_ok "Update available: ${app} ${current:-not installed} -> ${latest_clean}" + msg_ok "Update available: ${app} ${current:-not installed} -> ${latest_clean}${unfinished}" return 0 } @@ -2095,6 +2097,12 @@ fetch_and_deploy_release() { # throughout, which mangles tags like version-2. version="$(_forge_strip_v "$tag_name")" + # The stamp written at the end only says "deployed". This marker says the + # run around it still has to finish: on_exit removes it when the run exits 0, + # and check_for_release redoes the update while it exists (ProxmoxVE #18050). + touch "${version_file}.pending" 2>/dev/null || true + _CS_DEPLOYED=1 + local current_version="" [[ -f "$version_file" ]] && current_version="$(<"$version_file")" if [[ "$current_version" == "$version" && -d "$target" && "${FORCE_UPDATE:-}" != "1" ]]; then diff --git a/tests/release-stamp-pending.sh b/tests/release-stamp-pending.sh new file mode 100644 index 0000000..e9b6b15 --- /dev/null +++ b/tests/release-stamp-pending.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(sed -n '/^_FORGE_JQ_VKEY_DEF=/,/^_FORGE_JQ_VKEY=/p' "$CORE_DIR/lib/forge.func")" +eval "$(awk '/^_forge_(descriptor|strip_v|is_current|highest_version|same_shape|stable_filter)[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" +eval "$(awk '/^check_for_release[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" +eval "$(awk '/^on_exit[(][)] [{]/,/^}/' "$CORE_DIR/core/error_handler.func")" + +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +export HOME="$TEST_DIR/home" +mkdir -p "$HOME" +LOG="$TEST_DIR/log" +passed=0 failed=0 + +msg_info() { :; } +msg_ok() { echo "ok: $*" >>"$LOG"; } +msg_warn() { echo "warn: $*" >>"$LOG"; } +msg_error() { echo "error: $*" >>"$LOG"; } +ensure_dependencies() { :; } +getent() { return 0; } +_forge_designated_release() { return 1; } +# The list for the normal path, the single tag for the pinned one. +_forge_http_get() { + if [[ "$2" == */releases/tags/* ]]; then + printf '%s' '{"tag_name":"v2.16.0","prerelease":false,"draft":false}' >"$3" + else + printf '%s' '[{"tag_name":"v2.16.0","prerelease":false,"draft":false},{"tag_name":"v2.15.1","prerelease":false,"draft":false}]' >"$3" + fi +} +_is_container_context() { return 0; } +_container_write_failure() { :; } + +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + sed 's/^/ /' "$LOG" >&2 + failed=$((failed + 1)) + fi +} +# stamp, marker (yes/no), then the optional pin arguments; prints the return code +check() { + local stamp="$1" marker="$2" + shift 2 + : >"$LOG" + rm -f "$HOME/.npm" "$HOME/.npm.pending" + [[ -n "$stamp" ]] && echo "$stamp" >"$HOME/.npm" + [[ "$marker" == "yes" ]] && touch "$HOME/.npm.pending" + local rc=0 + export CHECK_UPDATE_RELEASE="" + check_for_release github NPM owner/repo "$@" >/dev/null 2>&1 || rc=$? + echo "$rc" +} + +expect "current stamp, no marker: nothing to do" 1 "$(check 2.16.0 no)" +expect "current stamp, marker: the update is redone" 0 "$(check 2.16.0 yes)" +expect "the message says why" 1 "$(grep -c 'did not finish' "$LOG")" +expect "old stamp: a normal update" 0 "$(check 2.15.1 no)" +expect "no stamp at all: a normal install" 0 "$(check "" no)" +expect "pinned and current: held back" 1 "$(check 2.16.0 no 2.16.0 "testing")" +expect "pinned, current, marker: redone anyway" 0 "$(check 2.16.0 yes 2.16.0 "testing")" + +# on_exit: only a run that deployed and exits 0 removes the marker +touch "$HOME/.npm.pending" +(_CS_DEPLOYED=1; on_exit) 2>/dev/null +expect "a clean exit after a deploy removes the marker" no "$([[ -f "$HOME/.npm.pending" ]] && echo yes || echo no)" +touch "$HOME/.npm.pending" +(on_exit) 2>/dev/null +expect "a clean exit without a deploy keeps it" yes "$([[ -f "$HOME/.npm.pending" ]] && echo yes || echo no)" +(_CS_DEPLOYED=1; false; on_exit) 2>/dev/null +expect "a failed run keeps it" yes "$([[ -f "$HOME/.npm.pending" ]] && echo yes || echo no)" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) From ba249e5cb32076cc4b7282f730a0bc35b1b30d5b Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:41:54 +0200 Subject: [PATCH 2/2] Strip the CR a Windows jq adds in the pending marker test --- tests/release-stamp-pending.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/release-stamp-pending.sh b/tests/release-stamp-pending.sh index e9b6b15..e76a26b 100644 --- a/tests/release-stamp-pending.sh +++ b/tests/release-stamp-pending.sh @@ -14,6 +14,8 @@ mkdir -p "$HOME" LOG="$TEST_DIR/log" passed=0 failed=0 +# jq on Windows prints CRLF; mapfile -t keeps the CR and the pin compare is exact. +jq() { command jq "$@" | tr -d '\r'; } msg_info() { :; } msg_ok() { echo "ok: $*" >>"$LOG"; } msg_warn() { echo "warn: $*" >>"$LOG"; }