CI/CD in practice
The night before the buildathon, the live site was three commits behind what everyone thought was live. Sahil's job is making sure that never happens again.
Before any of this — before branch protection, before Actions, back when "deploy" meant someone manually zipping the website folder and uploading it — Sahil was up at 11 PM the night before an event, dragging files onto a hosting dashboard by hand. He grabbed the folder from his own laptop. Which, it turned out, was three commits behind what Rachit and Anvesha had actually merged that afternoon — his local copy just hadn't been pulled recently. The event's schedule page went live showing last week's dates. Nobody caught it until a member asked, mid-event, why the workshop time on the website didn't match the one Discord had.
Nothing about that failure was really Sahil's fault personally — it's what happens when "which version is live" depends on one person remembering to pull, zip, and upload correctly, under time pressure, at 11 PM. His actual assignment since then, the one written on the dashboard as "CI/CD — Pages deploy," is removing that entire manual step from existing.
CI and CD are two separate promises
- Continuous Integration — every push and every PR gets automatically tested. The lint workflow from last chapter is already this.
- Continuous Delivery/Deployment — every merge to
mainautomatically becomes the live site. No manual zip, no manual upload, no "whichever laptop happened to be pulled most recently."
Workflow one: a real CI gate, wider than just lint
# .github/workflows/ci.yml
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
Every PR now gets a genuine build attempt, not just a style check — if the site doesn't actually compile, that's caught here, on GitHub's machine, before a human reviewer even opens the diff. The uploaded artifact means Vidha or Anvesha can download and preview the actual built output straight from the PR page, without checking the branch out locally at all.
Workflow two: the fix for Sahil's 11 PM problem
# .github/workflows/deploy-pages.yml
name: deploy-pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with: { path: dist }
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
Read what this actually guarantees, in plain terms: the moment a PR merges into main — which, since Chapter 15, only happens after review and passing checks — this workflow checks out that exact commit, builds it fresh on GitHub's own clean machine, and publishes exactly that build. Not whatever happened to be on Sahil's laptop. Not "probably the latest, I think I pulled this morning." The literal commit that was reviewed and approved, deployed automatically, every time, with zero manual steps for anyone to forget under pressure.
The needs: build line means deploy only runs after build finishes successfully — a broken build never reaches the live site. The concurrency block stops two deploys from racing each other if two merges happen close together.
Workflow three: a snapshot the club can point at
For the actual buildathon submission — the version judges see, that needs to stay exactly as it was regardless of what main does afterward:
# .github/workflows/release.yml
name: release
on:
push:
tags: ["v*.*.*"]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run build
- uses: softprops/action-gh-release@a1b2c3d4... # pinned SHA, per Chapter 17
with:
generate_release_notes: true
files: dist/*.zip
Push a tag like v1.0.0 (Chapter 10 already showed Sahil doing exactly this) and a Release page appears automatically, with generated notes and a downloadable snapshot attached — the same tag that lets Sahil cherry-pick a fix backward, now also drives an automatic publish.
Environments — a human checkpoint before anything irreversible
Settings → Environments → "production." This is where the club can add a required-reviewer gate: nobody deploys to production without an explicit approval click, even though the workflow itself is fully automated otherwise. A wait timer (say, 10 minutes) gives a window to notice something's wrong and cancel before it goes live. Scoped secrets keep production credentials separate from anything a staging or preview deploy can see.
Caching — because nobody should wait five minutes for the same dependencies twice
The cache: npm line already sitting in every workflow above hashes the lockfile and restores previously-downloaded dependencies between runs. Without it, every single push reinstalls everything from scratch — slow, and genuinely wasteful of the club's free Actions minutes.
When a run fails, and you need to know why
- Actions tab → click the failing run → expand the red step. Full log, right there.
- "Re-run failed jobs" — worth trying once before assuming it's a real bug, since flaky network conditions on shared runners do happen.
- For genuinely local debugging, nektos/act runs the same workflow file in Docker on your own machine, without pushing anything.
Deploys are automatic now. But the URL the site actually lives at, and what happens when someone wants a permanently citable snapshot rather than just "whatever main currently builds to" — that's the piece still missing, and it's the other half of what Sahil's dashboard task actually means.