# Run CI locally before you push

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`](#act) | Your GitHub Actions workflows | Docker | Yes |
| [`gitlab-ci-local`](#gitlab) | Your `.gitlab-ci.yml` jobs | Docker or a shell | Yes |
| [A `pre-push` hook](#hook) | The commands you choose | Git | Yes |
| [`hushrun run`](#count) | The commands you choose | hushrun | No, for a pass on the same files |

## Replay GitHub Actions with act

[`act`](https://github.com/nektos/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:

```bash
brew install act
act -l                 # list the jobs
act pull_request       # run what a pull request would start
act -j test            # run one job
```

It 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](https://nektosact.com/usage/runners.html).** Larger images come closer.
- **macOS and Windows jobs have no container.** On a matching machine, `act -P macos-latest=-self-hosted` runs 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_TOKEN` isn'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`](https://github.com/firecow/gitlab-ci-local) does the same for `.gitlab-ci.yml`, with Docker or in your shell:

```bash
npm install -g gitlab-ci-local
gitlab-ci-local --list     # list the jobs
gitlab-ci-local test       # run one job
```

GitLab's own `gitlab-runner exec` was [deprecated](https://docs.gitlab.com/update/deprecations/#the-gitlab-runner-exec-command-is-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:

```bash title=".git/hooks/pre-push"
#!/bin/sh
npm test
```

```bash
chmod +x .git/hooks/pre-push
```

- **Git doesn't clone hooks.** To share one, commit it to a directory and run `git config core.hooksPath .githooks` in 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-verify` skips 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](https://hushrun.com/docs/guides/claude-code-run-tests-before-push.md) 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`](https://hushrun.com/docs/run.md) records a pass for the exact files, check name, command and directory. Wrap a script once:

```json title="package.json"
"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:

```console
$ 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](https://hushrun.com/docs/ci.md#ci-only) 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](https://hushrun.com/docs/quickstart.md).

## Which one to use

- **You're changing a workflow file:** `act` or `gitlab-ci-local`.
- **You want failures before the push:** a `pre-push` hook.
- **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.

## Related

- [Skip CI when the code hasn't changed](https://hushrun.com/docs/guides/skip-ci.md)
- [Why CI runs the same tests twice](https://hushrun.com/docs/guides/duplicate-ci-runs.md)
- [Make Claude Code run tests before it pushes](https://hushrun.com/docs/guides/claude-code-run-tests-before-push.md)
