Back to Blog

How to Debug GitHub Actions Locally Without Pushing Constant 'Fix' Commits

How to Debug GitHub Actions Locally Without Pushing Constant 'Fix' Commits

How many times have you pushed a commit with the message "fix ci" only to follow it up three minutes later with "fix ci again"?

We’ve all been there. You spend twenty minutes crafting a beautiful YAML workflow, push it to GitHub, and then watch the little yellow spinning circle turn into a red "X" because you forgot a single pipe character or a required environment variable. It’s an embarrassing cycle that litters your git history with junk.

The good news is that you don't have to use GitHub's servers as your testing playground. You can run the entire dance locally.

The Tool: act

The magic happens via a tool called act.

The concept is simple: act reads your .github/workflows directory, parses the YAML, and spins up Docker containers that mimic the GitHub runner environment. It’s not a perfect 1:1 replica, but it’s close enough to catch 95% of the syntax errors and logic flaws that usually break your builds.

Getting Started

If you’re on macOS and use Homebrew, installation is a one-liner:

brew install act

Once installed, navigate to the root of your project and run:

act

The first time you run it, act will ask you which Docker image size you want to use. Don't just pick "Small" because you're in a hurry. The "Small" image is barely a shell; it lacks basic tools like curl, git, or even node. If your workflow does anything meaningful, you’ll want at least the "Medium" image. It’s a bit of a hefty download (several GBs), but it saves you the headache of manually installing dependencies inside your action steps.

Running Specific Jobs

If you have a massive workflow with twenty different jobs, running act by itself will trigger everything associated with a push event. That’s usually overkill.

To target a specific job by its ID:

act -j build

Or, if you want to see what would happen during a pull_request event instead of a push:

act pull_request

The "Secrets" Problem

GitHub Actions often rely on secrets (API keys, AWS tokens, etc.) stored in the repository settings. Locally, act has no idea these exist. If your workflow tries to pull ${{ secrets.NPM_TOKEN }}, it’s going to come back empty and your build will probably explode.

You have two ways to handle this. You can pass them directly via the CLI (annoying):

act -s NPM_TOKEN=your_real_token

Or, the better way: create a .secrets file. Make sure this file is in your `.gitignore`.

# .secrets file
NPM_TOKEN=your_super_secret_token
STRIPE_KEY=sk_test_12345

Then run act pointing to that file:

act --secret-file .secrets

Dealing with the "It Works on My Machine" Gotchas

act is fantastic, but it isn't a silver bullet. Here are the things that will inevitably trip you up:

1. The Docker-in-Docker Headache: If your workflow itself calls Docker (like building a container and pushing it to ECR), you’re running Docker inside a Docker container managed by act. This gets meta and occasionally messy.
2. Environment Mismatches: GitHub’s official runners are highly customized Ubuntu/Windows/macOS environments. act uses generic Debian-based Docker images. If your script relies on a specific pre-installed library that only exists on GitHub's proprietary runner, act will fail while the cloud succeeds.
3. Filesystem Paths: GitHub Actions sets the $GITHUB_WORKSPACE to a specific path. act tries to map your local directory to that path, but sometimes permissions or symlinks get weird, especially on Windows.

Is it worth it?

I used to think setting up local CI testing was a waste of time—that I could just "guess" the YAML syntax correctly. I was wrong.

The time it takes to download a 5GB Docker image for act is significantly less than the cumulative time spent waiting for GitHub’s queue, watching a job spin for three minutes, and then realizing I forgot to add uses: actions/checkout@v4.

If you value a clean git history and your own sanity, stop pushing to test. Run it locally, break it locally, and only push when you’re actually ready.