Works Locally but Fails in CI: How to Debug the Gap
The classic "but it works on my machine." The CI runner differs from your laptop in a handful of predictable ways.
CI-only failures almost always trace to an environment difference. Check these in order.
The usual suspects
| Difference | Symptom / fix |
|---|---|
| Case-sensitive filesystem | Linux CI is case-sensitive; import "./Foo" vs foo.js fails - fix the casing |
| Missing dependency / tool | Installed locally, not on the runner - add it to the workflow or image |
| Uncommitted file | A file works locally but is not in Git - commit it |
| Time zone / locale | CI runs UTC; date/locale-sensitive tests fail - pin TZ and locale |
| Resource limits | Less memory/CPU in CI causes OOM/timeouts - right-size or self-heal |
| Test ordering / parallelism | CI runs tests differently - fix hidden test interdependencies |
| Clean install vs cached node_modules | Use npm ci so CI matches a clean lockfile install |
Reproduce it fast
- Run the job in a container matching the runner image locally.
- Enable debug logging (
ACTIONS_STEP_DEBUG) and re-run. - Open an interactive shell on the runner (tmate) to inspect the real environment.
Key takeaways
- CI-only failures are environment differences, not magic.
- Case sensitivity, missing deps, TZ/locale, and resources are the top causes.
- Reproduce in a container matching the runner image.
Related guides
How to Debug a Failing GitHub Actions WorkflowDebug GitHub Actions: enable step debug logging, re-run with SSH/tmate, read the real error, and reproduce lo…
Flaky Tests in CI: Causes, Detection, and ContainmentWhat makes tests flaky, how to detect flakiness, and how to contain it with quarantines and bounded retries w…
Self-Healing CI: Recovering from OOM-Killed Jobs (Exit 137)Out-of-memory kills (exit 137) are mechanical, not code bugs. See the manual fix and how self-healing CI auto…