Skip to main content
CodeOath
← All posts

CI/CD & DevOps65 min total · 17 parts

CI/CD Pipelines Explained: From Push to Production

Part 5 of 17 · ~2 min

A Real Pipeline, Stage by Stage

Here's web's pipeline as the team first writes it, three separate jobs wired together by exactly the dependencies that actually exist:

# .github/workflows/web.yml
name: web

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run lint

  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm test

  build-image:
    needs: [lint, unit-tests]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t ghcr.io/snapledger/web:${{ github.sha }} .
      - run: docker push ghcr.io/snapledger/web:${{ github.sha }}

  deploy-staging:
    needs: build-image
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh staging ghcr.io/snapledger/web:${{ github.sha }}

lint and unit-tests have no needs: between them, so they start at the same moment, on two separate runners, and build-image waits for both — needs: [lint, unit-tests] means exactly that, not "run after lint, which runs after tests," which is the mistake of reading a job list top to bottom instead of reading the graph it actually declares. Notice, too, the tag on that image: ${{ github.sha }}, the exact commit hash, never latest. That's not a style preference — it's the direct lesson from the base-image chapter of this team's own Docker story, applied one level up: a moving tag is a promise you can't keep, and the whole rest of this pipeline depends on being able to say, unambiguously, which bytes are running where.

The on: block matters as much as the jobs do. Both a push to main and an open PR aimed at it trigger lint, unit-tests, and build-image, so whoever's reviewing sees green or red before they approve anything — but deploy-staging's if: github.ref == 'refs/heads/main' means only an actual merge ever reaches a real environment. However clean a branch looks, staging stays untouched until it's actually on main.