Skip to main content
7 ton shark

Faking a dynamic GitHub Actions "shell" property

Ran into an issue recently where I had to run a script in GitHub Actions in one of PowerShell or PowerShell Core, depending on the runner I was on (for legacy reasons, long story).

My first attempt, using a fake ternary operator expression:

  - name: Clean project folder
    shell: ${{ env.USE_POWERSHELL_CORE == 'true' && 'pwsh' || 'powershell' }}
    working-directory: ${{ matrix.project-folder }}
    run: git clean -f -d -x

However, I immediately got an invalid workflow error.

Invalid workflow file: [...]: Unrecognized named-value: 'env'. Located at position 1 within expression: env.USE_POWERSHELL_CORE == 'true' && 'pwsh' || 'powershell'

Long story short, for whatever reason, the shell property actually doesn't support any kind of expressions at all, as far as I can tell -- at least none that depend on env or matrix.

Double the steps #

In my case, I was able to hack around the problem by defining two steps, and then running one and skipping the other, depending on the value of the environment variable.

  - name: Clean project folder (pwsh)
    if: ${{ env.USE_POWERSHELL_CORE == 'true' }}
    shell: pwsh
    working-directory: ${{ matrix.project-folder }}
    run: git clean -f -d -x
  - name: Clean project folder (powershell)
    if: ${{ env.USE_POWERSHELL_CORE == 'false' }}
    shell: powershell
    working-directory: ${{ matrix.project-folder }}
    run: git clean -f -d -x

This trick also works if the steps produce outputs, but you'll need to make sure that consumers of the output reflect the fact you don't know which one runs. Something like this should work:

  - name: Consuming step
    run: |
      echo "Result: ${{ steps.thing-pwsh.outputs.result || steps.thing-powershell.outputs.result }}"

Wrap in an action #

If you need to do several of these blocks, consider wrapping the whole mess in a local reusable action instead. The advantage here is that although you are not allowed to refer to env or matrix contexts in the shell property, you can refer to an action's inputs property.

So, if you define a reusable composite action:

name: My reusable action

inputs:
  shell:
    required: true

steps:
  runs:
  using: 'composite'
  steps:
    - name: Clean project folder
      shell: ${{ inputs.shell }}
      working-directory: ${{ matrix.project-folder }}
      run: git clean -f -d -x

Now your workflow can call ./.github/actions/my-reusable-action.yaml and pass in the desired shell as an input, and you don't need to do the double-step nonsense. (The downside, as usual, is that all of the steps defined inside your reusable action stop being called out as individual steps in your workflow; your mileage may vary.)