Run CI locally before you push
Updated
You can run CI on your machine in two ways: replay the workflow with a tool such as act, or run the same commands before you push. Both find failures sooner. Neither saves CI any work, unless CI can see that your run passed.
| Tool | Runs | Needs | Does CI run it again? |
|---|---|---|---|
act | Your GitHub Actions workflows | Docker | Yes |
gitlab- | Your . jobs | Docker or a shell | Yes |
A pre- hook | The commands you choose | Git | Yes |
hushrun run | The commands you choose | hushrun | No, for a pass on the same files |
Replay GitHub Actions with act
act reads .github/workflows/ and runs each job in a container, which helps most when you're changing a workflow and don't want to push to try it:
brew install act
act -l # list the jobs
act pull_request # run what a pull request would start
act -j test # run one jobIt isn't the same as running on GitHub:
- It needs Docker, and it's a container where GitHub uses a virtual machine, so some software behaves differently.
- Its default images lack some of GitHub's tools. Larger images come closer.
- macOS and Windows jobs have no container. On a matching machine,
act -P macos-latest=-self-hostedruns them on the host. - Events are partial. A workflow that reads the pull request's details may need an event file, passed with
-e. GITHUB_TOKENisn't provided. Pass it yourself:act -s GITHUB_TOKEN="$(gh auth token)".- Artifacts need a path, set with
--artifact-server-path.
Replay GitLab CI
gitlab-ci-local does the same for .gitlab-ci.yml, with Docker or in your shell:
npm install -g gitlab-ci-local
gitlab-ci-local --list # list the jobs
gitlab-ci-local test # run one jobGitLab's own gitlab-runner exec was deprecated and removed in GitLab Runner 17.0, so older guides that use it are out of date.
Run checks in a pre-push hook
A pre-push hook runs before git push and stops it when it exits with an error:
#!/bin/sh
npm testchmod +x .git/hooks/pre-push- Git doesn't clone hooks. To share one, commit it to a directory and run
git config core.hooksPath .githooksin each clone, or use a tool such as Husky. - A long suite holds up the push, so keep the hook to fast checks.
git push --no-verifyskips it. Treat a hook as a convenience, and keep the checks that matter in CI.
Coding agents need this more than people do, since they push often. Make Claude Code run tests before it pushes covers that case.
Make the local run count
Everything above repeats work. Your laptop runs the tests, then CI runs them again on the same files. To run them once, CI has to see the pass.
hushrun run records a pass for the exact files, check name, command and directory. Wrap a script once:
"scripts": {
"test": "hushrun run -- vitest run"
}The first run on this code runs the check, and any machine that shares results skips it afterwards, CI included:
$ hushrun run e2e -- playwright test --project site hushrun: e2e running: no pass yet for this exact code … hushrun: e2e passed in 16s; it won't run again on this code $ hushrun run e2e -- playwright test --project site hushrun: e2e skipped: this exact code passed 1 min ago (saves 16s)
hushrun ready lists what CI would still run on your commit, and the commands that run it here. Whether CI should trust a laptop's result is your call: turn on CI reuses only CI's results and it won't.
hushrun runs your commands, not your workflow, so it doesn't test the YAML. npx hushrun init sets it up in about three minutes: see the Quickstart.
Which one to use
- You're changing a workflow file:
actorgitlab-ci-local. - You want failures before the push: a
pre-pushhook. - You don't want CI to repeat what passed:
hushrun run.
They combine: a hook that runs wrapped checks costs nothing on code that already passed.