Skip to content

How to run CI checks locally

Running CI checks locally means repeating a CI pipeline, e.g. a GitHub Actions workflow, on a developer's machine or in a container before a push.

Last updated , 8 min read

Key points

  • Put the pipeline's commands in one script, and call that script from the workflow and the terminal.
  • Run the script from a clean copy of the code, in a container with the CI runner's tool versions.
  • A local run only approximates CI, so the CI run on the pull request still gives the final result.

How do you run CI checks locally?

To run CI checks locally, repeat the commands that the continuous integration (CI) pipeline runs, with the same tool versions, from a clean copy of the repository. Run them in a container to match the CI machine more closely. For GitHub Actions, the open-source act tool runs the workflow file itself in Docker, e.g. with act pull_request.

A local run reports a failure before the change leaves the laptop. It does not replace the CI pipeline, which usually runs on a fresh machine and can be required before a merge. The pre-commit, pre-push, and CI comparison shows which checks fit where.

What do you need before you start?

A local CI run needs these things:

  • The workflow file. A workflow in .github/workflows/ names the runner, the tool versions, and the commands, as in a setup for tests in GitHub Actions.
  • The same tool versions. The laptop or the container uses the runtime version that the workflow installs, e.g. Node.js 24.
  • Docker and act. Docker runs containers, and the act command-line tool runs workflow jobs in them. A package manager installs act, e.g. brew install act.
  • Test values for secrets. Each secret the workflow reads needs a local test value, which act takes with -s NAME or from a file with --secret-file.

How do you run CI checks locally step by step?

Here is an illustrative example. Acme Co. sells furniture online. A developer at Acme asks a coding agent to "Let customers edit their delivery address during checkout." The agent's output says "done," and the developer runs the CI checks locally before a push.

1. Read the workflow file

Acme's .github/workflows/ci.yml runs one job on a Linux runner for pull requests and pushes to main:

on:
  pull_request:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: npm ci
      - run: npm run lint
      - run: npm test

2. Put the commands in one script

The developer moves the checks into one npm script that the workflow and the terminal both run:

{
  "scripts": {
    "check": "npm run lint && npm test"
  }
}

The last two workflow steps become run: npm run check. Each && starts the next command only when the one before it returns exit code 0. A nonzero code stops the chain and fails the CI step.

3. Run the script from a clean copy

The developer commits locally and checks the commit out into a second folder. A Git worktree checks out only committed files:

git worktree add --detach ../acme-check
cd ../acme-check
npm ci && npm run check

Here a unit test fails, though it passed in the main folder:

FAIL test/address.test.js
  ● formats a delivery address

    ENOENT: no such file or directory, open 'test/fixtures/address.json'

The agent created test/fixtures/address.json but never staged it. The developer commits it, removes the old copy with git worktree remove ../acme-check, and repeats the step from a fresh copy, which then passes.

4. Run the same commands in a container

The runner is Linux and the laptop is a Mac, so the developer runs the script from the clean copy in the official Node.js image:

docker run --rm -v "$PWD":/app -w /app node:24 \
  sh -c "npm ci && npm run check"

Linux shows differences that macOS hides, e.g. an import whose file name differs in case. The workflow can run its job in the same image with the container key, and a dev container can name that image for the editor.

5. Run the workflow file with act

To check the workflow itself, the developer lists its jobs and runs the test job with act:

act -l pull_request
act pull_request -j test --container-architecture linux/amd64

The act tool runs the job's steps in a container with GitHub's default environment variables, set to local values. The --container-architecture flag matches the x64 runner on an Apple silicon Mac. By default, act copies the current folder in place of actions/checkout, uncommitted files included, so the developer runs it from the clean copy too. When the workflow file changes, the actionlint static checker reports mistakes first, e.g. an unknown key.

6. Compare the result with CI

The developer pushes the branch and opens a pull request, which runs the workflow on the branch merged into its base. CI passes, as the local runs did. Then the developer starts the store and tries checkout by hand. The address saved, but the cart emptied. No step starts the store or checks the cart, so neither run caught the bug.

This example is simplified. A real project would add a step that starts the store and tries checkout.

What can a local run not reproduce?

A local run copies the commands, not the CI system. These parts differ:

  • The runner. GitHub's documentation describes most GitHub-hosted runners as virtual machines with many tools preinstalled. The default act images are containers that leave out many of those tools.
  • Ignored keys. The act documentation lists keys that act ignores, including concurrency, timeout-minutes, and job permissions.
  • Secrets and identity. A local run has only the secrets passed to it and no OpenID Connect URL for cloud logins. A personal GITHUB_TOKEN carries its owner's access, not the job's permissions.
  • The merge commit. A pull_request run tests the branch merged into its base, which can fail when the branch alone passes.
  • Other systems. The act tool runs Linux jobs in containers. It runs a macOS or Windows job only on a host with that system, with no container or fresh machine around it.
  • Timing and load. A test that depends on timing can pass on a fast laptop and fail in CI under load.

Each gap lowers environment parity, the degree to which two environments match. A local pass makes a CI pass likely, not certain.

What changes when a coding agent writes the code?

A coding agent can run the local check in its own session, before its output says "done." An agent that runs the documented check script gets the whole list, not only the unit tests. It runs that check in its working folder, so a file it never staged still joins the run.

A cloud agent works in a remote sandbox that can lack a container engine, so the container step and act cannot start there. An agent can also make a check pass by editing it, e.g. by deleting a step from the check script.

Review each change to the check script and to .github/workflows/ as closely as code. A pre-commit hook can run the fast part at each commit, but the CI run on the pull request stays the result to trust.

What are common mistakes?

These mistakes make a local pass say less:

  • Running from the working folder. Untracked files and uncommitted edits join the run, so checks pass on files that CI never receives.
  • Keeping two lists of commands. A workflow and a local script that each list the steps drift apart.
  • Mounting the main folder into a container. The npm ci command deletes an existing node_modules folder first, so the container replaces the laptop's packages with Linux builds.
  • Keeping real secrets in a tracked file. A .secrets file for act belongs in .gitignore, and secret scanning can catch a key that reaches the history.

How do you check that it worked?

The local setup works when these statements are true:

  • A change with a known fault, e.g. a lint error, fails the local run with a nonzero exit code.
  • The same commit gives the same result in the clean copy, the container, and CI.
  • Each difference from CI has a named cause, not a rerun that turned green.

These checks show that the local run matches CI, not that the app works.

How do you check the running software, not only the pipeline?

A local run and a CI run try the same commands, so a feature that no step checks stays untested in both. Start the built app, e.g. in an ephemeral environment, and try the feature that the change touched.

RunStory runs your software against the change in a separate environment, tries relevant workflows, and checks the results. It sends reproducible failures back to your coding agent. It is in private alpha for CLIs and web apps, and your team keeps the final release decision.

Join the RunStory alpha →

FAQs

Can GitHub Actions workflows run locally?

GitHub Actions workflows can run locally with an open-source tool, e.g. act, which runs each job in a Docker container. The containers only approximate GitHub's runners, so the CI run still gives the final result.

How do you test CI pipelines?

Testing a CI pipeline means checking the workflow file as well as the code it runs. A static checker, e.g. actionlint, reports mistakes in the file, and act runs its jobs locally. A pull request then runs the changed file on the real runners before the merge.

Can a local run use the same container image as CI?

A local run can use the same container image as CI when the CI job itself runs in a container, set with the job's container key. Most GitHub-hosted runners are virtual machines, so a local container without that key only approximates the runner.

How do you give a local run the secrets a workflow needs?

A local run gets secrets from the command line or from a secrets file that act reads. Use test tokens with narrow access and keep the file out of Git, because a personal token acts as its owner, not with the job's narrower permissions.