Skip to content

DoubleCurlyIf

if: is not wrapped in double-curly-braces.

Defined by DoubleCurlyIfRule which supports workflows, actions in the "Default" ruleset.

Description

Omitting, or over-using the double-curly-braces (${{ }}) can lead to unexpected behavior.

While the GitHub Actions Expressions documentation has a note for ${{ }} being optional for if:s. The if: being an exception also has an exception, as documented on conditionals.

The optionality listed above probably causes more problems than keystrokes it saves, and therefore it's strongly recommended to always wrap the full if: condition in ${{ }}.

If you find the examples confusing (I definitely did), the more reason to always use it. Simple rule, consistent outcomes.

Compliant examples

Compliant example #1

if:s starting with a ! must always be wrapped in double-curly-braces.

example.yml

on: push
jobs:
  example:
    if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"

Compliant example #2

To avoid confusion, if: is fully wrapped in double-curly-braces.

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"
        if: ${{ steps.calculation.outputs.result == 'world' }}

Non-compliant examples

Non-compliant example #1

When if: starts with !, it's going to break the condition. A string value starting with ! is reserved syntax in YAML.

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"
        if: ! startsWith(github.ref, 'refs/tags/')

  • Line 6: Step[#0] in Job[example] does not have double-curly-braces.

Non-compliant example #2

Simple comparison is non-commutative due to YAML syntax. The first step in this example will work correctly, but as soon as the condition order is swapped around the YAML doesn't even parse:

while parsing a block mapping
 in reader, line 3, column 5:
        if: 'bbb' == github.context.variable
        ^
expected <block end>, but found '<scalar>'
 in reader, line 3, column 15:
        if: 'bbb' == github.context.variable
                  ^
This is very confusing as commutativity is one of the basics of boolean math in programming languages. Especially for a simple thing like equality. In case both of these are wrapped in double-curly-braces, the order doesn't matter, and it "just works".

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"
        if: github.context.variable == 'example'
#     - run: echo "Example"
#       if: 'example' == github.context.variable

  • Line 6: Step[#0] in Job[example] does not have double-curly-braces.

Non-compliant example #3

The if: condition is wrapped in double-curly-braces, but only partially. Looking at the expression, it might be interpreted (by humans) as a valid string comparison, because the GitHub Actions Context variable is wrapped in an Expression as expected.

However, this condition will always evaluate to true: The way to interpret this expression is as follows:

  • Evaluate steps.calculation.outputs.result -> 'hello'
  • Substitute 'hello' -> if: hello == 'world'
  • Evaluate "hello == 'world'" -> true

This last step might be surprising, but after substituting the expressions, GitHub Actions just leaves us with a YAML String. That string is then passed to if, but it's a non-empty string, which is truthy.

To confirm this, you can run a workflow with step debugging turned on, and you'll see this:

Evaluating: (success() && format('{0} == ''world''', steps.calculation.outputs.result))

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "result=hello" >> "${GITHUB_OUTPUT}"
        id: calculation
      - run: echo "Example"
        if: ${{ steps.calculation.outputs.result }} == 'world'

  • Line 8: Step[#1] in Job[example] has nested or invalid double-curly-braces.

Non-compliant example #4

The if: condition is wrapped in double-curly-braces, but only partially. Looking at the expression, it might be interpreted (by humans) as a valid boolean expression, because the GitHub Actions Context variable accesses are wrapped in an Expression as expected.

However, this condition will always evaluate to true.

The way to interpret this expression is as follows:

  • Evaluate first net.twisterrob.ghlint.rules.DoubleCurlyIfRule$$Lambda/0x00007fb7c4161580@3d829787 -> for example true
  • Evaluate second net.twisterrob.ghlint.rules.DoubleCurlyIfRule$$Lambda/0x00007fb7c4161790@71652c98 -> for example false
  • Substitute expressions -> if: 'true && false'
  • Evaluate 'true && false' -> true

This last step might be surprising, but after substituting the expressions, GitHub Actions just leaves us with a YAML String. That string is then passed to if, but it's a non-empty string, which is truthy.

To confirm this, you can run a workflow with step debugging turned on, and you'll see this:

Evaluating: (success() && format('{0} && {1}', ..., ...))

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"
        if: ${{ github.event.pull_request.additions > 10 }} && ${{ github.event.pull_request.draft }}

  • Line 6: Step[#0] in Job[example] has nested or invalid double-curly-braces.