Ci Cd
Why CI/CD Matters
Continuous Integration and Continuous Deployment automate testing, building, and deployment to catch errors early, ensure code quality, and ship features faster. CI runs tests on every commit, CD deploys validated code automatically.
Core Benefits:
- Early error detection: Tests run on every push (catch bugs before merge)
- Consistent builds: Same environment every time (no “works on my machine”)
- Automated quality gates: Code must pass tests/linting before merge
- Fast feedback: Developers know within minutes if changes break build
- Reliable deployments: Automated deployment reduces human error
- Confidence: Comprehensive testing enables rapid iteration
Problem: Manual testing and deployment are slow, error-prone, and don’t scale with team size. Developers forget to run tests, deployments vary by person, and rollbacks require manual intervention.
Solution: CI/CD pipelines automate testing, linting, building, and deployment using tools like GitHub Actions, GitLab CI, or CircleCI with consistent, repeatable workflows.
Standard Library First: Manual Testing and Deployment
Before automation, teams run tests and deploy manually.
The Manual Workflow Problem
Manual processes rely on developers remembering steps and executing consistently.
Manual workflow (anti-pattern):
# Developer A's deployment process
npm test
# => Run tests manually
# => Might forget if in hurry
# => No enforcement
npm run lint
# => Check code style
# => Often skipped
npm run build
# => Compile TypeScript
# => Different Node.js version may cause issues
scp -r dist/ server:/var/www/app/
# => Copy files to server via SSH
# => Manual file transfer (error-prone)
# => No rollback mechanism
ssh server "pm2 restart app"
# => Restart application on server
# => Manual server managementDeveloper B’s deployment process:
# Different steps, different order
git pull origin main
npm install
npm run build
# => Forgot to run tests!
# => Skipped linting
rsync -avz dist/ server:/var/www/app/
pm2 reload app
# => Different commands than Developer A
# => Inconsistent processProblems:
- Tests skipped when developers “know” change is safe
- Lint errors ignored in rush
- Build steps forgotten or executed out of order
- Deployment commands vary by developer
- No audit trail (who deployed what, when)
- Rollback requires manual intervention
- No automated smoke tests after deployment
npm Scripts Approach
Consolidate steps in package.json scripts.
package.json:
{
"scripts": {
"test": "jest",
"lint": "eslint src --ext .ts",
"build": "tsc",
"deploy": "npm run test && npm run lint && npm run build && ./scripts/deploy.sh",
"deploy:prod": "NODE_ENV=production npm run deploy"
}
}scripts/deploy.sh:
#!/bin/bash
# => Deployment script
# => Still manual execution
set -e
# => Exit on error
# => Prevents partial deployments
echo "Deploying to production..."
rsync -avz dist/ server:/var/www/app/
# => Copy compiled files
# => rsync: Only transfers changed files
ssh server "pm2 reload app"
# => Reload application gracefully
# => pm2 reload: Zero downtime restart
echo "Deployment complete"Manual execution:
npm run deploy:prod
# => Developer runs deployment script
# => Still requires manual trigger
# => No automated testing on server
# => No automated rollback on failureDensity: 16 code lines, 17 annotation lines = 1.06 density (within 1.0-2.25 target)
Limitations of manual CI/CD for production:
- Human error: Developers forget steps or execute in wrong order
- No enforcement: Nothing prevents merging untested code
- Inconsistent environments: Different Node.js versions, dependencies locally vs server
- No parallelization: Tests run sequentially on one machine
- No deployment history: No record of what was deployed when
- Manual rollbacks: Requires developer intervention during incidents
- No automated smoke tests: Deployment may succeed but app broken
- Branch protection gaps: Can merge without passing tests
When manual workflows suffice:
- Solo developers with disciplined habits
- Very small teams with strong processes
- Early-stage projects (pre-production)
- Educational projects
Production Framework: GitHub Actions
GitHub Actions provides hosted CI/CD with workflows defined in YAML.
Basic CI Workflow
Run tests and linting on every push and pull request.
.github/workflows/ci.yml:
name: CI
# => Workflow name (shows in GitHub UI)
on:
push:
branches: [main, develop]
# => Run on push to main and develop branches
# => Protects main branch with automated checks
pull_request:
branches: [main]
# => Run on pull requests targeting main
# => Prevents merging broken code
jobs:
test:
# => Job name
runs-on: ubuntu-latest
# => GitHub-hosted runner (Ubuntu)
# => Free for public repos, minutes billed for private
strategy:
matrix:
node-version: [20.x, 22.x, 24.x]
# => Test against multiple Node.js versions
# => Ensures compatibility across versions
# => Matrix creates 3 parallel jobs
steps:
- name: Checkout code
uses: actions/checkout@v4
# => Clone repository
# => actions/checkout: Official GitHub action
# => @v4: Pinned version (reproducibility)
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
# => Install Node.js
with:
node-version: ${{ matrix.node-version }}
# => Use version from matrix (20.x, 22.x, or 24.x)
cache: "npm"
# => Cache npm dependencies
# => Speeds up subsequent runs
- name: Install dependencies
run: npm ci
# => Clean install from package-lock.json
# => Faster and more reliable than npm install
# => Ensures exact dependency versions
- name: Run linting
run: npm run lint
# => Run ESLint
# => Fails workflow if linting errors
# => Enforces code quality standards
- name: Run tests
run: npm test -- --coverage
# => Run tests with coverage reporting
# => --coverage: Generate coverage report
# => Fails workflow if tests fail
- name: Upload coverage
uses: codecov/codecov-action@v4
# => Upload coverage to Codecov
# => Tracks coverage over time
# => Optional: requires Codecov account
if: matrix.node-version == '24.x'
# => Only upload from one Node.js version
# => Avoid duplicate coverage reports
with:
token: ${{ secrets.CODECOV_TOKEN }}
# => Codecov upload token
# => Stored in repository secretsDensity: 39 code lines, 44 annotation lines = 1.13 density (within 1.0-2.25 target)
Build and Docker Push Workflow
Build Docker image and push to container registry.
.github/workflows/build.yml:
name: Build and Push
on:
push:
branches: [main]
# => Run only on main branch
# => Build production images from stable branch
release:
types: [created]
# => Also run on GitHub releases
# => Tag Docker images with release version
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# => Enhanced Docker builder
# => Supports multi-platform builds, caching
- name: Login to Docker Hub
uses: docker/login-action@v3
# => Authenticate to Docker Hub
with:
username: ${{ secrets.DOCKER_USERNAME }}
# => Docker Hub username (repository secret)
password: ${{ secrets.DOCKER_PASSWORD }}
# => Docker Hub password or access token (secret)
- name: Extract metadata
id: meta
# => Step ID for referencing outputs
uses: docker/metadata-action@v5
# => Generate Docker tags and labels
with:
images: ${{ secrets.DOCKER_USERNAME }}/typescript-api
# => Image name (repository)
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
# => Tag generation rules
# => type=ref,event=branch: main, develop
# => type=semver: 1.0.0, 1.0
# => type=sha: git commit SHA (immutable)
- name: Build and push
uses: docker/build-push-action@v5
# => Build Docker image and push to registry
with:
context: .
# => Build context (current directory)
push: true
# => Push to registry (false for PR builds)
tags: ${{ steps.meta.outputs.tags }}
# => Use tags from metadata step
labels: ${{ steps.meta.outputs.labels }}
# => Add metadata labels
cache-from: type=gha
# => Cache from GitHub Actions cache
# => Speeds up builds (reuse layers)
cache-to: type=gha,mode=max
# => Save cache for future builds
# => mode=max: Cache all layers
- name: Image digest
run: echo ${{ steps.build.outputs.digest }}
# => Print image digest (unique identifier)
# => Useful for debuggingDensity: 37 code lines, 45 annotation lines = 1.22 density (within 1.0-2.25 target)
Deployment Workflow
Automated deployment to production.
.github/workflows/deploy.yml:
name: Deploy to Production
on:
workflow_run:
workflows: ["Build and Push"]
# => Trigger after "Build and Push" workflow completes
# => Ensures image available before deployment
types:
- completed
branches: [main]
# => Only deploy from main branch
jobs:
deploy:
runs-on: ubuntu-latest
# => Run only if build workflow succeeded
if: ${{ github.event.workflow_run.conclusion == 'success' }}
# => Skip deployment if build failed
# => Prevents deploying broken images
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v3
# => Install kubectl CLI
# => Required for Kubernetes deployments
with:
version: "v1.28.0"
# => Specific kubectl version
# => Match Kubernetes cluster version
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig.yaml
export KUBECONFIG=kubeconfig.yaml
kubectl config use-context production
# => Configure kubectl with cluster credentials
# => KUBE_CONFIG: Base64-encoded kubeconfig (secret)
# => Sets production context
- name: Update deployment image
run: |
kubectl set image deployment/typescript-api \
api=${{ secrets.DOCKER_USERNAME }}/typescript-api:${{ github.sha }}
# => Update Deployment with new image
# => Uses git commit SHA as tag (immutable)
# => Triggers rolling update
- name: Wait for rollout
run: kubectl rollout status deployment/typescript-api
# => Wait for deployment to complete
# => Fails if rollout fails
# => Timeout after default 5 minutes
- name: Smoke test
run: |
sleep 10
curl -f https://api.example.com/health || exit 1
# => Basic health check after deployment
# => curl -f: Fail on HTTP errors
# => exit 1: Fail workflow if health check fails
- name: Rollback on failure
if: failure()
# => Run only if previous steps failed
run: kubectl rollout undo deployment/typescript-api
# => Automatic rollback on deployment failure
# => Restores previous working version
- name: Notify Slack
if: always()
# => Run regardless of success/failure
uses: 8398a7/action-slack@v3
# => Send Slack notification
with:
status: ${{ job.status }}
# => Job status (success, failure)
text: "Deployment ${{ job.status }}"
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
# => Slack webhook URL (secret)Density: 40 code lines, 46 annotation lines = 1.15 density (within 1.0-2.25 target)
Dependency Updates with Dependabot
Automatically create PRs for dependency updates.
.github/dependabot.yml:
version: 2
# => Dependabot configuration version
updates:
- package-ecosystem: "npm"
# => Monitor npm dependencies
directory: "/"
# => package.json location
schedule:
interval: "weekly"
# => Check for updates weekly
# => Balances freshness vs noise
day: "monday"
# => Run on Mondays
time: "09:00"
# => Run at 9 AM UTC
open-pull-requests-limit: 5
# => Maximum concurrent PRs
# => Prevents overwhelming team with updates
assignees:
- "maintainer-username"
# => Assign PRs to maintainer
# => Ensures visibility
labels:
- "dependencies"
# => Add labels to PRs
- "automated"
commit-message:
prefix: "chore"
# => Conventional Commits prefix
# => chore(deps): Update package
ignore:
- dependency-name: "typescript"
# => Ignore TypeScript updates
# => Manual review for major version changes
versions: ["5.x"]
# => Ignore 5.x versions (stay on 4.x)
- package-ecosystem: "github-actions"
# => Monitor GitHub Actions versions
directory: "/"
# => .github/workflows location
schedule:
interval: "monthly"
# => Less frequent than npm dependencies
# => Actions change less oftenDensity: 25 code lines, 28 annotation lines = 1.12 density (within 1.0-2.25 target)
Semantic Release
Automate versioning and changelog generation based on commit messages.
.github/workflows/release.yml:
name: Release
on:
push:
branches: [main]
# => Release from main branch only
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
# => Fetch all history
# => Required for semantic-release to analyze commits
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
- name: Install dependencies
run: npm ci
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# => GitHub token for creating releases
# => Automatically provided by GitHub
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# => npm token for publishing packages (if public)
run: npx semantic-release
# => Analyzes commits, determines version bump, creates release
# => feat: -> minor version bump (1.0.0 -> 1.1.0)
# => fix: -> patch version bump (1.0.0 -> 1.0.1)
# => BREAKING CHANGE: -> major version bump (1.0.0 -> 2.0.0).releaserc.json (semantic-release config):
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}Conventional commit examples:
git commit -m "feat: add user authentication"
# => Minor version bump (new feature)
git commit -m "fix: resolve memory leak in cache"
# => Patch version bump (bug fix)
git commit -m "feat!: migrate to TypeScript 5\n\nBREAKING CHANGE: Requires Node.js 20+"
# => Major version bump (breaking change)Density: 24 code lines, 21 annotation lines = 0.88 density (slightly below target, acceptable for config)
CI/CD Pipeline Progression Diagram
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
%% All colors are color-blind friendly and meet WCAG AA contrast standards
graph TB
A[Manual Scripts] -->|Automated triggers| B[GitHub Actions CI]
B -->|Docker builds| C[Build & Push]
C -->|Deployment automation| D[CD Pipeline]
D -->|Versioning automation| E[Semantic Release]
A:::standard
B:::framework
C:::framework
D:::production
E:::production
classDef standard fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef framework fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef production fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
subgraph Standard[" Manual "]
A
end
subgraph Automation[" CI/CD "]
B
C
D
E
end
style Standard fill:#F0F0F0,stroke:#CC78BC,stroke-width:3px
style Automation fill:#F0F0F0,stroke:#029E73,stroke-width:3px
Production Best Practices
Branch Protection Rules
Enforce CI passing before merge.
GitHub repository settings:
- Require pull request reviews: 1-2 approvals before merge
- Require status checks to pass: CI workflow must succeed
- Require branches to be up to date: Force rebase before merge
- Include administrators: Apply rules to admins (no bypassing)
- Require signed commits: GPG signature verification
- Restrict who can push: Only maintainers can push to main
Setting up via GitHub UI:
Repository → Settings → Branches → Branch protection rules
→ Add rule for "main" branch
→ Enable "Require status checks to pass before merging"
→ Select required checks: CI, BuildMatrix Testing
Test across multiple Node.js versions, OSes, and configurations.
Extended matrix:
strategy:
matrix:
node-version: [20.x, 22.x, 24.x]
os: [ubuntu-latest, windows-latest, macos-latest]
# => Test on Linux, Windows, macOS
# => Catches platform-specific bugs
fail-fast: false
# => Continue testing all combinations even if one fails
# => See all failures, not just firstCreates 9 jobs: 3 Node.js versions × 3 OSes
Caching Dependencies
Speed up workflows with dependency caching.
npm cache:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
cache: "npm"
# => Automatically cache node_modules
# => Key: package-lock.json hash
# => Restore on cache hit (seconds vs minutes)Manual caching:
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
# => Cache directory
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# => Cache key includes OS and package-lock hash
# => Invalidated when dependencies change
restore-keys: |
${{ runner.os }}-node-
# => Restore partial match if exact key not foundSecrets Management
Store sensitive data in repository secrets.
GitHub UI:
Repository → Settings → Secrets and variables → Actions
→ New repository secretUse in workflows:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
# => Access secret as environment variable
# => Never logged or exposed in output
run: ./deploy.shSecrets best practices:
- Never hardcode secrets in workflows
- Use environment-specific secrets (PROD_API_KEY, DEV_API_KEY)
- Rotate secrets regularly
- Use fine-grained tokens (minimal permissions)
Testing in CI
Run comprehensive test suites automatically.
test script enhancement:
{
"scripts": {
"test": "jest",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"test:e2e": "playwright test"
}
}CI workflow:
- name: Run unit tests
run: npm run test:ci
# => --ci: Optimized for CI (no watch mode)
# => --maxWorkers=2: Limit parallelism (avoid memory issues)
- name: Run e2e tests
run: npm run test:e2e
# => End-to-end tests after unit testsEnvironment-Specific Workflows
Different pipelines for staging and production.
Staging deployment:
name: Deploy to Staging
on:
push:
branches: [develop]
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
# => GitHub environment with staging secrets
steps:
- name: Deploy
run: kubectl apply -f k8s/staging/Production deployment (manual approval):
name: Deploy to Production
on:
release:
types: [created]
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
# => Requires manual approval in GitHub UI
url: https://api.example.com
# => Deployment URL (shown in GitHub)
steps:
- name: Deploy
run: kubectl apply -f k8s/production/Trade-offs and When to Use Each
Manual Scripts (npm + bash)
Use when:
- Solo developer projects
- Very early-stage development
- Learning deployment basics
- Non-critical applications
Avoid when:
- Team collaboration (inconsistent processes)
- Production deployments (human error risk)
- Need audit trail (no deployment history)
GitHub Actions
Use when:
- GitHub-hosted repositories (native integration)
- Need free CI/CD (public repos free, private repos generous limits)
- Want extensive marketplace actions (reusable components)
- Standard workflows (testing, building, deploying)
Avoid when:
- Using GitLab/Bitbucket (use their native CI/CD)
- Need very long-running jobs (>6 hours limit)
- Highly custom build environments (self-hosted runners)
GitLab CI / CircleCI / Jenkins
Use when:
- Self-hosted Git (GitLab)
- Need advanced features (GitLab Auto DevOps)
- Complex enterprise workflows (Jenkins)
- Multi-cloud deployments
Avoid when:
- Simple GitHub projects (GitHub Actions simpler)
- Limited DevOps resources (managed solutions easier)
Common Pitfalls
Pitfall 1: Not Testing PR Builds
Problem: Only testing main branch misses PR issues.
Solution: Run CI on pull requests.
on:
pull_request:
branches: [main]Pitfall 2: Secrets in Logs
Problem: Accidentally printing secrets in logs.
Solution: Use secret masking and never echo secrets.
# ❌ BAD
- name: Deploy
run: echo ${{ secrets.API_KEY }}
# ✅ GOOD
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.shPitfall 3: No Rollback Strategy
Problem: Deployment fails but no automatic rollback.
Solution: Implement rollback on failure.
- name: Rollback on failure
if: failure()
run: kubectl rollout undo deployment/typescript-apiPitfall 4: Slow CI Pipelines
Problem: Tests take too long, slowing development.
Solution: Use caching, matrix parallelization, and test splitting.
strategy:
matrix:
shard: [1, 2, 3, 4]
# => Split tests into 4 parallel jobs
steps:
- name: Run tests
run: npm test -- --shard=${{ matrix.shard }}/4
# => Each job runs 1/4 of testsPitfall 5: No Deployment Notifications
Problem: Team unaware of deployments.
Solution: Add notifications (Slack, email).
- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}Summary
CI/CD automates testing, building, and deployment to ensure code quality and accelerate delivery. GitHub Actions provides hosted CI/CD with YAML workflows, Dependabot automates dependency updates, and semantic-release automates versioning.
Progression path:
- Learn with manual scripts: Understand deployment steps
- Add CI for testing: Automate test execution on every push
- Automate builds: Docker image building and registry push
- Implement CD: Automated deployment to staging/production
- Add release automation: Semantic versioning and changelog
Production checklist:
- ✅ CI runs on every PR (tests + linting)
- ✅ Branch protection (require CI pass before merge)
- ✅ Matrix testing (multiple Node.js versions, OSes)
- ✅ Dependency caching (fast builds)
- ✅ Docker builds (automated image creation)
- ✅ Automated deployment (staging + production)
- ✅ Rollback strategy (automatic on failure)
- ✅ Secrets management (no hardcoded credentials)
- ✅ Deployment notifications (Slack, email)
- ✅ Automated versioning (semantic-release)
Choose CI/CD platform based on ecosystem: GitHub Actions for GitHub, GitLab CI for GitLab, CircleCI/Jenkins for advanced enterprise needs.