As developers, we've all been there: pushing commit after commit just to test a GitHub Actions workflow, waiting for runners to spin up, only to discover a simple syntax error or logic bug. What if I told you there's a better way? Here enters act - a tool that lets you run GitHub Actions locally using Docker containers.
So what exactly is act?
act is an open-source tool that reads your GitHub Actions workflow files and runs them locally using Docker containers that closely mimic GitHub's hosted runners. Think of it as a local simulator for your CI/CD pipelines. The tool was created to solve a fundamental problem: the slow feedback loop when developing GitHub Actions. Instead of the traditional "commit, push, wait, debug, repeat" cycle, act lets you iterate quickly on your local machine.
Under the hood, act performs several key operations, such as:
- Parsing your workflow files (.github/workflows/*.yml).
- Creating Docker containers that match GitHub's runner environments.
- Mounting your repository into the container.
- Executing the workflow steps just like GitHub would, but locally.
- Providing real-time feedback in your terminal.
act uses container images that replicate the Ubuntu, Windows, and macOS environments that GitHub provides, complete with pre-installed tools and software.
Installation and Basic Usage
# via Homebrew
brew install act
# via curl
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# Run all workflows
act
# Test specific events
act push
act pull_request
act workflow_dispatch
# Test specific workflow file
act -W .github/workflows/my-workflow.yml
# Dry run to see what would execute
act --dry-run
Let's try to look at a simple workflow file and see how it works. This workflow should be triggered on a pull requests and pushes. In terms of functionality, it should detect changes in a large JSON file and if there are changes, it should run a script to update the file. It also validates each modified JSON file in parallel and provide feedback on some required validations.
Most of the logic is handheld in a script or composite action, it's just for the sake of the example.
Here's a simplified version of the workflow:
name: Config Validation
on:
pull_request:
push:
branches: [master]
workflow_dispatch:
inputs:
config_path:
description: 'Specific config file to validate'
required: false
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
has-changes: ${{ steps.changes.outputs.has-changes }}
configs: ${{ steps.changes.outputs.configs }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed config files
id: changes
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.config_path }}" ]; then
CONFIG_PATH="${{ inputs.config_path }}"
echo "Manual validation requested for: $CONFIG_PATH"
echo "has-changes=true" >> $GITHUB_OUTPUT
echo "configs=[\"$CONFIG_PATH\"]" >> $GITHUB_OUTPUT
exit 0
fi
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_SHA="${{ github.event.pull_request.base.sha }}"
else
BASE_SHA="HEAD~1"
fi
CHANGED_FILES=$(git diff --name-only $BASE_SHA HEAD -- 'configs/' | grep '\.json$' || true)
if [ -z "$CHANGED_FILES" ]; then
echo "No config files changed"
echo "has-changes=false" >> $GITHUB_OUTPUT
echo "configs=[]" >> $GITHUB_OUTPUT
exit 0
fi
# Assuming 'jq' is available on the runner (it is on GitHub's default runners)
CONFIGS_JSON=$(echo "$CHANGED_FILES" | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "has-changes=true" >> $GITHUB_OUTPUT
echo "configs=$CONFIGS_JSON" >> $GITHUB_OUTPUT
validate-configs:
needs: detect-changes
if: needs.detect-changes.outputs.has-changes == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
config: ${{ fromJson(needs.detect-changes.outputs.configs) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Validate config file
run: |
echo "🔍 Validating: ${{ matrix.config }}"
npm run validate-config -- "${{ matrix.config }}"
Testing with act
# Test the entire workflow
act workflow_dispatch
# Test with specific input
act workflow_dispatch --input config_path="configs/app-settings.json"
# Test pull request simulation
act pull_request
# Debug with verbose output
act workflow_dispatch -v
This few simple commands saved me a lot of time when developing and testing the workflow.
Although it's not a perfect solution, it's a great tool to have in your arsenal when developing and testing GitHub Actions workflows. Let's look at some of its limitations that you should be aware of.
- Limited GitHub Features: No access to GitHub's REST API, can't post PR comments or update statuses, some GitHub-specific contexts might not work.
- Container Image Sizes: The full GitHub runner images are large (several GB), though act offers smaller alternatives.
- Platform Differences: On some cases, there might be differences between local Docker containers and GitHub's actual runners that can occasionally cause issues.
Some usage tips to take it further:
- Use the
--reuseflag to reuse containers between runs, this can speed up the process significantly. - Use the
--container-architectureflag to specify the architecture of the container image to use. - Use the
--verboseflag to get more detailed output. - Use the
--dry-runflag to see what would execute without actually running it. - Use the
--list-workflowsflag to list all available workflows. - Make sure to run your scripts (if you use any in your workflows) independently, so you can test them separately.
Make sure to check the act repository for more details and advanced usage.



