Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneeszack

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

  1. swamp init, add a command/shell definition named shell-consumer with an execute method.

  2. Workflow bad-cel with one model_method step on shell-consumer, methodName: execute, and

    inputs:
      run: "echo ${{ nonexistent.variable }}"
  3. 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, and failedOnForeignRoot (currently true for any non-swamp root).
  • src/domain/workflows/expression_evaluators.ts — the run-start catch in WorkflowExpressionEvaluator.evaluate, and findForeignTaskText so workflow validate fails an unlisted root in step arguments.
  • src/domain/expressions/expression_evaluation_service.ts — the step-time "passThrough" catch in evaluateDataValues.
  • src/domain/workflows/validation_service.ts — validateForeignTaskText needs 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 at literal().
  • 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.md and design/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 step inputs but is refused in a direct step's globalArgs (swamp-club#3389), so that issue should land with or before this one.
  • Workflows become stricter than model definitions: swamp model method run on 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.

02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 9 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/13/2026, 10:24:23 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
zack assigned zack10/13/2026, 9:15:39 PM

Sign in to post a ripple.