What this usually means
CI runs your tests in a clean environment. No stale `node_modules`. No cached build artefacts. No lingering processes. No shared state from a previous run. If your tests depend on anything that does not survive a clean checkout and install, they will fail in CI. The most common culprits are: missing env vars, test file ordering assumptions, uncommitted files your local run relies on, and OS-level differences between your machine and the CI runner.
The first ten minutes \u2014 establish facts before touching code.
- 1Check the CI log for the first failing test. Read the exact assertion error — do not assume it is the same root cause as other failures.
- 2Compare the Node/runtime version your local machine uses against the CI runner. Run `node --version` locally and check the CI config.
- 3Look for `npm install` warnings or errors in the CI log. A package that failed to install silently can break many tests.
- 4Check if any test relies on a file that is `.gitignore`d. CI gets a fresh clone — it will not have those files.
- 5Rerun the failing test in isolation locally (`npx jest -t 'failing test name'`). If it passes, the issue is test ordering or shared state.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchCI pipeline config file (`.github/workflows/*.yml`, `Jenkinsfile`, `.gitlab-ci.yml`) — check the runner image, Node version, and install steps
- search`package.json` `engines` field and lockfile — version mismatch with CI runner
- searchTest framework config (`jest.config.js`, `vitest.config.ts`) — test file ordering, timeout settings, parallelisation
- searchEnvironment variables in CI secrets/variables settings vs local `.env`
- search.gitignore — any file needed by tests that is not committed
- searchCI job logs — `npm install` output, test runner summary, and first failure
Practical causes, not theory. These are the things you will actually find.
- warningCI runs a different Node version than your local machine
- warningTest files run in a different order in CI (alphabetical on Linux vs something else locally)
- warningA test depends on global state set by a previous test that runs before it only locally
- warningMissing environment variable in CI secrets
- warningA dependency fails to install in CI (native module, platform-specific binary)
- warningCI runner has less memory or CPU than your local machine, causing timeouts
- warningTest snapshots or fixtures reference absolute paths that differ between machines
- warningCI uses a different timezone or locale than your local machine
Concrete fix directions. Pick the one that matches your root cause.
- buildPin the exact runtime version in CI config — use the same version your team uses locally
- buildRun tests with `--runInBand` or `--maxWorkers=1` in CI to rule out parallelisation and ordering bugs
- buildAdd a `test:ci` script that sets `NODE_ENV=test` and any CI-required env vars with safe defaults
- buildUse `--clearMocks` or equivalent to reset state between every test file
- buildCommit any fixture files, snapshots, or test data that tests need — never assume they exist only locally
A fix you cannot prove is a guess. Close the loop.
- verifiedPush a commit that only changes the CI config (no code changes) and confirm tests pass.
- verifiedRun the exact CI command locally: `NODE_ENV=test npx jest --ci --runInBand` or equivalent.
- verifiedDelete `node_modules` locally, run `npm ci` (not `npm install`), then run tests — this simulates a clean CI environment.
- verifiedRun tests on a different OS or in a Docker container that matches the CI runner image.
- verifiedCheck that all tests pass on two consecutive CI runs with no code changes between them.
Things that make this bug worse or harder to find.
- warningRerunning CI until it passes without investigating the failure
- warningAdding `--forceExit` or `--detectOpenHandles` as a permanent fix instead of closing resources
- warningAssuming the CI runner is the same OS as your local machine
- warningSkipping tests locally because 'they always pass on my machine'
- warningAdding arbitrary timeouts (`setTimeout`) instead of waiting for async operations properly