CI/CD Pipeline Design: From Zero to Production Deployment
CI is “every change is tested before it is anyone else’s problem.” CD is “a green build can be shipped without a ritual.” The rest is taste: how fast the feedback is, and whether a human still has to press the last button.
This page is a pipeline you can paste and then tighten. It assumes a Node app and GitHub Actions because that is what most of the tutorials on this site build toward — including this site’s own host, Cloudflare Pages. The git workflow is the branch policy that sits underneath. An AI review job can be one more check; it is not a substitute for tests.
Desk note — who this is for / what it’s bad at: Teams shipping a Node or static app from GitHub with tests that actually run. Bad as a reason to buy a second CI product when Actions plus the host’s preview URLs are unread, and a poor substitute for a rollback plan.
What to check before you buy a runner (or click deploy)
Listing identity: GitHub Actions is the YAML in this article — lint, test, build, then deploy from main. Vercel / Netlify / Cloudflare Pages git-push builds are host builds; they are not your unit suite. CircleCI / Buildkite / self-hosted runner farms are extra CI products. A host build is not a test suite.
Before you add a paid runner or connect a second CI:
- Fail cheap first. Lint and typecheck before Playwright. A pipeline people disable is not CD.
- Skip a second CI invoice if Actions already runs
npm ci, tests, and build, and the host already gives per-PR preview URLs. Another vendor does not write the tests you skipped. The AI unit-testing page is what those tests should assert. - Do not treat the host build as CI. Pages or Vercel running
astro buildwill not run AI review or your unit tests. Use the host for previews — see web hosting — and Actions for the red X. - Do not buy a flagship card so CI can run a local model. A 4090 search is unused VRAM on a GitHub-hosted runner (that runner does not have your card). Local inference for private diffs is a self-hosted runner plus the local LLM page, or skip the bot. Cursor’s HTTPS hop is the tunnel setup, not this YAML.
- Skip auto-deploy to production if you cannot roll back. That is continuous delivery: staging is automatic, production is a person. Retrieval and index rebuilds belong in this pipeline — see vector databases — not in a hope. An agent with a deploy tool is a write you have not earned yet.
Practical cadence: lint → test → build on every PR; preview URL from the host; production only from main after those checks. More articles live on the blog index. Amazon search links on this page use tcalnet-20; see how we make money.
What the words mean
Continuous integration. Every push and every pull request runs automated checks. A red X is a reject, not a suggestion.
Continuous delivery. main stays deployable. Staging updates itself. Production is one approval (or one click) away.
Continuous deployment. The same green build goes to production with no approval step. Only do this if the tests actually protect you and you can roll back.
If you cannot roll back, you do not have CD. You have a script that hopes.
Fail cheap, then fail real
Order the work by cost:
- Lint / format / typecheck — seconds, catches the boring mistakes.
- Unit tests — still fast if you did not couple them to the network.
- Build — the artifact you will actually ship (
npm run build, not “it works on my laptop”). - Integration / e2e — against a preview URL or an ephemeral database, not against production.
A pipeline that runs Playwright before ESLint is a pipeline people will disable. The AI unit-testing notes are about what those tests should assert; this page is about when they run.
A pipeline that matches a static or edge host
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci && npm run build
- name: Deploy to Cloudflare Pages
run: npx wrangler pages deploy dist/ --project-name="$PROJECT"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
PROJECT: ${{ vars.CF_PAGES_PROJECT }}
Notes that are easy to skip:
npm ci, notnpm install.ciinstalls the lockfile.installmay rewrite it. Reproducible builds are a security feature as much as a convenience — see AI-generated dependency risk.concurrency. A newer push to the same branch cancels the stale run. Otherwise you can deploy an older SHA after a newer one.environment: production. GitHub Environments give you a separate secret scope and an optional required reviewer. That is how you get continuous delivery (human gate) without forking the workflow.- Same Node version as production. This site’s
package.jsonasks for Node>=22.12.0. Pinning22in Actions is the minimum honest move.
Vercel, Netlify, and Cloudflare Pages will also build on git push if you connect the repo in their UI. That is fine. You still want this workflow for tests, because a host build that only runs astro build will not run your unit tests. Use the host for preview URLs; use Actions for the red X.
Preview deploys vs production
| Event | What should happen |
|---|---|
| Pull request | lint, test, build; optional preview URL |
Push to main | all of the above, then production |
| Tag / release | optional extra job (npm publish, changelog) |
Do not deploy PR branches to the production project. A preview hostname per PR is how designers and QA look at the change without touching customers. Playwright against that preview is the first e2e that is worth the minutes.
Secrets, logs, and the things that page you
- Secrets live in Actions secrets or the host’s dashboard. A token in YAML is a leaked token.
echoof env vars in logs is how tokens die in public forks. Prefer::add-mask::.- Fork PRs from public repos should not get your deploy token. Restrict the deploy job to
github.event_name == 'push'onmain, as above. - A red deploy with a green test job is still a red pipeline. Do not
continue-on-errorthe production deploy to keep a badge green.
If you add an AI review step, give it pull-requests: write and its own API key. Do not reuse the Cloudflare token.
When not to automate the last mile
- You have no rollback (no previous Pages deployment, no image tag, no database backward-compat).
- The release needs a migration you have not tested against a copy of production data.
- The only test is “the author clicked around.”
- You ship native binaries to customers who cannot take a broken update.
In those cases, stop at continuous delivery: staging is automatic, production is a person who can see the diff.
Related Reading
- Git Workflow Best Practices for Small Teams
- Automating Code Reviews with AI: A Practical Integration Guide
- Best Web Hosting for Developers
- AI Code Generation Security: Best Practices Every Developer Needs
- How to Automate Unit Testing with AI Tools
- How to Use a Local LLM in Cursor with Ollama (2026 Tunnel Setup)
- Vector Databases Explained: When You Need One and How to Choose
- Building AI Agents: Architecture Patterns and Practical Examples
- All articles
- How We Make Money