Relationships
#3400 Workflow step arguments pass any unknown ${{ }} root through; limit pass-through to known foreign roots
Opened by zack · 10/13/2026· Shipped 10/13/2026
Summary
Since swamp commit 09a74070 (swamp-club#2528, PR #3075), a ${{ ... }} expression in a workflow step's task inputs or globalArgs whose root is any name swamp does not bind is treated as another templating system's text and passed to the method literally. A typo or made-up root such as ${{ nonexistent.variable }} therefore no longer fails the run with Unknown variable; the method receives the raw text.
This broke the swamp-uat test "swamp workflow run fails at runtime for invalid CEL expression" (tests/cli/e2e/cel_evaluate_test.ts:424), which asserts a non-zero exit and output matching /unknown variable/i. CI failure: https://github.com/swamp-club/swamp-uat/actions/runs/38462850841/job/115522293747 (same UAT commit passed against swamp 35be0b9d).
Decision (Zack): limit pass-through to roots that are known foreign systems, instead of "anything that is not a swamp namespace".
Steps to reproduce
swamp init, add acommand/shelldefinition namedshell-consumerwith anexecutemethod.Workflow
bad-celwith onemodel_methodstep onshell-consumer,methodName: execute, andinputs: run: "echo ${{ nonexistent.variable }}"
swamp workflow run bad-cel
Actual (at a1734560): a Template syntax passed through warning, then the step runs and fails only because sh rejects the literal text (${{ nonexistent.variable }}: bad substitution, command exit 127). Unknown variable never appears. swamp workflow validate bad-cel passes with a warning.
Expected: Error: Workflow execution failed: Invalid expression: Unknown variable: nonexistent, before any step starts, and swamp workflow validate fails. ${{ github.sha }} in the same position must still pass through (swamp-club#2528).
Proposed change
Pass a failed expression through in step arguments only when every free root swamp does not bind is on a list of known foreign roots. Proposed starting list (GitHub Actions contexts swamp does not bind, plus the Gitea/Forgejo equivalents): github, gitea, forgejo, matrix, secrets, vars, needs, runner, job, jobs, strategy.
Touch points:
src/domain/expressions/swamp_namespaces.ts— the list, a predicate over an expression's roots, andfailedOnForeignRoot(currently true for any non-swamp root).src/domain/workflows/expression_evaluators.ts— the run-start catch inWorkflowExpressionEvaluator.evaluate, andfindForeignTaskTextsoworkflow validatefails an unlisted root in step arguments.src/domain/expressions/expression_evaluation_service.ts— the step-time"passThrough"catch inevaluateDataValues.src/domain/workflows/validation_service.ts—validateForeignTaskTextneeds its own message for an unlisted root in step arguments (the existing "only passed through inside a step's inputs or globalArgs" wording is wrong there) and should point atliteral().- The near-miss heuristic (
misspeltSwampNamespace,NOT_A_MISSPELLING) becomes redundant for workflows: a near-miss root is unlisted, so run and validate both fail it. Decide whether to remove it or keep it only for the "Did you mean" hint. design/enablers/expressions.mdanddesign/primitives/workflows.md— the sections added by #3075, including the statement that step arguments "follow the same rule" as definitions and that validate has one check that fails a workflow a run accepts.
A sketch was prototyped on a1734560 (not committed). With it: bad-cel exits 1 with Unknown variable: nonexistent; ${{ github.sha }} still reaches the shell and the run succeeds; ${{ input.variable }} fails run and validate; ${{ literal('${{ nonexistent.variable }}') }} in step inputs still passes the text through. Of the six affected test files, 253 tests pass and 2 fail, both asserting the near-miss wording: one in src/domain/workflows/validation_service_test.ts ("fails a root that nearly spells a swamp namespace in step arguments") and one in integration/foreign_template_text_test.ts ("a run and its resume warn about each step argument passed through").
Costs and open points
- Text for another system whose root is not listed fails the run again, as before 09a74070. The escape is
literal(), which works in stepinputsbut is refused in a direct step'sglobalArgs(swamp-club#3389), so that issue should land with or before this one. - Workflows become stricter than model definitions:
swamp model method runon a definition holding${{ nonexistent.variable }}still passes it through (swamp-club#2491). Decide whether that asymmetry is acceptable or definitions should use the same list. - The list needs an owner and a documented way to extend it.
Environment
swamp main at a1734560 (includes 09a74070), Linux.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.