@@ -161,9 +161,30 @@ die_notrace() {
161161 for line in " $@ " ; do
162162 error " ${DIE_PREFIX}${line} "
163163 done
164+
165+ # `exit` only leaves the current shell. When die is reached inside a $(...)
166+ # command substitution or other subshell, the parent keeps running with bad
167+ # data and usually hits another die, so the same failure gets reported several
168+ # times. BASHPID (unlike $$, which stays the top-level PID even in subshells)
169+ # lets us detect that case and signal the main script so the whole run stops
170+ # immediately. Passing 0 to kill terminates the whole process group.
171+ [[ ${BASHPID:- $$ } != $$ ]] && kill -s TERM 0
164172 exit 1
165173}
166174
175+ # When die fires inside a subshell it uses `kill -s TERM 0` to bring the whole
176+ # process group down (see die_notrace). Without a handler the main shell is
177+ # terminated by that signal and reports exit code 143. Trap SIGTERM in the
178+ # top-level shell so it exits with the conventional failure code 1 instead.
179+ # Subshells reset traps to their default, so they are unaffected and still die
180+ # immediately from the group signal.
181+ #
182+ # Only arm this in the real top-level shell (BASHPID == $$), and never clobber
183+ # an existing TERM trap. The latter keeps re-sourcing common.sh idempotent and
184+ # defers to any caller that installed its own handler first. Note that `trap -p`
185+ # reports the parent's traps even from a command substitution.
186+ [[ ${BASHPID:- $$ } == $$ && -z $( trap -p TERM) ]] && trap ' exit 1' TERM
187+
167188# Simple version comparison routine
168189# Note: not a true semver comparison and build revisions are ignored
169190cmp_ver () {
0 commit comments