fix(workflows): strip a resolved condition before the true/false check (#3883)

evaluate_condition() special-cases the strings "false"/"true" so that
`condition: "false"` behaves as a boolean, but it matches with
`result.lower()` and never strips.

The most common way a *string* reaches a condition is captured command
output, and the shell step stores stdout verbatim
(steps/shell/__init__.py:67 `"stdout": proc.stdout`). So `run: echo false`
resolves to "false\n", which matches neither branch and falls through to
`bool("false\n")` -> True:

  'false'     -> False
  'false\n'   -> True    <-- bug
  'false\r\n' -> True    <-- bug
  ' false'    -> True    <-- bug

An `if` step therefore takes its `then` branch on a step that printed
"false", and `while`/`do-while` keep dispatching their body.

A workflow author cannot work around it: the registered filters are
default/join/map/contains/from_json — there is no `trim`.

`InitStep._resolve_bool` and both catalog readers already strip before
matching boolean text. `bool(result)` still sees the raw string, so no
non-boolean text changes truthiness.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-08-10 13:20:47 -05:00
committed by GitHub
co-authored by Claude Opus 5
parent 3451a21277
commit 1b3695bfb3
2 changed files with 46 additions and 1 deletions
+13 -1
View File
@@ -671,8 +671,20 @@ def evaluate_condition(condition: str, context: Any) -> bool:
result = evaluate_expression(condition, context)
# Treat plain "false"/"true" strings as booleans so that
# condition: "false" (without {{ }}) behaves as expected.
#
# Strip before matching: the string a condition resolves to is most often
# captured command output, and a ``shell`` step stores ``proc.stdout``
# verbatim, so ``run: echo false`` resolves to ``"false\n"``. Without the
# strip that trailing newline matches neither branch and falls through to
# ``bool("false\n")`` -> True, silently taking an ``if`` step's ``then``
# branch (and keeping a ``while``/``do-while`` looping) on a step that
# printed "false". A workflow cannot strip it itself -- the registered
# filters are default/join/map/contains/from_json, there is no ``trim``.
# ``InitStep._resolve_bool`` and the catalog readers already strip before
# matching boolean text. ``bool(result)`` below still sees the raw string,
# so no non-boolean text changes truthiness.
if isinstance(result, str):
lower = result.lower()
lower = result.strip().lower()
if lower == "false":
return False
if lower == "true":