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.