Order CI Stages Cheapest and Most Likely to Fail First
Your CI wastes ten minutes catching a typo that lint could flag in thirty seconds. The fix isn't a faster machine — it's ordering. Run the cheapest, most likely-to-fail checks first, and a bad commit dies in seconds instead of after the full test suite.
Why order beats horsepower
The later a defect is caught, the more it costs to fix. The IBM Systems Sciences Institute's often-cited figures put a defect found in design at 1x, in implementation ~6.5x, in testing ~15x, and after release 60–100x. The same logic applies inside a single CI run at the scale of minutes: a typo caught by lint costs you 30 seconds; the same typo caught only when the test job crashes costs you the whole ten-minute run plus the context switch.
So the goal of a pipeline isn't just "run every check." It's surface the cheapest failure as early as possible. Every second a doomed commit spends in your slow stages is pure waste.
The ordering rule
Sort stages by two axes and run them in this order: cheap-and-flaky first, expensive-and-solid last.
| Stage | Typical time | Catches | Order |
|---|---|---|---|
| Format + lint | seconds | Style, typos, unused imports, obvious bugs | 1st |
| Type-check | seconds–1 min | Wrong signatures, null misuse, renamed APIs | 2nd |
| Unit + integration tests | minutes | Logic, regressions | 3rd |
| Build / package / deploy | minutes+ | Bundling, artifacts, env wiring | 4th |
Read it top-down: lint fails a typo before the type-checker ever boots; the type-checker rejects a bad signature before a single test spins up; tests catch logic before you burn a build. Each gate is cheaper and more likely to trip than the one below it, so the average failing commit exits early.
Make CI actually stop early
Ordering only pays off if a failed stage blocks the next one. In GitHub Actions, chain jobs with needs so an expensive job never starts until its cheap predecessor is green:
jobs:
lint:
runs-on: ubuntu-latest
steps: [ { uses: actions/checkout@v4 }, { run: make lint } ]
typecheck:
needs: lint
runs-on: ubuntu-latest
steps: [ { uses: actions/checkout@v4 }, { run: make typecheck } ]
test:
needs: typecheck
runs-on: ubuntu-latest
steps: [ { uses: actions/checkout@v4 }, { run: make test } ]
If a job a downstream job needs fails or is skipped, GitHub Actions skips the dependent job by default — so test never runs when lint is red.
Power moves
- Shift the cheapest gate off CI entirely. Run format + lint as a
pre-commithook so the typo never reaches the runner. Hooks run in seconds on the changed files, on every commit — the earliest possible catch. - Cancel superseded runs. Add a
concurrencygroup withcancel-in-progress: trueso a new push kills the in-flight run on the same branch instead of paying for a stale one. - Parallelize what's independent, gate what's dependent. Lint and type-check often don't depend on each other — run them in parallel, then make
testneedsboth. You keep fail-fast without serializing checks that could overlap. - Split the slow suite. If tests dominate, run a fast smoke subset first and gate the full matrix behind it. Same principle, one level deeper.
The payoff
A typo now fails fast and loud in seconds, not after a ten-minute run. Your slow, expensive stages only ever run on commits that already cleared the cheap ones — so your runner minutes, and your attention, go to the changes that actually deserve them.
Resources
- Cost of Defects in Software Testing: 2026 Data & Multipliers — the IBM/NIST cost-escalation figures
- GitHub Actions: workflow syntax —
jobs.<job_id>.needs— chaining jobs so cheap gates block expensive ones - GitHub Actions: using concurrency (
cancel-in-progress) — cancel superseded runs - pre-commit — run lint/format before the commit ever reaches CI
Shipping AI-assisted code fast? Yeda AI designs and audits the pipelines that keep speed from becoming breakage.