The expression evaluator was added in 2954b05, but it only handled
"complex" expressions as a fallback. Simple context references like
${{ env.FOO }} and ${{ runner.os }} were still resolved by six
separate regex preprocessors — each duplicating context resolution
logic that the evaluator already handles perfectly well.
This is the kind of redundancy that makes you maintain the same
mapping in two places and then wonder why they drift apart.
Rip out the individual regex preprocessors (preprocess_env_context,
preprocess_inputs_context, preprocess_github_context,
preprocess_runner_context, preprocess_step_outputs, and five regex
patterns). Now preprocess_expressions does exactly two things:
resolve hashFiles() (needs filesystem access), then route *all*
remaining ${{ }} through the expression evaluator. Net -46 lines.
While at it, fix three issues from code review:
- Add debug logging when expression evaluation fails in the
substitution path, instead of silently swallowing the error
- Document the surprising Bool/String coercion in expr_eq (where
false == "random" is true per GitHub Actions semantics)
- Add test coverage for that coercion edge case
The expression evaluator landed with a few landmines worth defusing
before they bite someone in production.
First, the EXPRESSION_FALLBACK regex used [^}]* to match expression
content, which breaks the *moment* someone uses format placeholders
like {0} inside an expression. The single } in {0} would terminate
the match early, producing garbage. Fix the regex to
(?:[^}]|\}[^}])* so it only stops at the actual }} delimiter.
Second, NaN was truthy. In IEEE 754, NaN != 0.0 is true, so
is_truthy() would happily report NaN as truthy. GitHub Actions
disagrees. Add !n.is_nan() to match the spec. While at it, guard
to_output_string() with n.is_finite() before the as-i64 cast to
prevent overflow on non-finite values.
Third, preprocess_fallback was dead code — evaluate_remaining_
expressions completely replaced its role in the pipeline. Remove it
and its tests rather than leaving a function that exists only to
confuse future readers.