If you're managing GitHub actions, you might want to try nektos act. It will make your development faster and you don't need to use up GitHub CI minutes. I cringe when I have to setup integration on macos-runner. It is slow and expensive.

Act is a command line tool that runs GitHub actions locally. It uses Docker containers to simulate GitHub action runner environment. You can do most of the testing with act: simulate events, even manual dispatches with user inputs, run matrixes, pass secrets, use artifacts, run service dependent jobs.

Act also has limitations. It only supports linux runners, no macos or windows. You can run macos, but if you are on mac computer. Running matrix and artifact jobs can be problematic.

Basics

List all workflows with act -l , run a single job with act -j job_name or an entire workflow with act -W ".github/workflows/your-workflow.yml".

Test an Event

You can invoke an event like workflow_dispatch, push, pull_request:

act workflow_dispatch --input name=Hello # Triggers all workflows with workflow dispatch trigger
act push # For all workflows with push trigger
act pull_request # For all workflows with pull_request trigger

Trigger a manual action with custom input arguments

on:
  workflow_dispatch:
    inputs:
      name:
        description: 'Name?'
        required: true

jobs:
  say_hello:
    runs-on: ubuntu-latest
    steps:
      - name: Greet
        run: |
          echo "Hello, ${{ github.event.inputs.name }}!"

say-hello.yml

You can pass manual dispatch input arguments inline:

act -j say_hello --input name=Ana

Need to pass a secret?

Same as with input arguments, you can pass secrets. I prefer inline, but there's also an option to point to secrets file. Secret won't be logged, you'll see obfuscated "****" if you try to log it.

env:
  SECRET_NAME: ${{ secrets.SECRET_NAME }}

jobs:
  say_hello2secret:
    runs-on: ubuntu-latest
    steps:
      - name: Greet
        run: |
          echo "Hello, ${{env.SECRET_NAME}}!"

use-secrets.yml

Run with act -j say_hello2secret -s SECRET_NAME=Ana

If your workflow has many secrets, passing them inline wont work. You can point act to secrets file, same format that you would put a dotenv file.

act -j say_hello2secret --secret-file .env

If your worklow specifies artifacts

If my workflow is split into multiple jobs, they are run in isolation from each other. But I sometimes want to share data between them, so I use artifacts. When running workflows with artifacts, it's important to specify artifact path, otherwise workflow run will fail.

name: Test Artifacts

on:
  workflow_dispatch:

jobs:
  produce-artifact:
    runs-on: ubuntu-latest
    steps:
      - run: echo "hello world" > greeting.txt
      - uses: actions/upload-artifact@v4
        with:
          name: hello-world
          path: greeting.txt

  consume-artifact:
    runs-on: ubuntu-latest
    needs: produce-artifact
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: hello-world
          path: downloads
      - run: ls downloads

test-artifacts.yml

And run it with act -W .github/workflows/test-artifacts.yml --artifact-server-path "$PWD/.artifacts".

Run in a matrix

I sometimes use matrix when I want to run same steps for different flavors (e.g. running in node 20 and node 22, or publishing an app with different flavor). There's nothing special about running matrixes using act, except when those matrixes take up the same port.

If a matrix job takes the same port, it will fail on nektos act.

It will not fail on GitHub Actions, because each job in the matrix runs in a separate virtual environment, so they do not share the same network namespace.

However, when running locally with nektos/act, you may encounter port conflicts because all jobs share the same network namespace.

name: Matrix Port Conflict Demo
on: [push]

jobs:
  serve-web:
    name: Serve Web (instance ${{ matrix.instance }})
    runs-on: ubuntu-latest
    strategy:
      matrix:
        instance: [1, 2]
    services:
      web:
        image: nginx:stable-alpine
        ports:
          - 8080:80
        options: >-
          --health-cmd="curl --fail http://localhost:80"
          --health-interval=2s
          --health-retries=5
    steps:
      - name: Probe nginx
        run: |
          echo "Instance ${{ matrix.instance }}"
          curl --fail http://localhost:8080

matrix-port-conflict.yml

Run a job that depends on a service

When I want to run integration database tests, I want them connected to an actual database service.

name: Test Postgres Service Sidecar

on:
  workflow_dispatch:

env:
  PGUSER: postgres
  PGPASSWORD: postgres
  PGDATABASE: test

jobs:
  test-db:
    runs-on: ubuntu-latest

    services:
      db-service:
        image: postgres:latest
        env:
          POSTGRES_USER: ${{ env.PGUSER }}
          POSTGRES_PASSWORD: ${{ env.PGPASSWORD }}
          POSTGRES_DB: ${{ env.PGDATABASE }}
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - name: Launch server
        env:
          PGHOST: localhost
        run: |
          npm start &
          sleep 5
      - name: Probe /health endpoint
        run: |
          echo "🔍 Checking health…"
          curl --fail http://localhost:3000/health | jq .

postgres-health-test.yml

This is a simple example to show how a job has access to a github service, just use localhost and the port you assigned to your service. My server is a simple express server that connects to postgres and has one postgres healthcheck route.

To run that action, just call it:

act -j test-db

Run a macOS action

If you need to run a macOS action, the only way it works is to run it from macOS hardware with self hosted option:

jobs:
  macos-job:
    runs-on: macos-latest
    steps:
      - run: echo "Hello from macOS self-hosted"

macos-simple-workflow.yml

Run with act -j macos-job -P macos-latest=-self-hosted


At some point, Docker will take up a lot of storage, so I clean it up with docker system prune.

Github - https://github.com/amarjanica/nektos-act-demo