Skip to content

CI/CD as a System

Mental model

CI/CD is an assembly line. Raw materials (source code) enter one end. Each station (pipeline stage) transforms, tests, or inspects the product. At the other end, a finished artifact rolls out — either to a human for approval (delivery) or straight to production (deployment).

What it looks like

"Automation that builds and deploys code." A YAML file in your repo that runs on every push.

What it really is

An event-driven system with distinct components:

event (git push)
  -> trigger (webhook fires)
    -> pipeline (build, test, scan)
      -> artifact (immutable output)
        -> promotion (dev -> staging -> prod)
          -> policy (gates, approvals, rollback rules)
  • CI (Continuous Integration): merge code frequently, build and test automatically on every push. Goal: catch breakage within minutes, not days.
  • CD (Continuous Delivery): pipeline produces a deployable artifact. A human decides when to release.
  • CD (Continuous Deployment): pipeline deploys to production automatically. No human gate.

Why it seems confusing

"CD" means two different things depending on who's talking. Delivery = human gate before prod. Deployment = no gate. Most teams do delivery and call it deployment.

People also conflate the pipeline (the process) with the platform (Jenkins, GitHub Actions, GitLab CI). The pipeline is the DAG of stages. The platform is just the executor.

What actually matters

  • Build once, deploy everywhere. The artifact that passed tests in staging is the exact artifact that goes to prod. Never rebuild for a different environment.
  • Immutable artifacts. A container image or binary gets a unique tag/hash. You promote the artifact, not the source.
  • Fast feedback. A pipeline that takes 45 minutes to tell you about a typo is broken. Fail fast: lint and unit tests run first.
  • Shift left. Move testing and security scanning earlier in the pipeline. A vulnerability caught at build time costs less than one caught in production.
flowchart LR
    PUSH["git push"] --> LINT["Lint"]
    LINT --> TEST["Test"]
    TEST --> BUILD["Build\n(immutable artifact)"]
    BUILD --> DEV["Deploy Dev"]
    DEV --> STG["Deploy Staging\n(integration tests)"]
    STG --> GATE{"Approval\nGate"}
    GATE -->|approved| PROD["Deploy Prod"]

    style PUSH fill:#888,color:#fff
    style BUILD fill:#36f,color:#fff
    style GATE fill:#f80,color:#fff
    style PROD fill:#5a5,color:#fff

Common mistakes

  • Rebuilding artifacts per environment instead of promoting one immutable artifact through stages.
  • No pipeline at all — deploying by SSH and prayer.
  • Giant monolithic pipelines with no parallelism. Split independent jobs to run concurrently.
  • Skipping the plan/scan/test stages "just this once" and shipping a broken build.
  • Treating CI and CD as the same thing. CI is about integration quality. CD is about release mechanics.

Small examples

# GitHub Actions — typical pipeline stages
name: CI/CD
on: [push]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make lint

  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t app:${{ github.sha }} .
      - run: docker push registry/app:${{ github.sha }}

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: helm upgrade app chart/ --set image.tag=${{ github.sha }}
        # promotion to prod would be a separate job with approval gate
# Artifact promotion flow
build -> app:abc123 (image)
  -> deploy to dev   (automatic)
  -> deploy to staging (automatic, integration tests run)
  -> deploy to prod  (manual approval, then automatic)

One-line summary

CI/CD is an event-driven pipeline that transforms source into immutable artifacts and promotes them through environments with increasing confidence.