CI & Preview Deploys
We have a build (Part 3) and a deploy (Part 4). This page closes the loop into continuous integration: the automation that runs on every change so that “it builds on my laptop” is never the standard. For a static monorepo, CI is simpler than the capstone’s — there is no image to sign, no cluster to reconcile — but the purpose is identical: catch breakage before users, on every commit, the same way every time.
The CI workflow
Section titled “The CI workflow”The repo’s .github/workflows/ci.yml runs on every push to main and every pull request targeting it. It has
two jobs:
on: push: { branches: [main] } pull_request: { branches: [main] }
jobs: build: # Job 1 — build all nine sites steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: { bun-version: 1.3.14 } - run: bun install --frozen-lockfile - run: bunx turbo run build # the same one command from Part 3 - uses: actions/upload-artifact@v4 # keep the built dist/s for 7 days with: { name: site-dists, path: apps/*/dist, if-no-files-found: error }
rust: # Job 2 — the companion Rust crates still compile steps: - uses: actions/checkout@v4 - run: cargo check --manifest-path rust/Cargo.toml --workspaceA few first-principles points hide in that YAML:
bun install --frozen-lockfile— CI must install the exact versions inbun.lockand fail if the lockfile andpackage.jsondisagree, rather than quietly resolving new versions. CI that installs different dependencies than you tested is CI that lies. This is the reproducible-build principle enforced at install time.bunx turbo run build— CI runs the identical command a developer runs locally and Cloudflare runs in the dashboard. One build definition, three places, no drift. Turbo’s cache (Part 3) keeps it fast.if-no-files-found: error— a build that silently produced no output should be a red pipeline, not a green one. Failing loudly on “nothing to upload” prevents the classic “CI passed but shipped nothing” trap.- The
rustjob — the books ship companion Rust crates underrust/;cargo check --workspacemakes sure a docs change never lands alongside code samples that no longer compile. The two jobs run in parallel (they share no state), so the whole check is as slow as the slower job, not their sum.
Note what CI here does not do: it does not deploy. It proves the build is good; deployment is a separate
concern, handled either by Cloudflare’s Git integration or by wrangler. Keeping “is it correct?” separate from
“ship it” is deliberate — the same separation the GitOps page argued for.
Preview deploys: a real environment per pull request
Section titled “Preview deploys: a real environment per pull request”The highest-leverage feature of this whole setup is preview deployments. With Cloudflare’s Git integration
(Option A), every pull request gets its own live URL serving
that branch’s build — a complete, real, edge-served copy of the site, isolated from production. (The wrangler
path gives the same thing on demand via deploy:preview / --branch=preview.)
open PR ──► Cloudflare builds the branch ──► https://<hash>.fp-devops.pages.dev (a real, isolated preview site) │ │ │ reviewer clicks the link, sees the actual │ │ rendered change on the real CDN, not a guess ◄─────┘ ▼ merge to main ──► production build ──► https://fp-devops.pages.devWhy this matters, in the language of Part 0: a preview URL shortens the feedback loop from “merge, deploy, hope, discover in prod” to “see it, on real infrastructure, before merging.” A broken layout, a CSP that blocks search (exactly the WASM bug from the last page), a wrong link — all of it is visible on the preview, at zero risk to the live site. The preview is a disposable production-like environment, created and destroyed per PR, with none of the cost that phrase carried in the Kubernetes world.
The promotion model: merge is the deploy
Section titled “The promotion model: merge is the deploy”Put the pieces together and the path a change takes is short and boring — which is the goal:
- Open a PR. CI builds all nine sites and checks the Rust crates. A preview URL appears.
- Review on the preview. Look at the actual rendered site on the edge, not a screenshot.
- Merge to
main. The push tomaintriggers the production build-and-deploy. Merging is the deploy — there is no separate, manual “release” step to forget or do wrong. - It’s live, globally, in one atomic swap. No canary, no gradual rollout — a static swap is instantaneous and all-or-nothing, and if it’s wrong, the previous deploy is still one click (or one revert) away.
That last point is the static-edge model paying off one final time. The capstone needed an Argo Rollout with SLO analysis to make a deploy safe, because a bad running process can hurt users gradually and in hard-to-see ways. A static swap has no such failure mode: the files are either the new set or the old set, the rollback is re-pointing to a prior immutable deploy, and the preview already let you see the change before it shipped. Fewer moving parts, fewer ways to be wrong — which is where the book started, and a fitting place to end this Part. Where the capstone pointed you toward SRE and platform engineering, this Part’s next step is the frontier — where the edge stops serving only static files and starts running code again.
Check your understanding
Section titled “Check your understanding”- The CI workflow builds the sites but never deploys them. Why is keeping “prove it builds” separate from “ship it” a deliberate design choice, not an omission?
bun install --frozen-lockfilewill fail if the lockfile andpackage.jsondisagree. Why is failing the right behavior for CI, when a developer’s local install might happily resolve new versions?- Explain what a preview deploy is and, in feedback-loop terms, exactly which risk it removes from the review process.
- “Merging is the deploy.” Describe the full path from opening a PR to a live global site, and name the manual step this model eliminates.
- The capstone needed a canary with SLO analysis; this Part deploys with an instant atomic swap and no canary. Why is that safe here but not there?
Show answers
- CI’s job is to prove correctness on every change; deployment is a separate decision about when to ship. Separating them means a build can be verified without shipping, the deploy path (Git integration or wrangler) can change without touching CI, and you never conflate “the code is good” with “release it now.” It’s the same separation-of-concerns the GitOps page argued for.
- CI must test the exact dependency versions that will ship.
--frozen-lockfileguarantees the install matchesbun.lock; ifpackage.jsondrifted from the lockfile, silently resolving new versions would mean CI validated software different from what you committed — a green build that lies. Failing forces you to commit an updated, reviewed lockfile. - A preview deploy is a complete, isolated, edge-served copy of the site built from a PR’s branch, at its own URL, separate from production. It removes the risk of merging blind — reviewers see the actual rendered change on real infrastructure before merge, so layout breaks, a broken CSP, or bad links are caught pre-merge instead of in production. It shortens the feedback loop from “merge → deploy → discover” to “see → then merge.”
- Open a PR → CI builds all nine sites + checks Rust, and a preview URL is generated → review the change on
that real preview → merge to
main, which triggers the production build-and-deploy → the site is live globally in one atomic swap. It eliminates the separate manual “release/deploy” step — there is nothing to run by hand after merge. - A static swap is atomic and all-or-nothing: production is either the new set of files or the old set, with no partially-broken running process and an instant revert to a prior immutable deploy. A running service can fail gradually and subtly under real traffic, so the capstone needs a canary + SLO gate to expose only a slice of users and auto-roll-back on bad signals. No running process here means no such failure mode to guard against.