CI/CD: What It Is and Why Your Team Needs It Yesterday
Before CI/CD, deploying code looked like this: someone copies files to a server on Friday evening, prays nothing breaks over the weekend, and spends Monday morning fixing the things that broke. If this sounds familiar, you're not alone — a lot of teams still operate this way.
CI/CD replaces manual, error-prone deployments with automated pipelines that build, test, and deploy your code every time you push a change.
Continuous Integration (CI) means every developer merges their code into the shared branch frequently — ideally multiple times per day. Each merge triggers an automated process that builds the code and runs tests. If anything fails, the team knows within minutes.
Continuous Delivery (CD) extends this: code that passes CI is automatically packaged and ready to deploy. Actual deployment to production may still require someone clicking a button.
Continuous Deployment goes further — every change that passes the pipeline goes straight to production. No human approval. This requires excellent test coverage and monitoring, but companies like Netflix deploy thousands of times per day this way.
The payoff is real. The DORA (DevOps Research and Assessment) metrics consistently show that high-performing teams deploy 208 times more frequently than low performers, with 106 times faster lead time and 7 times lower change failure rate. CI/CD is the infrastructure that makes those numbers possible.
GitHub Actions: How It Works
GitHub Actions is CI/CD built directly into GitHub — no separate service to manage. It's free for public repos and includes 2,000 minutes/month for private repos on the free tier.
The mental model:
- Workflow — A YAML file in
.github/workflows/that defines your automation - Trigger (event) — What kicks off the workflow: a push, a pull request, a schedule, or a manual button
- Job — A collection of steps that run on a single virtual machine (runner)
- Step — An individual command (
npm test) or a reusable action (actions/checkout@v4) - Runner — The virtual machine that executes your job. GitHub provides Ubuntu, Windows, and macOS runners.
The YAML syntax takes some getting used to, but once you understand the hierarchy — workflow contains jobs, jobs contain steps — it clicks quickly. And the GitHub Marketplace has thousands of pre-built actions for common tasks, so you rarely have to write everything from scratch.
Your First Working CI Pipeline
Create a file at .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run tests
run: npm test
- name: Build
run: npm run buildPush this file and GitHub immediately starts running it. Every future push to main and every PR targeting main will trigger this workflow. If any step fails, the workflow fails and your PR shows a red X.
A few notes on what's happening: npm ci is used instead of npm install because it's faster and reproducible (it uses the exact versions from package-lock.json). The cache: 'npm' option on setup-node caches the npm cache directory between runs so you're not downloading packages from scratch every time.
Managing Secrets and Environment Variables
Your pipeline will need API keys, database URLs, and deployment tokens. Never put these in your workflow file — they'd be visible to anyone who can see your repository.
Adding secrets: Go to your repository Settings, then Secrets and Variables, then Actions. Click "New repository secret." Secrets are encrypted at rest and masked in logs.
Using them in workflows:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: ./deploy.sh
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Important security details:
- Secrets from forks are not available in PR workflows. This prevents malicious PRs from external contributors from stealing your credentials.
- Secrets are automatically redacted from workflow logs. If your secret is
abc123and your script prints it, the log shows***instead. - For different environments (staging vs production), use GitHub's Environments feature. You can require manual approvals before production deployments.
One gotcha: if your secret contains special characters, you may need to quote it in shell commands. And never echo secrets for debugging — even masked, it's a bad habit to develop.
Matrix Builds: Testing Across Multiple Configurations
You want to verify your code works on Node 18, 20, and 22. And on both Ubuntu and Windows. Instead of writing separate jobs, use a matrix strategy:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testThis creates 6 parallel jobs (2 operating systems x 3 Node versions). All six run simultaneously, so the total time is roughly the time of the slowest single job — not six times longer.
You can also structure independent jobs to run in parallel and then gate later jobs on their success:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run buildLint and test run simultaneously. Build only starts if both pass. This parallelism can cut your pipeline time in half.
Deploying to Production
Here's a real deployment workflow I've used for Next.js apps on Vercel (adapt for your platform):
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
- run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'The environment: production setting enables environment protection rules — you can require manual approval before deploy runs, add wait timers, or restrict which branches can deploy.
Other deployment targets have their own actions:
- AWS (S3, ECS, Lambda):
aws-actions/configure-aws-credentials+ AWS CLI - Docker Hub:
docker/login-action+docker/build-push-action - Firebase:
FirebaseExtended/action-hosting-deploy - Any server with SSH access:
appleboy/ssh-action
Caching, Speed, and Keeping Pipelines Fast
A pipeline that takes 20 minutes to run is a pipeline people avoid triggering. Speed matters.
Caching dependencies is the biggest win. The setup-node action handles this with a single line (cache: 'npm'). For custom caches:
- name: Cache Cypress binary
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
cypress-${{ runner.os }}-Cancel redundant runs. If you push three commits in quick succession, you don't need three pipeline runs. Cancel the older ones:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueSkip unnecessary runs with path filters:
on:
push:
paths:
- 'src/**'
- 'package.json'
- '.github/workflows/**'This means documentation changes, README updates, or config file tweaks won't trigger a full CI run. Saves minutes and runner costs.
Other speed tips:
- Run independent jobs in parallel (lint, test, type-check at the same time)
- Use
npm ciovernpm install— it's 2–3x faster - Add
timeout-minutes: 15to jobs to prevent runaway processes from burning your minutes
Keeping Your Pipeline Healthy Long-Term
A CI/CD pipeline is infrastructure. Like all infrastructure, it degrades if you don't maintain it.
Reliability practices:
- Pin action versions to specific tags:
actions/checkout@v4, not@main. Unpinned actions can change under you and break your pipeline overnight. - Set up Slack or email notifications for failed workflows. A failed pipeline that nobody notices defeats the purpose.
- Review third-party actions before using them. They run in your environment with access to your code and secrets.
Security practices:
- Use the
permissionskey to limit what your workflow can access. Principle of least privilege applies to CI just like production systems. - Rotate secrets regularly — especially deployment tokens
- Use OpenID Connect (OIDC) for cloud provider authentication instead of static credentials. It's more secure and doesn't require secret rotation.
Maintenance practices:
- Delete unused workflow files. Orphaned workflows are confusing.
- Monitor pipeline run times over time. If your 5-minute pipeline gradually becomes 15 minutes, investigate — it usually means dependencies grew or caching stopped working.
- Use reusable workflows (
workflow_call) to share common steps across multiple pipelines. DRY applies to CI/CD, too.
A well-maintained pipeline is one of the best investments your team can make. It catches bugs before users see them, enforces code quality standards automatically, and gives everyone confidence that merging to main means shipping to production safely. That confidence compounds over time into a genuinely faster, more reliable engineering organisation.
Ready to Take the Next Step?
Our tutorials are just the beginning. Explore our expert-led courses and certifications for hands-on, career-ready training.