Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin9/20/202610 min read

CI/CD Pipelines Explained: What a Good One Actually Catches Before Production

CI/CD Pipelines Explained: What a Good One Actually Catches Before Production

# CI/CD Pipelines Explained: What a Good One Actually Catches Before Production

TL;DR: A CI/CD pipeline is a chain of automated gates — lint, type-check, test, build, scan, deploy — and each gate exists to catch one specific class of bug that the others can't. Skip a gate, or configure it to just report and not block, and you get exactly the failure class it was supposed to stop. CrowdStrike's July 2024 outage is a documented case of a gap in one of those gates (content validation) letting a bad update ship to every connected machine at once.

What "CI/CD" actually means, briefly

Continuous Integration (CI) is the automated process that runs every time code changes: install dependencies, lint, type-check, test, build. Continuous Delivery/Deployment (CD) is what happens after CI passes — packaging the build as an artifact (a container image, a binary, a static bundle) and pushing it toward staging or production, either automatically or with a manual approval gate.

Chaining these into one pipeline isn't process theater — each stage is cheap and catches a different failure mode, and stages get more expensive to run, and to fail, the further right they sit. A lint error costs a few seconds; a production incident costs an on-call page and possibly a postmortem with your company's name on it.

GitHub Actions is the dominant CI/CD platform by adoption today — roughly a third of organizations use it, ahead of Jenkins and GitLab CI, per JetBrains' 2026 CI tooling data — so the examples below use its YAML syntax, but the stage breakdown applies to any runner (GitLab CI, CircleCI, Buildkite, Jenkins).

The stage breakdown

StageWhat it checksWhat it does NOT catchTypical tool
LintStyle, unused vars, obvious anti-patterns, banned APIsLogic errors, type mismatchesESLint, Ruff, golangci-lint
Type-checkStructural correctness — does this value actually have this shape/methodRuntime-only failures (bad data, network timeouts)tsc --noEmit, mypy, go vet
TestBehavior — does this function/endpoint do what it's supposed to given known inputsAnything not covered by a written testJest/Vitest, pytest, go test
BuildDoes the whole thing actually compile/bundle into a deployable artifact with correct dependenciesBehavior at runtimewebpack/esbuild/tsc, Docker build
ScanKnown vulnerable dependencies, leaked secrets, container misconfigurationsZero-days, logic flaws, business-logic vulnerabilitiesTrivy, Semgrep, npm audit, Gitleaks
DeployCan the new artifact actually run in the target environment; does it pass a health check; can it be rolled backBugs that only show up under real production load/dataKubernetes rollout, blue-green, canary

Each row is a different net: a bug that slips lint might get caught by type-check, one that slips type-check might get caught by tests. A bug that slips all three should never reach deploy — but if it does, deploy's job is to limit blast radius (canary a slice of traffic, watch health checks, auto-rollback), not to catch correctness.

A concrete example: the bug that reaches production because a stage is missing

Here's a small, realistic TypeScript function with a bug that different pipeline stages would catch at different points — or miss entirely if that stage doesn't exist.

// src/billing/applyDiscount.ts
export function applyDiscount(order: Order, coupon?: Coupon): number {
  const discountPercent = coupon.percentOff; // bug: coupon can be undefined
  return order.total - order.total * (discountPercent / 100);
}

Walk this through the stages:

  • Lint won't catch it. coupon.percentOff is syntactically valid; ESLint has no idea whether coupon can be undefined at runtime.
  • Type-check catches it immediately in strict mode: 'coupon' is possibly 'undefined'. tsc --noEmit fails with a non-zero exit code before a single test runs — precisely the class of bug static typing exists for, which is why strict: true in tsconfig.json isn't optional in a serious codebase.
  • Test would also catch it, but only if someone wrote a test that calls applyDiscount(order) with no coupon. If the only test in the suite always passes a coupon, tests pass and the bug ships. This is the real danger zone: a green suite proves the cases someone thought to write are handled, not that the code is correct.
  • Build wouldn't catch it in a language without a compiler gate (plain JS bundling ships it happily).
  • Scan is irrelevant here — it's a logic bug, not a dependency vulnerability or leaked secret.
  • Deploy is the last line of defense: a canary watching error rates (a spike in 500s on checkout) can auto-rollback before every user hits it, instead of after.

The lesson isn't "type-check is the important stage" — it's that each stage covers a gap the others leave open. A pipeline that only runs tests, skipping type-check (common in JS/TS projects that treat tsc as "just for the editor"), is missing a gate that would've caught this bug for free, in under two seconds.

When a missing gate becomes a real incident

The synthetic example above is illustrative, but the same principle shows up in real, publicly documented outages — just at bigger scale, in the deploy and scan stages rather than type-check.

CrowdStrike, July 19, 2024. A faulty "Rapid Response Content" update (Channel File 291) shipped to Windows machines running CrowdStrike's Falcon sensor, causing the mass BSOD outage that grounded flights and took down hospital systems worldwide. CrowdStrike's own root cause analysis found the update passed their Content Validator — the automated gate meant to check integrity — because of a mismatch between the number of fields the update's template defined (21) and the number the sensor code provided (20). The 21st field used wildcard matching in every test case run between March and April 2024, so the specific non-wildcard case that broke in production was never exercised. Functionally, this is the same shape of failure as the coupon example above: a test gate existed, but its cases didn't cover the input shape that shipped. Source: CrowdStrike's Channel File 291 root cause analysis.

Knight Capital, August 1, 2012. A deployment-stage failure, not a logic bug: a technician's script silently failed to push new code to one of eight production servers, and that server's deployment reported success anyway. When markets opened, that server ran eight-year-old repurposed dead code and started firing unintended trades — $460 million in losses in 45 minutes. A deploy stage that verifies the artifact actually running on every target host, rather than trusting a script's reported exit code, is built to catch exactly this. See the SEC's enforcement release and Speculative Branches' writeup.

Neither company was careless — both had automated testing and validation. The failures were in test coverage and deploy verification, which is why "we have CI" and "we have a pipeline that actually catches things" are different claims.

A minimal but complete pipeline

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  build-and-verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24'   # current Active LTS as of 2026
          cache: 'npm'
      - run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type-check
        run: npx tsc --noEmit

      - name: Test
        run: npm test -- --ci

      - name: Build
        run: npm run build

      - name: Dependency & secret scan
        uses: aquasecurity/trivy-action@0.35.0   # pin exact versions after Trivy's 2026 supply-chain incident
        with:
          scan-type: 'fs'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'

  deploy:
    needs: build-and-verify
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "deploy artifact to staging, run health check, promote to prod on green"

Two details matter more than they look: exit-code: '1' on the scan step (a scan that only reports and never blocks the merge isn't a gate, it's a dashboard), and pinning trivy-action to an exact version rather than a floating tag — Trivy itself was hit by a supply-chain compromise in March 2026, after which Aqua Security's own guidance was to pin to specific known-good versions rather than track latest (StepSecurity's writeup).

CI/CD best practices worth calling out specifically

  • Make every stage a blocking gate, not an advisory report. A lint/scan step that runs but doesn't fail the build on error is a false sense of security.
  • Run cheap checks first. Lint and type-check take seconds; fail fast before spending minutes on a full test suite or build.
  • Pin action/tool versions in CI configs. Floating tags (@latest, @main) on third-party actions are a supply-chain risk, as both the Codecov 2021 bash uploader compromise and Trivy's 2026 incident show — in both cases a widely trusted tool became the attack vector because consumers weren't pinned to a known-good version.
  • Treat `strict` compiler/type-checker settings as non-negotiable — most of the value of type-checking in CI disappears if strict is off.
  • Use staged rollouts (canary or blue-green), not one-shot full replacement, so a bug that clears every earlier gate has a small, reversible blast radius instead of an instant, total one.
  • Verify what actually deployed, not just that the deploy script exited 0 — the Knight Capital failure was invisible to the deploy tooling itself.

Teams without the in-house bandwidth to build and maintain this — pipeline config, scanning, staged rollouts, patched runner images — often bring in outside help for the initial setup; it's one of the more common asks under Wise Hustlers' cloud & DevOps services.

FAQ

What's the difference between continuous delivery and continuous deployment?

Delivery means every change that passes the pipeline produces a deployable artifact, but a human approves the production push. Deployment removes that manual gate — a passing pipeline deploys automatically. Most teams start with delivery and move to deployment once they trust their test/scan coverage.

Do I need all six stages for a small project?

Lint, type-check, and test are cheap enough to always run. Build and scan matter once you have real dependencies and a deployable artifact. Staged/canary deploys matter once enough traffic makes an instant full rollout genuinely risky — a two-person side project can usually skip that.

Why did my tests pass but production still broke?

Tests only cover inputs someone wrote a test for. A green suite proves the tested paths work, not that all paths work — exactly what happened with CrowdStrike's untested non-wildcard field case.

Is a security scan the same as a penetration test?

No. Tools like Trivy or npm audit check for known vulnerabilities and misconfigurations via CVE databases. They won't find a business-logic flaw or novel vulnerability in your own code — that needs manual review, SAST tools like Semgrep for custom rules, or a real pentest.

Sources

Related articles