iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📟

Execute GitHub Actions Workflows as Batch Scripts

に公開

ghx (GitHub eXecute) allows you to run GitHub Actions workflows in your local environment (Windows, macOS, Linux).

You can use workflows with the same feeling as "just running npm run ... in any environment!"

Supported Workflows

Supported workflows follow a flow like 👇:

  • uses for reproducing the local environment
  • matrix configurations
  • Executing cross-platform build tools via run

https://github.com/sator-imaging/GitHubWorkflow/blob/main/.github/workflows/test.yml

This is for workflows that don't rely heavily on complex Bash scripts or handling credentials/secrets.

How It Works

Modern development language toolchains are essentially cross-platform. Therefore, many people have likely thought that as long as the matrix is expanded, run can be used directly as a batch script. ghx makes that possible.

By running the following in a repository that has the workflow shown at the beginning...

dotnet tool install -g ghx   # Install ghx

ghx dry test --once   # Expand only one matrix combination (dry run)

👇 It looks like this. Exactly as you'd imagine.

echo ============================================================================
echo job 'dry': matrix count=12
echo ============================================================================


echo ----------------------------------------------------------------------------
echo dry: configuration=Debug mode= once=
echo ----------------------------------------------------------------------------

dotnet run -c Debug -f net10.0 --project ./src -- \
  dry \
   \
   \
  sample


echo ============================================================================
echo job 'run': matrix count=6
echo ============================================================================


echo ----------------------------------------------------------------------------
echo run: configuration=Debug runner=ubuntu-latest
echo ----------------------------------------------------------------------------

dotnet run -c Debug -f net10.0 --project ./src -- run sample
dotnet run -c Debug -f net10.0 --project ./src -- new workflow-template

Also handles things like removing redirects to $GITHUB_STEP_SUMMARY, etc.

Windows Support

But! Usually, runners use ubuntu-latest most of the time.

Therefore, we need to bridge the minor differences between Bash and CMD.

  • End-of-line escape \^
  • Positional parameters $0-9%0-9 (which are rarely used anyway)
  • sleep Ntimeout /t N /nobreak >nul (for some reason, it stabilizes after waiting a few seconds!)

In addition:

  • Executing .bat / .cmd within a batch script causes it to exit regardless of success or failure
    • Since dnx or npx are not .exe files, executing them prevents subsequent commands from running
  • There is no way to reproduce the same behavior as bash -e (exit immediately on error) from GitHub Actions in standard batch scripts

To address this, we need to perform a conversion like CALL <original command> || EXIT 1.

👇 Result (for Windows)

@ECHO OFF


echo ============================================================================
echo job 'dry': matrix count=12
echo ============================================================================


echo ----------------------------------------------------------------------------
echo dry: configuration=Debug mode= once=
echo ----------------------------------------------------------------------------

CALL dotnet run -c Debug -f net10.0 --project ./src -- ^
       dry ^
        ^
        ^
       sample  || CALL :ERROR


echo ============================================================================
echo job 'run': matrix count=6
echo ============================================================================


echo ----------------------------------------------------------------------------
echo run: configuration=Debug runner=ubuntu-latest
echo ----------------------------------------------------------------------------

CALL dotnet run -c Debug -f net10.0 --project ./src -- run sample  || CALL :ERROR
CALL dotnet run -c Debug -f net10.0 --project ./src -- new workflow-template  || CALL :ERROR      


GOTO :EOF

:ERROR
  ECHO.
  ECHO ======= ERROR OCCURRED =======
  ECHO.
  EXIT 310

It's a bit quirky, but it works perfectly.

Creating New Workflows

Few people can remember the GitHub Composite action syntax from scratch.

ghx new <new-workflow-name>

By running this, you can create a best-practice-oriented C# workflow that uses SHA pinning.

name: workflow-template

on:
  #push:
  #  branches: [ "main" ]
  #pull_request:
  #  branches: [ "main" ]
  #workflow_call:
  workflow_dispatch:

jobs:

  workflow-template:
    #strategy:
    #  matrix:
    #    configuration: [Debug, Release]

    runs-on: ubuntu-latest   # tip: runs-on can use ${{ matrix.<name> }}

    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5      # v4.3.1
      - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9  # v4.3.1
        with:
          dotnet-version: 10.x.x

      - run: |
          echo Template created with 'ghx'

Since you basically just need to list the build tool calls in the final run, it's better to build it for GitHub compatibility from the start.

In projects involving multiple languages or those with unique requirements, simple commands provided by development environments like dotnet build, cargo build, or go build often aren't enough. It's useful to use ghx test or ghx build to create cross-platform scripts.

Composite Action Support Table

The basic style is "covering with operations if it doesn't work."

Feature Support Status Notes
workflow_call trigger ✅ Full Main use case. workflow_dispatch also works
Input definitions ✅ Full Type declarations are ignored. Default values are required when referencing
Matrix strategy ✅ Full Expanded as Cartesian product. --once flag available
Multiple jobs ✅ Full Executed sequentially. No parallelism or environment isolation
Run steps ✅ Full Only run: is extracted; uses: is ignored
Placeholder expressions ⚠️ Partial Only ${{ inputs.* }} and ${{ matrix.* }} are supported
Bash scripts ✅ Full Default shell. Becomes cross-platform through conversion
Custom shells ❌ None Specifying shell: results in an error
Runner ⚠️ Limited Only ubuntu-latest runs. Others trigger a warning
Positional parameters ⚠️ Limited Converts $0-$9 to %0-%9 when outputting to CMD
Sleep command ✅ Full Converts sleep N to TIMEOUT /T N /NOBREAK >nul on Windows

👇 Other details

https://github.com/sator-imaging/GitHubWorkflow/blob/main/README.ja.md#technical-notes

Reusable Workflows

By adding workflow_call to on, you can call the workflow from another workflow.

Therefore, by keeping workflows that only handle build tool calls small and calling them from more complex workflows (such as those involving authentication), it becomes easier to support both local and GitHub environments with ghx.

Restrictions when reusing workflows:

  • Workflows cannot be organized into subfolders, etc.
  • uses and steps cannot be used together.
jobs:
  reusable-workflow:
    uses: ./.github/workflows/workflow.yml   # Complete the job using only uses

  # Link subsequent jobs
  subsequent-job:
    needs: reusable-workflow
    if: success()
    runs-on: ubuntu-latest
    ...

In the if condition, you can use success(), failure(), always(), cancelled(), and so on.

https://t-cr.jp/article/3f71j78u2pi146e

By utilizing outputs and other features, you should also be able to receive workflow artifacts via Artifacts.

https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows#using-outputs-from-a-reusable-workflow

name: Call a reusable workflow and use its outputs

on:
  workflow_dispatch:

jobs:

  job1:
    uses: ./.github/workflows/called-workflow.yml

  job2:
    needs: job1
    if: success()
    runs-on: ubuntu-latest

    steps:
      - uses: actions/download-artifact@v4
        with:
          # Download artifacts from the previous job
          name: ${{ needs.job1.outputs.artifact_name }}

Conclusion

It helps save on private repository usage quotas, and being able to expand the matrix in a local environment is also quite convenient.

That's all. Thank you for reading.

Discussion