fix: Skip exit-code check after else/negated lines

What does this MR do and why?

With FF_ENABLE_BASH_EXIT_CODE_CHECK on, the concrete bash generator appends a post-command exit-code check after every user script line:

_runner_exit_code=$?; if [ $_runner_exit_code -ne 0 ]; then exit $_runner_exit_code; fi

That check lands in several places where $? is not the result of a completed user command:

  • Right after a bare else, it becomes the first statement of the else branch, where $? still holds the failed if-condition's status — so it exits, and the else body never runs. else # comment is the same shape.
  • Right after a negated ! cmd, $? is non-zero when cmd succeeds — so a command the user deliberately negated aborts the job.
  • Right after fi or done, $? is the status of the last body command, which may itself be such a negation.
  • Right after an until cmd; do header, $? is the status of the failed condition — so a retry loop exits before its body runs.

set -o errexit does not abort in any of these cases, so the manual check must not either. This is the same shape as the pre-command restore removed for #39610 (closed), but a different mechanism (the post-command check) and gated on the flag.

Related to #39634 (closed).

The issue lists three generators with this defect. This MR fixes the concrete one only, so the issue stays open for the legacy paths (see Scope below).

How

Root cause: buildBashScript emits the check unconditionally per line, with no awareness of shell block structure.

Fix: gate the per-line check with exitCodeCheckApplies(line). It splits the line with strings.Fields and decides on the first word, returning false for !, fi and done, for until ... do, and for else unless a real command follows it — so bare else, else # comment and a tab after the bang are all covered.

For a multi-line script: item (a YAML - | block) the whole item arrives as one string and the check is appended after its last line, so that last line decides, not the first word of the item.

Other block keywords (then, while ...; do, elif) are intentionally left checked: their body runs only when the preceding condition succeeded, so $? is 0 there. Failures that errexit itself ignores must still abort — false && true is the case the tests pin — as must a plain failing command (false) or an explicit exit N.

Scope: this fixes the concrete (FF_CONCRETE) generator, where the regression tests for #39634 (closed) already live. The legacy paths (functions/script_legacy command_processor.go and shells/bash.go driven by shells/abstract.go) share the same shape and are left for a follow-up, which is why this MR does not close the issue.

Known gap: the check still aborts on && / || lists such as false && echo X, where errexit continues. That is the same class of defect and is better handled as a focused follow-up than by widening this MR.

Test coverage

All cases run a real bash through TestBashScript_Execute.

Two execution tests for this gap already existed, skipped with "un-skip when #39634 (closed) is fixed" — else branch runs to completion with exit code check and negated command does not abort the script with exit code check. Both are un-skipped and pass.

Added for the per-line shapes: else with a trailing comment, a negation with a tab after the bang, an if body ending in ! true, a loop body ending in ! true, a nested else that guards the exact-match logic, and an until retry loop.

Added for multi-line items, which are the only shape that reaches a closing keyword with $? still set by the body: an item whose last line is false && true (must still abort), and items ending in a negation, in fi, and in done. One multi-line case also runs with ScriptSections enabled, since that path appends the same per-line check.

The guard cases stay green: failing command with exit code check still aborts the script (exit 1) and explicit exit preserves its status (exit 7), confirming failure propagation is unchanged.

Edited by Jeston Singh

Merge request reports

Loading
Loading