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
^
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 exampletrue
- Evaluate second
net.twisterrob.ghlint.rules.DoubleCurlyIfRule$$Lambda/0x00007fb7c4161790@71652c98
-> for examplefalse
- 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.