From 132576e13ca5d83ee7db9be038e836e2bf304754 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 25 Aug 2026 21:30:16 +0700 Subject: [PATCH 1/6] feat: install script to check for hard stops The idea came from Discord, and Alex (stayalive) lay out a very good approach on this: https://discord.com/channels/621778831602221064/796028405833007104/1541789134006259814 --- install.sh | 1 + install/check-hard-stop.sh | 203 +++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 install/check-hard-stop.sh diff --git a/install.sh b/install.sh index 3b4d9b50011..2cdef4c7f23 100755 --- a/install.sh +++ b/install.sh @@ -21,6 +21,7 @@ source install/dc-detect-version.sh source install/error-handling.sh # We set the trap at the top level so that we get better tracebacks. trap_with_arg cleanup ERR INT TERM EXIT +source install/check-hard-stop.sh source install/check-latest-commit.sh source install/check-minimum-requirements.sh diff --git a/install/check-hard-stop.sh b/install/check-hard-stop.sh new file mode 100644 index 00000000000..a3ce08cad99 --- /dev/null +++ b/install/check-hard-stop.sh @@ -0,0 +1,203 @@ +# The idea of this file is to prevent users from skipping a hard stop. +# This is done by creating a file in /var/run/sentry-hard-stop (or anything set +# in HARD_STOP_FILE) and checking for its existence before. If the file exists, +# we assume (and trust) that it's the latest version of the self-hosted sentry +# version, and no further check (git tag, values on `.env` ) would be done. +# Otherwise, we assume either this is the first installation, or the first +# time after this file is being written, and write into that file (by reading +# either the Git tag or values on `.env` or `.env.custom`). +# +# If the "reading" part fails anyway, we would skip this process and continue +# with the installation. +# +# If the user skipped a hard stop, we would halt the installation (or maybe, +# cancel it altogether), and ask for confirmation. +# +# This bit is written by a human. + +echo "${_group}Checking for hard stop ... " + +latest_version_file=${HARD_STOP_FILE:-"/var/run/sentry-hard-stop"} +# This should be a bash array string, and should be equivalent with the list +# on https://develop.sentry.dev/self-hosted/releases/#hard-stops +hard_stops=("9.1.2" "21.5.0" "21.6.3" "23.6.2" "23.11.0" "24.8.0" "25.5.1" "26.5.0" "26.7.0") + +_write_latest_version() { + echo "$1" >"$latest_version_file" +} + +# Helper function to parse version components +# BASH_REMATCH requires Bash 3.0+ +_parse_version_components() { + local ver="$1" + if [[ $ver =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]*)?(\+[0-9A-Za-z.-]*)?$ ]]; then + echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]#-} ${BASH_REMATCH[5]#+}" + else + echo "" + fi +} + +# Compare two calver versions +# Usage: compare_calver "1.2.3" "1.2.4" +# Returns: -1 if first < second, 0 if equal, 1 if first > second +# +# This bit is written by Claude Haiku 4.5. +compare_calver() { + local v1="$1" + local v2="$2" + + if [[ -z "$v1" ]] || [[ -z "$v2" ]]; then + echo -e "ERROR: Invalid CalVer format" >&2 + exit 1 + fi + + # Remove leading 'v' if present + v1="${v1#v}" + v2="${v2#v}" + + # Extract components + local parsed1=$(_parse_version_components "$v1") + local parsed2=$(_parse_version_components "$v2") + + if [[ -z "$parsed1" ]] || [[ -z "$parsed2" ]]; then + echo -e "ERROR: Invalid CalVer format" >&2 + exit 1 + fi + + # Compare major.minor.patch + local arr1=($parsed1) + local arr2=($parsed2) + + if ((arr1[0] > arr2[0])); then + return 1 + elif ((arr1[0] < arr2[0])); then + return -1 + fi + + if ((arr1[1] > arr2[1])); then + return 1 + elif ((arr1[1] < arr2[1])); then + return -1 + fi + + if ((arr1[2] > arr2[2])); then + return 1 + elif ((arr1[2] < arr2[2])); then + return -1 + fi + + # Compare prerelease versions (versions without prerelease > versions with prerelease) + local pre1="${arr1[3]}" + local pre2="${arr2[3]}" + + if [[ -z "$pre1" ]] && [[ -n "$pre2" ]]; then + return 1 # v1 > v2 (release > prerelease) + elif [[ -n "$pre1" ]] && [[ -z "$pre2" ]]; then + return -1 # v1 < v2 (prerelease < release) + elif [[ -n "$pre1" ]] && [[ -n "$pre2" ]]; then + if [[ "$pre1" > "$pre2" ]]; then + return 1 + elif [[ "$pre1" < "$pre2" ]]; then + return -1 + fi + fi + + return 0 # Equal +} + +# Acquire the new version. This is done by reading `.env` / `.env.custom` +# for Docker image tags; or by reading the Git tag for the current commit +declare new_version +# if `.env.custom` exists, prioritize it over `.env` +if [[ -f ".env.custom" ]]; then + source .env.custom + new_version=$(grep -E '^SENTRY_IMAGE=' .env.custom | sed 's/^.*=//' | cut -d: -f2) +fi + +if [[ -z "$new_version" ]]; then + new_version=$(grep -E '^SENTRY_IMAGE=' .env | sed 's/^.*=//' | cut -d: -f2) +fi + +if [[ -z "$new_version" ]]; then + # Check whether `git` exists as a command, and `.git` directory exists + if [[ -n "$(command -v git)" ]] && [[ -d "../.git" ]]; then + # Get the latest tag from the repository + new_version=$(git describe --tags --abbrev=0) + fi +fi + +# If the `new_version` is still empty, we emit a warning that +# they're on their own +if [[ -z "$new_version" ]]; then + echo "--------------------------------------------------------------------------------" + echo "WARNING: Could not determine the current version of the self-hosted Sentry" + echo "to perform a hard stop check. Assuming you know what you're doing. Good luck." + echo "--------------------------------------------------------------------------------" + echo "${_endgroup}" + exit 0 # Should not exit the entire `install` process. +fi + +# If the `new_version` is nightly, we emit a different warning. +# This is for fun. +if [[ "$new_version" == "nightly" ]]; then + echo "--------------------------------------------------------------------------------" + echo "WARNING: Hello, dear brave traveler. You are installing the nightly version." + echo "The hard stop check is skipped for this version. We wish you a safe journey." + echo "Good luck." + echo "--------------------------------------------------------------------------------" + echo "${_endgroup}" + exit 0 +fi + +# Acquire the current version. Read the file. +declare current_version +if [[ -f "$latest_version_file" ]]; then + current_version=$(cat "$latest_version_file") +fi + +# We perform some checks if the `current_version` is not empty. +if [[ -n "$current_version" ]]; then + # We iterate over the list of hard stops, and check whether the current + # version is below any of them. + for hard_stop in "${hard_stops[@]}"; do + compare_result=$(compare_calver "$current_version" "$hard_stop") + if [[ "$compare_result" == 0 ]]; then + # equal, this is correct, they're visiting a hard stop + _write_latest_version "$new_version" + echo "${_endgroup}" + exit 0 + elif [[ "$compare_result" == 1 ]]; then + # the current version is greater than the current hard stop loop, we continue + continue + fi + + # the current version is less than the current hard stop loop + # we alert the user and provide a confirmation + echo "--------------------------------------------------------------------------------" + echo + echo "WARNING: Your new version ($new_version) will skip a required hard stop of $hard_stop." + echo "It is recommended to stop the current installation, and go through the hard stop first." + echo "Otherwise, you may encounter unexpected behaviors, such as migration failures, or data loss." + echo + echo "For future reference, please visit https://develop.sentry.dev/self-hosted/releases/#hard-stops" + echo + echo "Do you wish to continue? [y/N]" + read -r confirmation + + if [[ "$confirmation" == "y" ]]; then + _write_latest_version "$new_version" + echo "${_endgroup}" + exit 0 + else + echo "Canceled. 😅" + exit 1 + fi + done +else + # If the `current_version` is empty (or the file does not exists), we assume + # this is a new installation. + echo "Self-hosted Sentry version tracking file not found. No hard stop check is needed." + _write_latest_version "$new_version" +fi + +echo "${_endgroup}" From 18d204f05aa0e91812d97f2de2bbaa2750746815 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 25 Aug 2026 21:43:09 +0700 Subject: [PATCH 2/6] fix: wrap exit code on a subshell --- install/check-hard-stop.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/install/check-hard-stop.sh b/install/check-hard-stop.sh index a3ce08cad99..edec418cc9e 100644 --- a/install/check-hard-stop.sh +++ b/install/check-hard-stop.sh @@ -48,7 +48,7 @@ compare_calver() { if [[ -z "$v1" ]] || [[ -z "$v2" ]]; then echo -e "ERROR: Invalid CalVer format" >&2 - exit 1 + return 2 fi # Remove leading 'v' if present @@ -61,7 +61,7 @@ compare_calver() { if [[ -z "$parsed1" ]] || [[ -z "$parsed2" ]]; then echo -e "ERROR: Invalid CalVer format" >&2 - exit 1 + return 2 fi # Compare major.minor.patch @@ -134,7 +134,7 @@ if [[ -z "$new_version" ]]; then echo "to perform a hard stop check. Assuming you know what you're doing. Good luck." echo "--------------------------------------------------------------------------------" echo "${_endgroup}" - exit 0 # Should not exit the entire `install` process. + (exit 0) # Should not exit the entire `install` process. fi # If the `new_version` is nightly, we emit a different warning. @@ -146,7 +146,7 @@ if [[ "$new_version" == "nightly" ]]; then echo "Good luck." echo "--------------------------------------------------------------------------------" echo "${_endgroup}" - exit 0 + (exit 0) fi # Acquire the current version. Read the file. @@ -165,10 +165,14 @@ if [[ -n "$current_version" ]]; then # equal, this is correct, they're visiting a hard stop _write_latest_version "$new_version" echo "${_endgroup}" - exit 0 + (exit 0) elif [[ "$compare_result" == 1 ]]; then # the current version is greater than the current hard stop loop, we continue continue + elif [[ "$compare_result" == 2 ]]; then + # invalid version, we exit + echo -e "ERROR: Invalid version in $latest_version_file" + exit 1 fi # the current version is less than the current hard stop loop @@ -187,7 +191,7 @@ if [[ -n "$current_version" ]]; then if [[ "$confirmation" == "y" ]]; then _write_latest_version "$new_version" echo "${_endgroup}" - exit 0 + (exit 0) else echo "Canceled. 😅" exit 1 From 3af65dd2470b492ba604c2140948f975b8a8a2e8 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 26 Aug 2026 08:20:41 +0700 Subject: [PATCH 3/6] fix: never have exit 0 --- install/check-hard-stop.sh | 142 +++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 78 deletions(-) diff --git a/install/check-hard-stop.sh b/install/check-hard-stop.sh index edec418cc9e..e432300419d 100644 --- a/install/check-hard-stop.sh +++ b/install/check-hard-stop.sh @@ -17,7 +17,7 @@ echo "${_group}Checking for hard stop ... " -latest_version_file=${HARD_STOP_FILE:-"/var/run/sentry-hard-stop"} +latest_version_file=${HARD_STOP_FILE:-".sentry-hard-stop"} # This should be a bash array string, and should be equivalent with the list # on https://develop.sentry.dev/self-hosted/releases/#hard-stops hard_stops=("9.1.2" "21.5.0" "21.6.3" "23.6.2" "23.11.0" "24.8.0" "25.5.1" "26.5.0" "26.7.0") @@ -86,22 +86,6 @@ compare_calver() { return -1 fi - # Compare prerelease versions (versions without prerelease > versions with prerelease) - local pre1="${arr1[3]}" - local pre2="${arr2[3]}" - - if [[ -z "$pre1" ]] && [[ -n "$pre2" ]]; then - return 1 # v1 > v2 (release > prerelease) - elif [[ -n "$pre1" ]] && [[ -z "$pre2" ]]; then - return -1 # v1 < v2 (prerelease < release) - elif [[ -n "$pre1" ]] && [[ -n "$pre2" ]]; then - if [[ "$pre1" > "$pre2" ]]; then - return 1 - elif [[ "$pre1" < "$pre2" ]]; then - return -1 - fi - fi - return 0 # Equal } @@ -110,12 +94,11 @@ compare_calver() { declare new_version # if `.env.custom` exists, prioritize it over `.env` if [[ -f ".env.custom" ]]; then - source .env.custom - new_version=$(grep -E '^SENTRY_IMAGE=' .env.custom | sed 's/^.*=//' | cut -d: -f2) + new_version=$(grep -E '^SENTRY_IMAGE=' .env.custom | sed 's/^.*=//' | cut -d: -f2 || true) fi if [[ -z "$new_version" ]]; then - new_version=$(grep -E '^SENTRY_IMAGE=' .env | sed 's/^.*=//' | cut -d: -f2) + new_version=$(grep -E '^SENTRY_IMAGE=' .env | sed 's/^.*=//' | cut -d: -f2 || true) fi if [[ -z "$new_version" ]]; then @@ -133,8 +116,6 @@ if [[ -z "$new_version" ]]; then echo "WARNING: Could not determine the current version of the self-hosted Sentry" echo "to perform a hard stop check. Assuming you know what you're doing. Good luck." echo "--------------------------------------------------------------------------------" - echo "${_endgroup}" - (exit 0) # Should not exit the entire `install` process. fi # If the `new_version` is nightly, we emit a different warning. @@ -145,63 +126,68 @@ if [[ "$new_version" == "nightly" ]]; then echo "The hard stop check is skipped for this version. We wish you a safe journey." echo "Good luck." echo "--------------------------------------------------------------------------------" - echo "${_endgroup}" - (exit 0) -fi - -# Acquire the current version. Read the file. -declare current_version -if [[ -f "$latest_version_file" ]]; then - current_version=$(cat "$latest_version_file") -fi - -# We perform some checks if the `current_version` is not empty. -if [[ -n "$current_version" ]]; then - # We iterate over the list of hard stops, and check whether the current - # version is below any of them. - for hard_stop in "${hard_stops[@]}"; do - compare_result=$(compare_calver "$current_version" "$hard_stop") - if [[ "$compare_result" == 0 ]]; then - # equal, this is correct, they're visiting a hard stop - _write_latest_version "$new_version" - echo "${_endgroup}" - (exit 0) - elif [[ "$compare_result" == 1 ]]; then - # the current version is greater than the current hard stop loop, we continue - continue - elif [[ "$compare_result" == 2 ]]; then - # invalid version, we exit - echo -e "ERROR: Invalid version in $latest_version_file" - exit 1 - fi - - # the current version is less than the current hard stop loop - # we alert the user and provide a confirmation - echo "--------------------------------------------------------------------------------" - echo - echo "WARNING: Your new version ($new_version) will skip a required hard stop of $hard_stop." - echo "It is recommended to stop the current installation, and go through the hard stop first." - echo "Otherwise, you may encounter unexpected behaviors, such as migration failures, or data loss." - echo - echo "For future reference, please visit https://develop.sentry.dev/self-hosted/releases/#hard-stops" - echo - echo "Do you wish to continue? [y/N]" - read -r confirmation - - if [[ "$confirmation" == "y" ]]; then - _write_latest_version "$new_version" - echo "${_endgroup}" - (exit 0) - else - echo "Canceled. 😅" - exit 1 - fi - done else - # If the `current_version` is empty (or the file does not exists), we assume - # this is a new installation. - echo "Self-hosted Sentry version tracking file not found. No hard stop check is needed." - _write_latest_version "$new_version" + # Only perform the hard stop check when we have a parseable semver version. + # Skip for empty or non-semver versions (e.g. "nightly") — the warnings above + # already informed the user. + + # Acquire the current version. Read the file. + declare current_version + if [[ -f "$latest_version_file" ]]; then + current_version=$(cat "$latest_version_file") + fi + + # We perform some checks if the `current_version` is not empty. + if [[ -n "$current_version" ]]; then + # We iterate over the list of hard stops, and check whether the current + # version is below any of them. + for hard_stop in "${hard_stops[@]}"; do + compare_result=$(compare_calver "$current_version" "$hard_stop") + if [[ "$compare_result" == 0 ]]; then + # equal, this is correct, they're visiting a hard stop + _write_latest_version "$new_version" + break + elif [[ "$compare_result" == 1 ]]; then + # the current version is greater than the current hard stop loop, we continue + continue + elif [[ "$compare_result" == -1 ]]; then + # the current version is less than the current hard stop loop + # we alert the user and provide a confirmation + echo "--------------------------------------------------------------------------------" + echo + echo "WARNING: Your new version ($new_version) will skip a required hard stop of $hard_stop." + echo "It is recommended to stop the current installation, and go through the hard stop first." + echo "Otherwise, you may encounter unexpected behaviors, such as migration failures, or data loss." + echo + echo "For future reference, please visit https://develop.sentry.dev/self-hosted/releases/#hard-stops" + echo + echo "Do you wish to continue? [y/N]" + read -r confirmation + + if [[ "$confirmation" == "y" ]]; then + _write_latest_version "$new_version" + break + else + echo "Canceled. 😅" + exit 1 + fi + elif [[ "$compare_result" == 2 ]]; then + # invalid version, we exit + echo "ERROR: Invalid version in $latest_version_file" + exit 1 + else + # a bug on our end, the `compare_result` returns unexpected value + echo 'ERROR: Unexpected return value from `compare_calver` function. This is a bug on our end.' + echo "The 'compare_result' value is: $compare_result" + exit 2 + fi + done + else + # If the `current_version` is empty (or the file does not exists), we assume + # this is a new installation. + echo "Self-hosted Sentry version tracking file not found. No hard stop check is needed." + _write_latest_version "$new_version" + fi fi echo "${_endgroup}" From 93771105e24ca67dd712fe664e15076035534de7 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 26 Aug 2026 08:28:26 +0700 Subject: [PATCH 4/6] fix: assign empty string to declaration --- install/check-hard-stop.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/check-hard-stop.sh b/install/check-hard-stop.sh index e432300419d..285e79c15d2 100644 --- a/install/check-hard-stop.sh +++ b/install/check-hard-stop.sh @@ -91,7 +91,7 @@ compare_calver() { # Acquire the new version. This is done by reading `.env` / `.env.custom` # for Docker image tags; or by reading the Git tag for the current commit -declare new_version +declare new_version="" # if `.env.custom` exists, prioritize it over `.env` if [[ -f ".env.custom" ]]; then new_version=$(grep -E '^SENTRY_IMAGE=' .env.custom | sed 's/^.*=//' | cut -d: -f2 || true) @@ -132,7 +132,7 @@ else # already informed the user. # Acquire the current version. Read the file. - declare current_version + declare current_version="" if [[ -f "$latest_version_file" ]]; then current_version=$(cat "$latest_version_file") fi From 66deb843ee9980c4ac4293cb8c8b78aa2f5c1624 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 26 Aug 2026 20:39:55 +0700 Subject: [PATCH 5/6] fix: address AI review comments --- .gitignore | 3 +++ install/check-hard-stop.sh | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 52ef87d58a7..6e4ca6e2c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,9 @@ geoip/GeoIP.conf geoip/*.mmdb geoip/.geoipupdate.lock +# hard stop version tracking +.sentry-hard-stop + # integration testing _integration-test/custom-ca-roots/nginx/* sentry/test-custom-ca-roots.py diff --git a/install/check-hard-stop.sh b/install/check-hard-stop.sh index 285e79c15d2..8f47cd4bb50 100644 --- a/install/check-hard-stop.sh +++ b/install/check-hard-stop.sh @@ -103,7 +103,7 @@ fi if [[ -z "$new_version" ]]; then # Check whether `git` exists as a command, and `.git` directory exists - if [[ -n "$(command -v git)" ]] && [[ -d "../.git" ]]; then + if [[ -n "$(command -v git)" ]] && [[ -d "../.git" || -d "./.git" ]]; then # Get the latest tag from the repository new_version=$(git describe --tags --abbrev=0) fi @@ -118,9 +118,14 @@ if [[ -z "$new_version" ]]; then echo "--------------------------------------------------------------------------------" fi -# If the `new_version` is nightly, we emit a different warning. -# This is for fun. -if [[ "$new_version" == "nightly" ]]; then +# If the `new_version` is empty, we cannot perform any hard stop check. +# This means the version detection failed across all methods. We already +# warned the user above, so we skip the check and continue with the installation. +if [[ -z "$new_version" ]]; then + echo "Skipping hard stop check: unable to determine the current version." +elif [[ "$new_version" == "nightly" ]]; then + # If the `new_version` is nightly, we emit a different warning. + # This is for fun. echo "--------------------------------------------------------------------------------" echo "WARNING: Hello, dear brave traveler. You are installing the nightly version." echo "The hard stop check is skipped for this version. We wish you a safe journey." @@ -141,11 +146,13 @@ else if [[ -n "$current_version" ]]; then # We iterate over the list of hard stops, and check whether the current # version is below any of them. + local _wrote_version=0 for hard_stop in "${hard_stops[@]}"; do compare_result=$(compare_calver "$current_version" "$hard_stop") if [[ "$compare_result" == 0 ]]; then # equal, this is correct, they're visiting a hard stop _write_latest_version "$new_version" + _wrote_version=1 break elif [[ "$compare_result" == 1 ]]; then # the current version is greater than the current hard stop loop, we continue @@ -166,6 +173,7 @@ else if [[ "$confirmation" == "y" ]]; then _write_latest_version "$new_version" + _wrote_version=1 break else echo "Canceled. 😅" @@ -182,6 +190,11 @@ else exit 2 fi done + # If the loop completed without writing (current_version > all hard stops), + # update the tracking file so the version stays current. + if [[ "$_wrote_version" -eq 0 ]]; then + _write_latest_version "$new_version" + fi else # If the `current_version` is empty (or the file does not exists), we assume # this is a new installation. From 2472e2c3a85baccf295b7625bd7a8861c818831a Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sun, 13 Sep 2026 22:24:37 +0700 Subject: [PATCH 6/6] feat: read hard-stops from json file --- hard-stop.json | 13 +++++++++++++ install/check-hard-stop.sh | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 hard-stop.json diff --git a/hard-stop.json b/hard-stop.json new file mode 100644 index 00000000000..d05ef7ee837 --- /dev/null +++ b/hard-stop.json @@ -0,0 +1,13 @@ +{ + "hard_stops": [ + "9.1.2", + "21.5.0", + "21.6.3", + "23.6.2", + "23.11.0", + "24.8.0", + "25.5.1", + "26.5.0", + "26.7.0" + ] +} diff --git a/install/check-hard-stop.sh b/install/check-hard-stop.sh index 8f47cd4bb50..f1879621037 100644 --- a/install/check-hard-stop.sh +++ b/install/check-hard-stop.sh @@ -20,7 +20,7 @@ echo "${_group}Checking for hard stop ... " latest_version_file=${HARD_STOP_FILE:-".sentry-hard-stop"} # This should be a bash array string, and should be equivalent with the list # on https://develop.sentry.dev/self-hosted/releases/#hard-stops -hard_stops=("9.1.2" "21.5.0" "21.6.3" "23.6.2" "23.11.0" "24.8.0" "25.5.1" "26.5.0" "26.7.0") +mapfile -t hard_stops < <(cat hard-stop.json | $jq -r '.hard_stops[]') _write_latest_version() { echo "$1" >"$latest_version_file"