Chapter 37
Delivery Pipelines
Delivery pipelines for React SPAs — GitHub Actions matrix for Node + Bun + Deno, preview deploys on Vercel / Netlify / Cloudflare Pages, OpenFeature flags, and SPA canary releases.
Published 2026-05-23
🎯 Chapter Goal — After this chapter you can wire a GitHub Actions matrix for Node + npm + Vite (with optional Bun / Deno jobs), set up preview deploys to Vercel / Netlify / Cloudflare Pages, run feature flags through OpenFeature, and ship canary releases of an SPA.
🧭 Prerequisites — Ch 17 (Vite), Ch 18 (Bun/Deno), Ch 19 (monorepo), Ch 28 (feature flags).
🔹 37.1 GitHub Actions matrix
The core CI workflow for a Vite SPA (or monorepo — Ch 19 §19.7):
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push: { branches: [main] }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run build
- run: npm test
- run: npm run lint
build-bun:
runs-on: ubuntu-latest
continue-on-error: true # observer mode (Ch 18.6 stage 1)
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with: { bun-version: 1.x }
- run: bun install --frozen-lockfile
- run: bun run build
- run: bun run test
The build-bun job runs alongside Node, doesn’t block merges. Watch its times; if Bun’s a clear win and stays stable for a month, promote it to the primary job.
Matrix for multiple Node versions
strategy:
matrix:
node: [20, 22]
For libraries (Ch 11) you usually test against the LTS versions you support. For apps you usually pin to one — the one you’ll deploy with.
Concurrent cancellation
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
When a developer pushes a follow-up commit to a PR, the previous CI run cancels. Saves minutes; saves $.
Caching the install + Playwright browsers
- uses: actions/cache@v4
with:
path: |
~/.npm
~/.cache/ms-playwright
key: deps-${{ hashFiles('package-lock.json') }}
setup-node with cache: 'npm' handles the npm cache; the Playwright cache is separate (Ch 36.10).
🔹 37.2 Preview deploys
The pattern: every PR gets a unique URL. Designers and PMs review there before merge.
Vercel — the default
Push a PR; Vercel auto-builds; the PR gets a comment with the preview URL. Zero config beyond connecting the repo.
For monorepos (Ch 19):
Project: acme-web
Root directory: apps/web
Install command: cd ../.. && bun install --frozen-lockfile
Build command: cd ../.. && turbo run build --filter=web
Output directory: dist
Repeat per app. Each PR gets web-pr-123.vercel.app, admin-pr-123.vercel.app.
Cloudflare Pages
Same shape, different dashboard. Wrangler CLI for scripted deploys.
Netlify
Same shape. Netlify pioneered the preview-deploy pattern; still solid.
Self-hosted alternatives
- Render, Fly.io, Railway — all support PR previews.
- For air-gapped enterprise: spin up your own preview infra with a small script + S3 + an evict-after-N-days lambda.
The PR review flow:
- Developer pushes PR.
- CI runs; preview deploy publishes.
- Bot comments the URL on the PR.
- Designer reviews on the URL; comments on Figma/PR.
- Lighthouse CI fails the PR if budgets are exceeded (Ch 30).
- Merge → production deploy.
🔹 37.3 Feature flags
Cross-link Ch 28.2. The mechanism:
OpenFeature + a provider
import { OpenFeature } from '@openfeature/web-sdk';
import { GrowthBookProvider } from '@openfeature/growthbook-provider';
OpenFeature.setProvider(new GrowthBookProvider({
clientKey: env.VITE_GROWTHBOOK_KEY,
attributes: { tenantId: currentTenant.id, userId: currentUser.id },
}));
import { useFlag } from '@openfeature/react-sdk';
const { value: showNewCheckout } = useFlag('new-checkout-flow', false);
if (showNewCheckout) return <NewCheckout />;
return <LegacyCheckout />;
The deploy / release split
- Deploy — the code goes to production. The flag controls visibility.
- Release — flip the flag; the feature is live.
This decouples scary-merge from scary-release. The risky flag-flip can happen Monday at 10am with the team watching, not Friday at 5pm because that’s when the PR merged.
Flag governance
Flags accumulate. Set rules:
- Every flag has an owner and an expiry date.
- Quarterly review removes flags past expiry.
- Flags that haven’t flipped in 90 days are candidates for removal.
A 5-year-old “experimental_v2” flag is tech debt with a fancier name.
🔹 37.4 Canary releases for SPAs
Server apps canary easily (route 5 % of traffic to the new version). SPAs are harder because the user’s browser caches the bundle.
The percentage-rollout pattern
// at app load:
const inCanary = await fetch('/api/canary-flag').then((r) => r.json());
if (inCanary && !document.cookie.includes('canary=1')) {
// load the canary bundle instead of the main one
window.location.replace('/canary/');
return;
}
Two builds deployed:
/— current stable./canary/— new build.
A backend endpoint decides per-user. Cookie sticks the user to one variant for the session.
The version-pinning pattern
For long-lived sessions (admin tools): every API response includes a X-App-Version header. If the client’s loaded version differs from the server’s expected version, show “New version available — refresh.”
const checkVersion = (res: Response) => {
const v = res.headers.get('X-App-Version');
if (v && v !== currentVersion) showUpdateBanner(v);
return res;
};
The flag-gate-everything pattern
Instead of canarying the whole app, ship one big “v2 mode” feature flag. Internal users get it first; gradually widen. Bug found? Flip the flag, no redeploy.
Cache-busting the SPA shell
Even with canaries, your index.html must not be cached forever. Common headers:
index.html Cache-Control: no-cache
*.js, *.css Cache-Control: max-age=31536000, immutable (content-hashed)
The hashed assets cache forever; the entry HTML revalidates every request.
🪤 Common Pitfalls
- CI without concurrency cancellation → wasted minutes on superseded commits.
- Preview deploys with auth — anonymous reviewers can’t see them. Use signed URLs or basic auth.
- Feature flags without expiry → tech debt accumulation.
- Canary rollout without a kill switch — bug hits 100 % users while you scramble.
- SPA
index.htmlcached forever — users stuck on old versions for weeks. - Lighthouse CI with aspirational budgets nobody enforces.
✅ Recap
- GitHub Actions matrix with
cancel-in-progressandcache. - Preview deploys for every PR; designer review before merge.
- Feature flags decouple deploy from release; OpenFeature is the spec; pick a provider.
- Canary SPAs via percentage rollout, version-pinning, or feature-flag gating.
- Cache the assets, not the shell.
🔗 Further Reading
- https://docs.github.com/en/actions — GitHub Actions reference.
- https://vercel.com/docs — Vercel docs.
- https://developers.cloudflare.com/pages — Cloudflare Pages docs.
- https://openfeature.dev — OpenFeature spec (cloud-vendor-neutral).
- https://www.growthbook.io — GrowthBook (open-source flag provider).
- https://launchdarkly.com — LaunchDarkly (commercial flag provider).
- Ch 17 (Vite), Ch 18 (Bun/Deno in CI), Ch 19 (monorepo CI), Ch 28 (auth + flags), Ch 30 (Lighthouse).
In the book — not on the site
Each topic has an 🧠 Under-the-hood subsection — the algorithm, the data structures, what React DevTools surfaces, debugging recipes. Plus a 🧪 hands-on lab per chapter with a starter repo. Reserved for the book.