act

Running Workflows Locally with act

Pushing a commit to learn whether a workflow works is slow. nektos/act 72,126 (https://github.com/nektos/act 72,126 ) (MIT) reads .github/workflows and runs each job in a Docker 514 container on your machine: run steps execute as written, JavaScript actions are cloned and run with the container's Node.js 2,131 , checking out your own repository becomes a copy of the working tree, and services: become sibling containers. Besides the release binary it installs with brew 6,457 install act, winget 26,463 install act, choco 15,799 install act or gh 29 extension install nektos/gh-act.

Installing act 0.2.89 and running one leg of BookNest's test jobShell
curl -sSL -o act.tgz \
  https://github.com/nektos/act/releases/download/v0.2.89/act_Linux_x86_64.tar.gz
tar xzf act.tgz act && rm act.tgz && ./act --version
./act pull_request -W .github/workflows/ci.yml -j test \
  --matrix os:ubuntu-24.04 --matrix node:24 -P ubuntu-24.04=node:24-slim --rm \
  --artifact-server-path /home/dev/v5-ch3/act-artifacts
Output
act version 0.2.89
...
[CI/test (ubuntu-24.04, node 24)]   ✅  Success - Main npm ci [2.394869626s]
...
[CI/test (ubuntu-24.04, node 24)]   | ℹ tests 16
[CI/test (ubuntu-24.04, node 24)]   | ℹ pass 16
[CI/test (ubuntu-24.04, node 24)]   | ℹ fail 0
...
[CI/test (ubuntu-24.04, node 24)]   ❌  Failure - Main actions/upload-artifact@v7
  [27.105907555s]
...
[CI/test (ubuntu-24.04, node 24)] 🏁  Job failed

act maps each runs-on label to an image; its defaults (catthehacker/ubuntu:act-*) imitate GitHub 29 's only in part, and -P substitutes slim Node.js here. --matrix picks one leg; --artifact-server-path starts a local artifact service. On this shared machine DOCKER_HOST pointed act at a private Docker-in-Docker daemon, so port 5432 could not collide with anyone else's.

All 16 tests passed against the PostgreSQL 1,289 service, yet the job failed: act 0.2.89's artifact server rejects a field that actions/upload-artifact@v7 sends. act reimplements GitHub's services, so such gaps are normal. Skip those steps locally with if: ${{ !env.ACT }} (act sets ACT=true), and pass --rm, or a failed job's containers keep running. act checks run steps and expressions fast; the real runner has the final word.