Skip to main content
← Back to list
01Issue
BugIn ProgressSwamp CLIPublic
Assigneeszack

Relationships

#3389 literal() output is refused in a direct step globalArgs, so foreign ${{ }} text has no escape there

Opened by zack · 10/13/2026

What happens

A workflow step that runs a model type directly (modelType + modelName with inline globalArgs) cannot use literal() to pass ${{ ... }} text through a global argument. The run fails when the step tries to persist its definition.

task:
  type: model_method
  modelType: command/shell
  modelName: literal-shell
  methodName: execute
  globalArgs:
    run: 'echo MSG=$MSG'
    env:
      MSG: ${{ literal('${{ github.sha }}') }}

swamp workflow run fails with:

Refusing to persist definition 'literal-shell': the following global arguments hold expression text that was not written in the workflow or definition source, so it arrived as data content:
  env.MSG: ${{ github.sha }}
Write the expression into the definition itself, or pass the value as a method argument instead of a global argument.

Reproduced on release 20261013.163523.0-sha.84a186cd.

Why

literal() evaluates to a string, so its result is data by provenance. The direct-execution persist gate (src/libswamp/models/direct_execution.ts, the "Refusing to persist definition" check) refuses any global argument where expression text sits in data, and does not ask whether that text could ever be swamp's.

Why it matters

literal() is the documented way to keep ${{ ... }} text literal, and several messages point users to it:

  • GitHub Actions text that uses a swamp namespace, such as ${{ inputs.version }} or ${{ steps.build.outputs.sha }}, is evaluated as swamp's. literal() is the only way to pass it through.
  • swamp-club#2528 makes swamp workflow validate fail a root that nearly spells a swamp namespace (input.region) and tells the author to use literal() if the text is another system's.

In a step's inputs this works. In a direct step's globalArgs it does not, so there is no way to write such text there.

Expected

literal() output reaches a direct step's global argument unchanged.

Design question

The gate exists so data content can never be stored where a later run would evaluate it as an expression (swamp-club#3205). Two possible directions:

  • Let the gate accept data text that isForeignExpression judges foreign under the widest scope, since no swamp evaluation could ever own it. This covers ${{ github.sha }} but not ${{ inputs.version }}, which is the case that most needs literal().
  • Record that a value came from literal() and store it in a form a later pass will not evaluate.

Found while fixing swamp-club#2528.

02Bog Flow
✓OPEN✓TRIAGED◉IN PROGRESS○SHIPPED+ 1 MOREASSIGNED+ 8 MOREREVIEW+ 31 MOREVERIFICATION_PASSED

In Progress

10/13/2026, 9:32:31 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
zack assigned zack10/13/2026, 8:56:30 PM

Sign in to post a ripple.