Run GitHub Actions workflows on your machine before pushing: lint them with
actionlint, then execute the real job in an Ubuntu container via act.
./scripts/test-workflow.sh pre-commit
./scripts/test-workflow.sh --list| Tool | Purpose | Install |
|---|---|---|
docker CLI + daemon |
containers for act |
brew install docker + colima start |
act |
runs workflows locally | brew install act |
actionlint |
static workflow lint (fast fail gate) | brew install actionlint |
python3 |
scratch-copy patching | preinstalled on macOS |
The script finds a working docker on its own (including the Homebrew one when
a version-manager shim shadows it) and starts colima automatically if the
daemon is down. Missing tools abort with an install hint.
Tested on macOS (Apple Silicon) with Docker via
colima. Linux with a native Docker daemon should work the same way; Windows is untested.
scripts/test-workflow.sh <name> [--event EVENT] [--job JOB] [--image IMAGE] [--amd64]
scripts/test-workflow.sh --list
scripts/test-workflow.sh --help<name>selectsscripts/workflows/<name>.sh, which pins the workflow file, trigger event, job, and container image.--event,--job,--imageoverride the profile for one-off runs without editing files.--amd64forces Intel emulation on Apple Silicon (slower; default runs native arm64, which is fine for Python/shell jobs).- Exit code mirrors the container run:
0= job succeeded.
What a run does:
- Lints the workflow file with
actionlint(fails fast, no containers needed). - Copies
.github/workflows/*.ymlto a temp dir and runsactagainst the copy, so local-only patches can never leak into the repo (there is even a safety check that aborts ifACT-LOCALmarkers appear in the real file). - Streams container output live, saves the full log to a temp file (path is printed), then prints a per-step summary.
Copy the pre-commit profile and adjust four variables:
cp scripts/workflows/pre-commit.sh scripts/workflows/my-job.shWORKFLOW_FILE=".github/workflows/my-job.yml" # required
WORKFLOW_EVENT="pull_request" # required: any trigger in the file
WORKFLOW_JOB="build" # optional: empty runs the whole event
ACT_IMAGE="catthehacker/ubuntu:act-24.04" # optional container image
# ACT_EXTRA_ARGS="--matrix os:ubuntu-latest" # optional raw `act` flagsThen ./scripts/test-workflow.sh my-job. No runner changes needed.
act's fake checkout copies files without the .git directory, and the
container runs as root (git "dubious ownership" refusal). Jobs that shell out
to git need the same shim the pre-commit profile uses:
WORKFLOW_NEEDS_GIT_SHIM="yes"
WORKFLOW_SHIM_SNIPPET="$SCRIPTS_DIR/workflows/pre-commit-shim.yml"The snippet (a plain YAML step fragment) is injected as the first step of the
tested job in the temp copy only, via scripts/lib/inject_step.py, and the
temp dir is deleted afterwards. Write your own snippet file if your job needs
different setup; keep the [act-local] prefix so such steps are recognizable.
- The final
git pushcannot succeed locally (dummy token, no remote). Validate everything up to it here; the push itself is covered byworkflow_dispatchruns in CI. actions/cacheworks underact(local cache backend), so repeat runs are fast.- The
@v4/@v5action pins are resolved from the network on first use and then cached under~/.cache/act.
This scripts/ folder is template-developer tooling: it is listed in
copier.yaml _exclude and covered by my_tests/test_exclusions.py, so it
never lands in generated projects.