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
.