Files
pulse/scripts/tests/test-pulse-auto-update.sh
T
rcourtman 6351fd6526 fix(updates): select releases by highest version, not GitHub list order
Production telemetry (7d, 2026-07-17) shows a persistent tail of live
installs on 6.0.0-rc.x and 6.0.3/6.0.4 while 6.0.5 has been stable since
2026-07-09. Root cause for the reachable stranding class: release
selection trusted GitHub's created_at ordering, and this repo interleaves
v5-line maintenance releases with v6 releases (v5.1.36 was created the
day before v6.0.5). Whenever a v5.1.x release is the most recently
created stable:

- stable channel (getLatestReleaseForChannel) returned the first
  non-prerelease in list order -> "no update" for every v6 install
  until the next v6 release ships
- the RSS rate-limit fallback returned the first feed entry matching the
  channel, same failure, both channels
- the unattended updater trusted /releases/latest, which GitHub defines
  as most-recently-created stable -> timer no-ops in the same window

All three now select the highest eligible version: the API path tracks
newest stable + newest prerelease in one pass (prerelease = GitHub flag
OR tag shape), the feed fallback picks the max matching the channel, and
pulse-auto-update.sh scans /releases pairing each tag with its own
draft/prerelease flags before the fail-closed shape filter, keeping
/releases/latest as fallback. helm-chart-* tags fail semver parsing and
are dropped everywhere.

Verified not broken (pinned by new tests): rc-channel installs are
offered the newer stable (6.0.0-rc.6 -> 6.0.5) and move onto the next rc
line when one opens (-> 6.1.0-rc.2); the auto-update prerelease filter
(fail-closed is_prerelease_tag, metadata-flag refusal) still refuses
prerelease targets on stable. Go tests fail on the pre-fix code
(returned 5.1.37 / "no update").

Contract: deployment-installability now pins version-max release
selection with proofs in manager_stranded_upgrade_test.go and
pulse_auto_update_test.go.
2026-07-17 23:21:49 +01:00

218 lines
5.8 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Smoke tests for scripts/pulse-auto-update.sh helper behavior (#1323).
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
AUTO_UPDATE_SCRIPT="${ROOT_DIR}/scripts/pulse-auto-update.sh"
if [[ ! -f "${AUTO_UPDATE_SCRIPT}" ]]; then
echo "pulse-auto-update.sh not found at ${AUTO_UPDATE_SCRIPT}" >&2
exit 1
fi
# Sourcing relies on the BASH_SOURCE guard so main() does not run on import.
# shellcheck disable=SC1090
source "${AUTO_UPDATE_SCRIPT}"
failures=0
assert_success() {
local desc="$1"
shift
if "$@"; then
echo "[PASS] ${desc}"
return 0
else
echo "[FAIL] ${desc}" >&2
((failures++))
return 1
fi
}
test_wait_for_service_active_succeeds_after_retry() {
local calls=0
systemctl() {
if [[ "$1" == "is-active" ]]; then
((calls += 1))
if (( calls >= 3 )); then
return 0
fi
return 1
fi
return 1
}
sleep() { :; }
wait_for_service_active pulse 5
}
test_wait_for_service_active_times_out_when_never_active() {
systemctl() { return 1; }
sleep() { :; }
if wait_for_service_active pulse 3; then
echo "expected wait_for_service_active to fail when service never becomes active" >&2
return 1
fi
return 0
}
test_pick_highest_stable_tag_ignores_list_order_and_prereleases() {
local picked
picked=$(pick_highest_stable_tag <<'EOF'
v5.1.37
helm-chart-6.0.5
v6.0.5-rc.4
v6.0.5
v6.0.4
v6.0.0-rc.7
EOF
)
if [[ "$picked" != "v6.0.5" ]]; then
echo "pick_highest_stable_tag picked ${picked:-<empty>}, want v6.0.5" >&2
return 1
fi
return 0
}
test_get_latest_stable_version_prefers_highest_over_created_order() {
# A v5-line maintenance release created after v6.0.5 sits first in the
# /releases list and is what /releases/latest would point at; the updater
# must still pick v6.0.5.
curl() {
local url="${*: -1}"
if [[ "$url" == *"/releases?per_page="* ]]; then
cat <<'EOF'
[
{ "tag_name": "v5.1.37", "prerelease": false },
{ "tag_name": "helm-chart-6.0.5", "prerelease": true },
{ "tag_name": "v6.0.5", "prerelease": false },
{ "tag_name": "v6.0.5-rc.4", "prerelease": true },
{ "tag_name": "v6.0.4", "prerelease": false }
]
EOF
return 0
fi
echo "unexpected curl call in test: $*" >&2
return 1
}
local got
got=$(get_latest_stable_version)
unset -f curl
if [[ "$got" != "v6.0.5" ]]; then
echo "get_latest_stable_version returned ${got:-<empty>}, want v6.0.5" >&2
return 1
fi
return 0
}
test_perform_update_restores_backup_when_service_stays_down() {
local tmpdir
tmpdir="$(mktemp -d)"
local status=0
# perform_update installs a RETURN trap referencing these; declare them here
# so the trap is safe under set -u if it surfaces in this calling scope.
local installer_tmp="" signature_tmp=""
INSTALL_DIR="${tmpdir}/opt/pulse"
CONFIG_DIR="${tmpdir}/etc/pulse"
mkdir -p "${INSTALL_DIR}/bin" "${CONFIG_DIR}"
printf 'v5.1.24\n' > "${INSTALL_DIR}/VERSION"
cat > "${INSTALL_DIR}/bin/pulse" <<'EOF'
#!/usr/bin/env bash
echo "v5.1.24"
EOF
chmod +x "${INSTALL_DIR}/bin/pulse"
export INSTALL_DIR
export FAKE_NEW_VERSION="v5.1.25"
# Stub out the v6 download/verify pipeline so perform_update reaches the
# post-install service check deterministically.
is_prerelease_tag() { return 1; }
detect_service_name() { echo "pulse"; }
resolve_install_script_url() { echo "http://localhost/install.sh"; }
verify_release_signature() { return 0; }
get_current_version() { tr -d '\r\n' < "${INSTALL_DIR}/VERSION"; }
# curl writes the installer / signature to the -o target. The fake installer
# bumps VERSION to the new version, simulating a successful install.
curl() {
local out="" prev=""
local arg
for arg in "$@"; do
if [[ "$prev" == "-o" ]]; then out="$arg"; fi
prev="$arg"
done
if [[ -n "$out" ]]; then
case "$out" in
*.sig.*) printf 'dummy-signature\n' > "$out" ;;
*)
cat > "$out" <<'INSTALLER'
#!/usr/bin/env bash
printf '%s\n' "${FAKE_NEW_VERSION}" > "${PULSE_INSTALL_DIR}/VERSION"
exit 0
INSTALLER
;;
esac
fi
return 0
}
# Service was running before the update (first is-active call true), then
# never comes back up; start also fails -> perform_update must restore + fail.
local is_active_calls=0
systemctl() {
if [[ "$1" == "is-active" ]]; then
((is_active_calls += 1))
if (( is_active_calls == 1 )); then
return 0
fi
return 1
fi
return 1
}
sleep() { :; }
if perform_update "v5.1.25"; then
echo "perform_update unexpectedly succeeded while service stayed down" >&2
status=1
fi
# perform_update installs a RETURN trap; clear it so it does not leak into
# subsequent function returns in this sourced test harness.
trap - RETURN 2>/dev/null || true
if [[ "${status}" -eq 0 ]] && [[ "$(tr -d '\r\n' < "${INSTALL_DIR}/VERSION")" != "v5.1.24" ]]; then
echo "expected VERSION to be restored to v5.1.24 after failed restart" >&2
status=1
fi
rm -rf "${tmpdir}"
return "${status}"
}
main() {
assert_success "wait_for_service_active retries until active" test_wait_for_service_active_succeeds_after_retry
assert_success "wait_for_service_active times out when never active" test_wait_for_service_active_times_out_when_never_active
assert_success "pick_highest_stable_tag ignores list order and prereleases" test_pick_highest_stable_tag_ignores_list_order_and_prereleases
assert_success "get_latest_stable_version prefers highest version over created order" test_get_latest_stable_version_prefers_highest_over_created_order
assert_success "perform_update restores backup when service stays down" test_perform_update_restores_backup_when_service_stays_down
if (( failures > 0 )); then
echo "Total failures: ${failures}" >&2
return 1
fi
echo "All pulse-auto-update smoke tests passed."
}
main "$@"