Phase 8: CI/CD with GitHub Actions
A typical GitHub Actions workflow that lints, type-checks, tests, and builds. Vercel handles deployment separately. Branch protection enforces review.
Phase 8: CI/CD with GitHub Actions
In one line: GitHub Actions lints, type-checks, tests, and builds on every PR. Vercel handles deployment. Branch protection makes the green-CI requirement non-optional.
"CI/CD" is just two ideas: every change is verified automatically (CI), and verified changes deploy automatically (CD). At startup scale, you don't need to build either yourself — GitHub Actions runs your tests for free, and Vercel deploys whenever main updates. The interesting work is configuring branch protection so neither step can be bypassed under pressure.
A typical workflow file
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint and format
run: bunx biome check .
- name: Type check
run: bun run typecheck
- name: Unit + integration tests
run: bun run test
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- name: Build
run: bun run build
e2e:
runs-on: ubuntu-latest
needs: validate
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Playwright browsers
run: bunx playwright install --with-deps chromium
- name: Run E2E tests
run: bun run test:e2e
env:
BASE_URL: ${{ secrets.PREVIEW_URL }}
In English: This is a GitHub Actions workflow — the YAML lives at
.github/workflows/ci.ymland runs on every push tomainand every pull request. There are two jobs.validateruns on a fresh Ubuntu VM: it checks out the code, installs Bun, runsbun install --frozen-lockfile(the strict version that fails ifbun.lockis out of date), lints with Biome, type-checks, runs unit/integration tests against a real test database, and finally compiles the production build.e2eonly runs aftervalidatepasses (needs: validate); it installs Playwright's headless Chromium and runs the end-to-end suite against a preview URL. Secrets (TEST_DATABASE_URL,PREVIEW_URL) are stored in GitHub's encrypted secrets store, not in the YAML.
CI typically completes in 5–10 minutes. Vercel handles deployment separately — every PR gets a preview URL automatically; merges to main deploy to production.
Branch protection
GitHub branch protection on main:
- Require pull request before merging.
- Require status checks to pass (CI, type check, tests).
- Require approval from at least 1 reviewer.
- Dismiss stale approvals when new commits are pushed.
- Require linear history (no merge commits).
Hot fixes
For emergency production fixes, the same flow applies — branch, PR, review, merge, deploy. CI is fast enough that emergency fixes ship in 15–30 minutes.
A user reports checkout is broken. You confirm in Sentry: a null check is missing in the Stripe webhook handler.
- 23:02 — Branch off main:
git checkout -b fix/stripe-webhook-null. - 23:05 — Add the null check, add a test that would have caught it.
- 23:07 — Push, open PR.
- 23:08 — Ping a teammate on Slack. They review in 3 minutes.
- 23:14 — CI green, approval in. Merge.
- 23:17 — Vercel deploy completes. Verify in production.
Total: 15 minutes from "user reports broken" to "fixed in production" — without bypassing review, without deploying off a personal laptop. The fast-CI + branch-protection combination is what makes that possible.
At 5 people, "we trust each other to not push broken code" is reasonable. At 15, that trust breaks down — not because anyone is bad, but because somebody will get tired and push a typo to main on a Friday.
Branch protection makes the safe path the default path. Nobody has to remember to ask for review; nobody has to remember to wait for CI. The system enforces both. This is process earning its way in.
Common mistakes
- Letting CI grow to 25+ minutes. Once feedback takes longer than a coffee break, engineers stop running it locally and start "fixing forward" through pushes. Split jobs, parallelize tests, cache aggressively — aim for under 10 minutes total. Speed is a feature.
- Adding a "skip CI" escape hatch for emergencies. It will be used for non-emergencies within a month. The worked example proves you don't need one — branch protection plus a fast pipeline ships hot fixes in 15 minutes through the normal path.
- Trusting Vercel previews as the deploy gate. Previews are great for visual review but don't run your full DB migrations against production data. Have a real staging environment or a migration dry-run step before merging schema changes.
- Letting one admin grant themselves bypass-protection rights. The CTO becomes the single point of failure — and the one person nobody pushes back on at 11 PM. Branch protection should apply to everyone, founders included.
- Not deleting merged branches. A repo with 300 dead branches makes
git branch -auseless and confuses tooling. Turn on GitHub's auto-delete after merge — one toggle, recovered forever.
Page checkpoint
Did CI/CD at startup scale stick?
RequiredWhat's next
→ Continue to Phase 9: Deployment & Infrastructure where we compare three popular hosting patterns.