1 min read

Designing a CI/CD Pipeline

Stages that catch problems early and ship with confidence: lint, test, build, and deploy, each gating the next.

On this page

A good pipeline turns "it works on my machine" into "it works because the pipeline proved it." Each stage gates the next, so a failure stops the line before bad code reaches production.

The stage flow

graph LR;
    A[Push] --> B[Lint];
    B --> C[Test];
    C --> D[Build];
    D --> E[Deploy to staging];
    E --> F[Deploy to production];

A workflow definition

Jobs run in order, and a red stage blocks everything downstream:

yml.github/workflows/ci.yml
name: CI
on: [push]
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install
      - run: pnpm lint
      - run: pnpm test
      - run: pnpm build

Fail fast, fail cheap

Order stages from quickest to slowest. Linting takes seconds and catches the silly mistakes before a multi-minute test suite even starts:

sh
pnpm lint && pnpm test --bail

Cache the slow parts

Cache dependencies and build output keyed on the lockfile so unchanged installs are near-instant. CI minutes add up fast otherwise.

Promote, do not rebuild

Build the artifact once and promote the same one through staging to production. Rebuilding per environment invites drift.

A reusable deploy job

A deploy job with environment protection rules:

class HelloWorld
def initialize(name)
@name = name.capitalize
end
def sayHi
puts "Hello #{@name}!"
end
end
hello = HelloWorld.new("World")
hello.sayHi
view raw hello_world.rb hosted with ❤ by GitHub

Keep stages ordered, fast feedback first, and ship the exact artifact you tested.