Setting default shell (or other step metadata) conditionally in workflows

I just posted this to the setup-msys2 issue tracker, but I thought I’d bounce it off the folks here as well:

So, I’m kind of banging my head against a wall with something, and I wanted to see if anyone has any clever solutions.

I have a project which builds in CMake, and the cmake commands are actually all the same whether running on Linux, macOS, or Windows. I’ve worked very hard to keep things that way, in fact, so that the build tooling could be as platform-agnostic as possible.

So, I have a workflow written with build steps that are largely shared between all platforms in the matrix. (Each OS has its own dependency step, gated with an if: ${{ runner.os == 'Linux' }} (or 'Windows' or 'macos'), but that’s the only non-shared config.)

To differentiate the -G argument to cmake, which selects the generator to use for the build system, I’m using action-cond from @haya14busa which works great:

     - name: Select CMake generator
        uses: haya14busa/action-cond@v1
        id: generator
        with:
          cond: ${{ runner.os == 'Windows' }}
          if_true: 'MinGW Makefiles'
          if_false: 'Unix Makefiles'
      - name: Build
        run: |
          cmake -B build -S . -G "${{ steps.generator.outputs.value }}" ...

The problem is, I can’t think of any way to do the same thing for the MSYS2 shell. If I wanted to take advantage of the “Default shell” option from the README, I’d end up setting it as the default for every shell, including the ones that run on Linux and macOS, which obviously won’t work! I’ve tried 100 different ways to conditionalize the setting, and they all failed:

  1. Add an if: to the defaults: section of the config: Syntax error
  2. Add another haya14busa/action-cond@v1 step that sets either msys2 {0} or bash, and use shell: ${{ steps.select_shell.outputs.value }} in subsequent steps: Syntax error, apparently the steps object is not accessible from the metaconfiguration of subsequent steps.
  3. Capture the output into an environment variable, the same way I can with CC: ${{ matrix.compiler }}:
    env:
      RUNSHELL: ${{ steps.select_shell.outputs.value }}
    steps:
       - name: Select shell...
         id: select_shell
         with:
          cond: ${{ runner.os == 'Windows' }}
          if_true: 'msys2 -c'
          if_false: 'bash -c'
         ...
      - name: Build
        run: |
          $RUNSHELL 'cmake...'
    
    (In my defense, I never expected that one to work, and it doesn’t. Same issue with accessing steps before it’s defined.)
  4. The same thing, but set the env in every step that needs it:
    steps:
       - name: Select shell...
         id: select_shell
         with:
          cond: ${{ runner.os == 'Windows' }}
          if_true: 'msys2 -c'
          if_false: 'bash -c'
         ...
      - name: Build
        env:
          RUNSHELL: ${{ steps.select_shell.outputs.value }}
        run: |
          $RUNSHELL 'cmake...'
    
    That actually came close to working, believe it or not! Actually worked on Linux and macOS. Blew up in the default Powershell on Windows, though.
  5. Set up a completely separate job config before the build job, with the same matrix, and have it relay the results of action-cond into an outputs. Have the second job needs: the first, and set shell: ${{ needs.job1.outputs.shell }} on each step: Syntax error, it seems the needs context isn’t any more accessible for defining steps than the steps context is.

At this point I feel like I’ve exhausted all possibilities. Any suggestions, or do I really have to take my nice, cross-platform workflows and duplicate all of the run steps, just so I can get Windows to run them using the correct shell? :disappointed:

Over at my setup-msys2 issue, we did some brainstorming and finally came up with this, which works unexpectedly well.

It uses what appears to be undocumented syntax for the matrix context in the Actions yaml, and its contents will get flagged as containing syntax errors by Github’s web workflow editor. Nevertheless, it works exactly as you’d hope it would. All credit to @eine for coming up with the necessary matrix config wizardry.

jobs:
  build:
    runs-on: ${{ matrix.sys.os }}
    strategy:
      matrix:
        sys:
          - { os: windows-latest, shell: 'msys2 {0}' }
          - { os: ubuntu-latest,  shell: bash  }
          - { os: macos-latest,   shell: bash  }
        compiler:
          - { cc: gcc,   cxx: g++ }
          - { cc: clang, cxx: clang++ }
    defaults:
      run:
        shell: ${{ matrix.sys.shell }}
    env:
      CC: ${{ matrix.compiler.cc }}
      CXX: ${{ matrix.compiler.cxx }}