Your CI Pipeline Takes Longer Than the Sprint

You push. The green check appears. Then it's gone, replaced by the spinner of doom โ€” the one that says "Building..." for 47 minutes on a repository that, until Tuesday, finished in 12.

You look at the pipeline. It has five stages. The first is "Install Dependencies." The fifth is "Test." The stages between them are "Install More Dependencies," "Restore Dependency Cache," and "Install Dependencies (Docker)."

The Pipeline That Grew

It started simple. A .github/workflows/main.yml file with 30 lines. It ran npm ci, ran npm test, and if both passed, you shipped. It took 6 minutes. You were optimistic.

Then someone added linting. Then type checking. Then a Docker build because "we need to reproduce staging." Then a second Docker build for the "production-like" image. Then someone added a matrix for three Node versions, two operating systems, and an architecture nobody on the team owns.

Your pipeline now has enough jobs to qualify as a small economy. The original 6-minute workflow is still in there, somewhere, buried under npm ci && npm run build --if-present && docker build ..

The Docker Problem

Your Dockerfile has 14 layers. It starts from node:22-alpine. The final image is somehow large enough to contain a second, angrier application.

The extra weight is node_modules. Somewhere inside is node-sass, which has reached end of life, calling node-gyp, which needs Python and a native compiler when it has to build an addon from source. Your frontend dependency has become a toolchain.

Then the Dockerfile copies the entire repository before installing packages. Change one source file and the cache for every following layer is gone. Docker is doing exactly what the Dockerfile asked; the Dockerfile was written during an incident.

The fix is to copy the package manifests first, install from the lockfile, and copy the application source afterward. Use a multi-stage build so compilers and development dependencies stay in the build stage instead of riding to production.

The Matrix Explosion

You have three Node versions, two operating systems, and two architectures. That's 12 combinations.

Then someone adds a fourth Node version. Now it's 16. Add a third operating system and it's 24. GitHub Actions creates a job for every combination, because multiplication is the one part of your pipeline with perfect test coverage.

The jobs run in parallel when runners are available, but the workflow still finishes when the slowest required job does. One obscure platform fails. You fix the typo. You push again. The matrix gets another full meal.

The Honest Fix

Your pull-request pipeline may not need the full compatibility matrix. Run the fast, representative checks on every change. Run broader platform coverage on main, on a schedule, or before a release โ€” wherever it matches the support promise you actually make.

# Every pull request:
strategy:
matrix:
os: [ubuntu-latest]
node: [22]

# Scheduled or release workflow:
# expand to every supported OS and Node version

Use actions/setup-node dependency caching or an equivalent cache keyed by the lockfile. Order Dockerfile steps so dependency layers survive ordinary source changes. Pin important base images deliberately. Cancel superseded runs on the same branch when only the newest commit matters.

And inspect the critical path before buying faster runners. Twelve jobs in parallel can still wait on one serial integration test that boots half the company.

Your pipeline should be fast enough to remain part of development. If everyone starts merging before it finishes, you don't have a quality gate. You have an email notification.


Your CI pipeline is like a morning routine: the fourth install step is not self-care.

โ† You Picked the "Best" Model. It Hasn't Changed Anything.