5+ years software engineer
5+ years software engineer
5+ years software engineer
5+ years software engineer
CI/CD turns deployment from a nerve-wracking manual ritual into a boring automated one. You push code, and a pipeline installs dependencies, lints, tests, builds, and deploys — the same way, every time. The payoff is speed and safety: bad code fails the build instead of production, and every release is identical. Here's how I set one up, with a complete GitHub Actions file you can drop in.
CI (Continuous Integration) and CD (Continuous Delivery/Deployment) are one flow. Continuous Integration is the test-and-build half; Continuous Delivery is the deploy half. Together they look like this:
git push / open PR
│
▼
┌─────────────── CI ───────────────┐
│ 1. Checkout code │
│ 2. Install dependencies (npm ci) │
│ 3. Lint │
│ 4. Run unit tests │
│ 5. Build │
│ 6. Security scan │
│ 7. Package the artifact │
└──────────────┬───────────────────┘
▼
┌─────────────── CD ───────────────┐
│ 8. Deploy to staging │
│ 9. Integration / e2e / smoke │
│ 10. (optional) manual approval │
│ 11. Deploy to production │
│ 12. Monitor + rollback if needed │
└──────────────────────────────────┘
The key idea: the same artifact you built and tested in CI is the one you promote to production. You never rebuild for prod — that reintroduces the risk you just eliminated.
| Platform | Best for | Notes |
|---|---|---|
| GitHub Actions | Repos already on GitHub | Easiest start, huge marketplace, generous free tier |
| GitLab CI | Teams on GitLab | Tightly integrated, .gitlab-ci.yml |
| Jenkins | Self-hosted, full control | Powerful but you maintain it |
| CircleCI | Fast builds, good caching | Strong Docker support |
For most teams I default to GitHub Actions — the pipeline lives next to the code and there's nothing extra to host. The choice matters less than people think; the concepts (stages, artifacts, secrets, environments) transfer between all of them. Learn the ideas once and you can read any pipeline.
This is a real, runnable CI workflow for a Node app. It runs on every push and PR, and only deploys from main.
yaml1name: CI/CD 2 3on: 4 push: 5 branches: [main] 6 pull_request: 7 branches: [main] 8 9jobs: 10 ci: 11 runs-on: ubuntu-latest 12 steps: 13 - name: Checkout 14 uses: actions/checkout@v4 15 16 - name: Setup Node 17 uses: actions/setup-node@v4 18 with: 19 node-version: 20 20 cache: npm 21 22 - name: Install dependencies 23 run: npm ci 24 25 - name: Lint 26 run: npm run lint 27 28 - name: Run tests 29 run: npm test -- --ci 30 31 - name: Build 32 run: npm run build 33 34 - name: Security audit 35 run: npm audit --audit-level=high 36 37 deploy: 38 needs: ci 39 if: github.ref == 'refs/heads/main' 40 runs-on: ubuntu-latest 41 environment: production 42 steps: 43 - name: Checkout 44 uses: actions/checkout@v4 45 46 - name: Deploy 47 run: ./scripts/deploy.sh 48 env: 49 DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
A few things worth pointing out:
npm ci, not npm install — it installs exactly what's in the lockfile, so CI is reproducible.cache: npm — caches dependencies between runs to keep the pipeline fast.needs: ci — deploy only runs if CI passed.secrets.DEPLOY_TOKEN — credentials come from GitHub's secret manager, never the repo.environment: production — lets you require a manual approval before this job runs.The workflow above deploys straight to production once CI passes, which is fine for a small project. For anything with real users, insert a staging step: deploy the tested artifact to a staging environment, run end-to-end and smoke tests against it, and only then promote to production. In GitHub Actions you model this with separate jobs and environment protection rules, which can also gate the production job behind a required reviewer's approval.
yaml1 deploy-staging: 2 needs: ci 3 runs-on: ubuntu-latest 4 environment: staging 5 steps: 6 - uses: actions/checkout@v4 7 - name: Deploy to staging 8 run: ./scripts/deploy.sh staging 9 - name: Run e2e smoke tests 10 run: npm run test:e2e -- --base-url=$STAGING_URL 11 12 deploy-production: 13 needs: deploy-staging # only runs if staging + its tests passed 14 if: github.ref == 'refs/heads/main' 15 runs-on: ubuntu-latest 16 environment: production # can require a manual approval here 17 steps: 18 - uses: actions/checkout@v4 19 - name: Promote to production 20 run: ./scripts/deploy.sh production
The chain ci → deploy-staging → deploy-production means a failure anywhere short-circuits the rest. Nothing reaches production that didn't pass every gate before it.
How you release matters as much as that you release. The three common strategies:
| Strategy | How it works | Trade-off |
|---|---|---|
| Rolling | Replace instances gradually, a few at a time | Simple; brief mixed-version window |
| Blue/green | Two identical environments; switch traffic all at once | Instant rollback; needs double the infra |
| Canary | Route a small % of traffic to the new version first | Safest for risky changes; more complex routing |
For a payment-critical system like an e-commerce checkout, I lean toward blue/green or canary — being able to roll back instantly is worth the extra infrastructure. For a low-risk internal tool, rolling is fine.
A healthy pipeline has a few observable properties. It's fast — most runs finish in a handful of minutes, so people actually wait for it instead of merging around it. It's trustworthy — a green build genuinely means the code is deployable, because the tests are meaningful and not routinely skipped. It's reproducible — the same commit always produces the same artifact and the same result. And it's reversible — when something does slip through (it will), you can roll back in seconds, not scramble through a manual redeploy.
When I set one of these up on a payment-critical system, the goal wasn't zero incidents — that's not realistic. The goal was making the cost of any single deploy small: small changes, shipped often, each easy to reason about and trivial to undo. A pipeline that lets you deploy ten small safe changes a day beats one that makes you batch a month of risky changes into a single terrifying release.
CI/CD automates the deployment you first do by hand — so if you haven't done a manual deploy yet, start with How to Deploy a Full-Stack Application to understand what the pipeline is automating. For the bigger picture of production readiness, see the hub: Deploying, Scaling & Architecting Full-Stack Apps.
A practical map for taking a full-stack app from a working codebase to a production system that ships reliably and scales — deployment, CI/CD, performance, and architecture choices.
A step-by-step walkthrough for deploying a full-stack app to production — environment prep, choosing hosting, deploying the database, backend, and frontend, connecting them, and going live with a custom domain and HTTPS.
A priority-ordered playbook for making web apps fast — measure Core Web Vitals first, then reduce JavaScript, optimize images, fix caching, tune the backend, and cut re-renders. Based on real high-traffic optimization work.