Continuous Integration (CI) is a machine that, on every push, rebuilds the project from scratch and runs the tests in a clean environment. Its value rests on two properties: reproducibility (the same commit yields the same result, for everyone and always) and fast feedback (a developer learns of a breakage within minutes, while the context is still in their head). Lose the first and red becomes an accident; lose the second and people stop waiting for the build. Both properties are designed on purpose.
Reproducibility starts with fixing the inputs. Pin the Node version - CI and local must run the same one, or the behavior of timers, Intl, and streams will drift apart. Install dependencies from the lockfile with npm ci, not npm install: ci installs exactly what is recorded and fails on a mismatch, whereas install is free to update the tree. Cache the package manager store (npm/pnpm), not node_modules blindly: a blind module cache outlives a Node or platform change and one day slips you incompatible binaries.
The --ci flag changes one specific behavior, and does so for the sake of reproducibility: on meeting a snapshot not yet on disk, Jest in CI will not write it silently but fail the test and require a run with --updateSnapshot. Locally a new snapshot is created automatically - handy during development; in CI that is a hole through which an unreviewed snapshot would leak into a green build. So the test:ci script is jest --ci, and the order of steps in the pipeline is fixed: install first, then static checks, then tests, then build.
Speed is the other half of the task. By default Jest spins up a worker pool of cores minus one and spreads files across them - fast on a multi-core machine. The --runInBand flag forces everything into a single process: needed for debugging and for stability on tight runners with a small memory limit, but you pay for it in time. The middle lever is --maxWorkers: for example, --maxWorkers=50% caps the pool at half the cores so as not to choke a container with a hard limit.
When the suite no longer fits into an acceptable time, it is sharded. The --shard=1/3 flag, in index/count format, splits the test set into equal parts, and three parallel CI jobs take a third each - jest --shard=1/3, --shard=2/3, --shard=3/3. Feedback speeds up linearly with the number of shards, and coverage is then assembled from partial reports. A kindred lever of fast feedback is --bail: it aborts the run after the first failed suite, saving minutes when it is already clear the build is red.
Artifacts turn a red build into a diagnosis. Save the test report and coverage as job artifacts - then, without reproducing the failure locally, you can see which test failed and on which line. The cache here works for speed honestly: it speeds up transform roughly twofold, and disabling it (--no-cache) is worth doing only for a proven cache-specific problem, because without it Jest is at least twice as slow.
A major upgrade is a separate operation, not a line in the same PR. Moving to Jest 30, first read the upgrade guide, then run the suite on a separate branch and quench every warning before you touch assertions. Order matters: change the checks and the version at once and you will not know what broke - the new runtime or your edit. Space the changes out in time, and red will always point to a single cause.
npm ci
npm run lint
npm run typecheck
npm run test:ci
npm run build
# A large suite can be split up:
npx jest --shard=1/3
npx jest --shard=2/3
npx jest --shard=3/3