Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/verify-split-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions core/error_handler.func
Original file line number Diff line number Diff line change
Expand Up @@ -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:-}"
Expand Down
16 changes: 12 additions & 4 deletions lib/forge.func
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions tests/release-stamp-pending.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/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

# 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"; }
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))
Loading