fix(workflows): strip a resolved condition before the true/false check - #3883
Open
jawwad-ali wants to merge 1 commit into
Open
fix(workflows): strip a resolved condition before the true/false check#3883jawwad-ali wants to merge 1 commit into
jawwad-ali wants to merge 1 commit into
Conversation
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
evaluate_condition()special-cases the strings"false"/"true"so that a condition resolving to text behaves as a boolean — its own comment says so:It matches with
result.lower()and never strips. But 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):So
run: echo falseresolves to"false\n"— which matches neither branch and falls through tobool("false\n")→True.Reproduction on current
main(81bf741)An
ifstep takes itsthenbranch on a step that printedfalse, andwhile/do-whilekeep dispatching their body —engine.pyuses this same function as the loop-continuation predicate.There is no workaround
_REGISTERED_FILTERSis("default", "join", "map", "contains", "from_json")— there is notrimfilter, anddocs/reference/workflows.md:510lists the same five. A workflow author has no way to strip stdout before a condition, so this cannot be worked around in YAML.Fix
One line —
result.strip().lower().Existing precedent for stripping before matching boolean text:
steps/init/__init__.py:241resolved.strip().lower() in ("true", "1", "yes")workflows/catalog.py:410raw_install.strip().lower() in (...)workflows/catalog.py:1099To be precise about the precedent: those return
Falsefor anything unmatched, whereasevaluate_conditionfalls through tobool(result). They justify the strip, not the surrounding semantics.Breaking risk
Only strings whose stripped form is exactly
false/truebut whose raw form is not. Whitespace-paddedfalse/FALSEnow evaluate toFalseinstead ofTrue— that is the bug being fixed. Paddedtruewas alreadyTrueviabool()and staysTrue.bool(result)below still sees the raw string, so nothing else changes: a whitespace-only string is still truthy, and"falsey"is still truthy. Both are pinned by a new test.Verification
test_condition_strips_captured_command_outputfail (True = evaluate_condition('{{ steps.check.output.stdout }}', ...)); restoring it gives 2 passed.tests/test_workflows.py→ 839 passed, 20 failed; the failure set is byte-identical to the clean-mainbaseline I captured on81bf741(20 in this file, all Windows symlink-privilege).uvx ruff@0.15.0 check src tests→ cleanTests go into the existing
TestExpressions, besidetest_condition_evaluation.Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current
main.