CI/CD Pipelines: Complete Guide
A comprehensive guide to CI/CD pipelines, covering tools, patterns, and best practices for automating deployments.
CI/CD Pipelines: The Complete Guide
Continuous Integration and Continuous Deployment (CI/CD) has become essential for modern software development. By automating the build, test, and deployment process, teams can ship code faster, more frequently, and with higher confidence.
This comprehensive guide covers everything you need to know about CI/CD pipelines, from basic concepts to advanced patterns.
What are CI and CD?
Continuous Integration (CI)
CI is the practice of merging all developer copies to a shared mainline several times a day. Each merge triggers an automated build and test sequence to detect integration errors as early as possible.
Key Principles:
- Automate builds - No manual compilation steps
- Run tests automatically - Every commit triggers test suite
- Fast feedback - Developers know within minutes if build fails
- Maintain clean repository - Always keep mainline deployable
Continuous Deployment (CD)
CD is the practice of having code deployed to production at all times, typically through automated pipelines.
Key Principles:
- Automate deployment - Zero manual steps
- One-click deploy - Deploy on approval or automatic
- Rollback capability - Instant revert if issues occur
- Environment parity - Dev/staging/production environments match
CI/CD Workflow
┌─────────────────────────────────────────┐
│ Developer Workflow │
├─────────────────────────────────────────┤
│ │
│ 1. Create Branch │
│ git checkout -b feature/new │
│ │
│ 2. Make Changes │
│ Edit code │
│ Write tests │
│ │
│ 3. Commit Changes │
│ git add . │
│ git commit -m "Add feature" │
│ │
│ 4. Push to Remote │
│ git push origin feature/new │
│ │
├───────────────┬─────────────────────┤
│ ▼ │
│ ┌─────────────────┐ │
│ │ CI Pipeline │ │
│ ├─────────────────┤ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ Build │ │ │
│ │ │ - Compile │ │ │
│ │ │ - Lint │ │ │
│ │ │ - Test │ │ │
│ │ │ - Package │ │ │
│ │ └───────────┘ │ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ Deploy │ │ │
│ │ │ - Staging │ │ │
│ │ │ - Production│ │ │
│ │ └───────────┘ │ │
│ └─────────────────┘ │
│ │
├───────────────┴─────────────────────┤
│ ▼ │
│ ┌─────────────────┐ │
│ │ Production │ │
│ │ Application │ │
│ └─────────────────┘ │
└─────────────────────────────────────────┘
Popular CI/CD Tools
GitHub Actions
Basic Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Multi-Stage Pipeline
# .github/workflows/ci-cd.yml
name: CI/CD
on:
push:
branches: [main]
workflow_dispatch: # Manual trigger
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage
path: coverage/
build:
name: Build
needs: test # Runs after test job succeeds
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: build
path: dist/
deploy-staging:
name: Deploy to Staging
needs: build
runs-on: ubuntu-latest
environment: staging # Requires approval
steps:
- uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v3
with:
name: build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: ./vercel
deploy-production:
name: Deploy to Production
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v3
with:
name: build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
GitLab CI/CD
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
NODE_VERSION: "18"
CACHE_KEY: "$CI_COMMIT_REF_SLUG"
test:
stage: test
image: node:$NODE_VERSION
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
script:
- npm ci
- npm test -- --coverage
coverage: '/Coverage: \d+\.\d+\%/'
build:
stage: build
image: node:$NODE_VERSION
dependencies:
- test # Only runs if test passes
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy_staging:
stage: deploy
image: node:$NODE_VERSION
dependencies:
- build
only:
- main
when: manual # Requires manual trigger
script:
- npm install -g vercel
- vercel --token=$VERCEL_TOKEN --yes --prod=false
deploy_production:
stage: deploy
image: node:$NODE_VERSION
dependencies:
- build
- deploy_staging
only:
- main
when: manual
script:
- npm install -g vercel
- vercel --token=$VERCEL_TOKEN --yes --prod
CircleCI
# .circleci/config.yml
version: 2.1
jobs:
test:
docker:
- image: cimg/node:18.18
steps:
- checkout
- restore_cache:
keys:
- node-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- save_cache:
keys:
- node-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run: npm test -- --coverage
- store_artifacts:
path: coverage/
build:
docker:
- image: cimg/node:18.18
steps:
- checkout
- restore_cache:
keys:
- node-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- run: npm run build
- store_artifacts:
path: dist/
deploy:
docker:
- image: cimg/node:18.18
steps:
- checkout
- attach_workspace:
at: dist
- run:
name: Deploy to Heroku
command: |
git remote add heroku https://git.heroku.com/myapp.git
git push heroku HEAD:main --force
workflows:
version: 2
test-build-deploy:
jobs:
- test
- build
- deploy
Build Strategies
Build Caching
# GitHub Actions with caching
steps:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm' # Caches node_modules
- name: Get npm cache directory
id: npm-cache-dir-path
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Cache node modules
uses: actions/cache@v3
id: npm-cache
with:
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
Parallel Jobs
# Run jobs in parallel
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run linter
run: npm run lint
test-unit:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run unit tests
run: npm test --unit
test-integration:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run integration tests
run: npm test --integration
build:
name: Build
needs: [lint, test-unit, test-integration] # Runs after all three jobs complete
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: npm run build
Matrix Builds
# Build and test across multiple environments
jobs:
test:
name: Test (${{ matrix.os }} - Node ${{ matrix.node-version }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [16, 18, 20]
fail-fast: false # Continue testing even if one fails
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload results
if: failure()
uses: actions/upload-artifact@v3
with:
name: test-results-${{ matrix.os }}-node-${{ matrix.node-version }}
path: test-results/
Deployment Strategies
Blue-Green Deployment
# Deploy to blue and green environments
deploy_blue:
runs-on: ubuntu-latest
environment:
name: blue
url: https://blue.example.com
steps:
- uses: actions/checkout@v3
- name: Deploy to Blue
run: |
npm run build
aws s3 sync dist/ s3://blue-bucket/
aws cloudfront create-invalidation --distribution-id BLUE_DISTRIBUTION_ID --paths /*
deploy_green:
runs-on: ubuntu-latest
needs: deploy_blue
environment:
name: green
url: https://green.example.com
steps:
- uses: actions/checkout@v3
- name: Deploy to Green
run: |
npm run build
aws s3 sync dist/ s3://green-bucket/
aws cloudfront create-invalidation --distribution-id GREEN_DISTRIBUTION_ID --paths /*
switch_traffic:
runs-on: ubuntu-latest
needs: deploy_green
steps:
- name: Switch traffic to Green
run: |
aws route53 change-resource-record-sets \
--hosted-zone-id example.com \
--change-batch file://traffic-switch.json
Canary Deployment
# Gradual rollout to production
deploy_canary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy canary (10%)
run: |
npm run build
aws s3 sync dist/ s3://production-bucket/canary/
kubectl patch deployment myapp -p '{"spec":{"replicas":2}}' # 2 pods for 10% traffic
monitor_canary:
runs-on: ubuntu-latest
needs: deploy_canary
steps:
- name: Monitor canary metrics
run: |
node scripts/monitor-canary.js --duration=300 # Monitor for 5 minutes
deploy_full:
runs-on: ubuntu-latest
needs: monitor_canary
if: success() # Only runs if monitoring succeeds
steps:
- uses: actions/checkout@v3
- name: Deploy to production (100%)
run: |
npm run build
aws s3 sync dist/ s3://production-bucket/
kubectl patch deployment myapp -p '{"spec":{"replicas":20}}' # 20 pods for 100% traffic
Rolling Update
# Kubernetes rolling update
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker push myapp:${{ github.sha }}
- name: Deploy with rolling update
run: |
kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}
kubectl rollout status deployment/myapp --watch=true
Quality Gates
Code Quality Checks
steps:
- name: Lint code
run: npm run lint
- name: Check code style
run: npm run format:check
- name: Type check
run: npm run type-check
- name: Security audit
run: npm audit --audit-level=moderate
- name: Dependency check
run: npm outdated
Test Coverage
steps:
- name: Run tests with coverage
run: npm test -- --coverage
- name: Check coverage threshold
run: |
if [ $(cat coverage/coverage-summary.json | jq '.total.lines.pct') -lt 80 ]; then
echo "Coverage below 80%"
exit 1
fi
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
fail_ci_if_error: true
Performance Budgets
steps:
- name: Build application
run: npm run build
- name: Check bundle size
run: |
npm run build:analyze
- name: Compare with budget
run: |
BUDGET_SIZE=244000 # 244KB
ACTUAL_SIZE=$(wc -c dist/bundle.js | awk '{print $1}')
if [ $ACTUAL_SIZE -gt $BUDGET_SIZE ]; then
echo "Bundle size exceeds budget"
exit 1
fi
Best Practices
1. Keep Pipelines Fast
# ✅ Good: Parallel jobs
jobs:
lint-test-build:
strategy:
matrix:
task: [lint, test, build]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run ${{ matrix.task }}
run: npm run ${{ matrix.task }}
# ❌ Bad: Sequential jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Lint
run: npm run lint
test:
needs: lint # Waits for lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Test
run: npm run test
build:
needs: test # Waits for test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build
run: npm run build
2. Use Caching Effectively
steps:
- name: Cache node modules
uses: actions/cache@v3
id: cache-node
with:
path: |
node_modules
~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
3. Fail Fast
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [16, 18]
fail-fast: true # Stop all jobs if one fails
steps:
- name: Test
run: npm test
4. Environment Secrets
# ✅ Good: Use secrets
steps:
- name: Deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync dist/ s3://my-bucket/
# ❌ Bad: Hardcoded secrets
steps:
- name: Deploy
env:
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
run: |
aws s3 sync dist/ s3://my-bucket/
5. Artifact Management
steps:
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-output
path: |
dist/
build-logs/
retention-days: 30 # Keep for 30 days
- name: Upload test results
if: always() # Upload even if tests fail
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results/
if-no-files-found: ignore
Monitoring and Notifications
Status Checks
# .github/workflows/status.yml
name: Status Check
on:
schedule:
# Run every 15 minutes
- cron: '*/15 * * * *'
jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check website status
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://example.com)
if [ $STATUS -ne 200 ]; then
echo "Website is down"
exit 1
fi
Notifications
# Send Slack notifications on deployment
steps:
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
Deployment ${{ job.status }} for ${{ github.sha }}
Branch: ${{ github.ref }}
Author: ${{ github.actor }}
webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
if: always() # Send on both success and failure
Conclusion
CI/CD pipelines are the backbone of modern software development. By implementing comprehensive automation, you can ship code faster, with higher confidence, and reduce manual errors.
Key Takeaways
- Automate Everything - Build, test, deploy should all be automated
- Parallelize Where Possible - Run jobs concurrently to speed up pipelines
- Use Caching - Cache dependencies and build artifacts
- Implement Quality Gates - Lint, test coverage, security checks
- Choose Right Deployment Strategy - Blue-green, canary, rolling updates
- Monitor Pipelines - Get notified of failures quickly
- Secure Secrets - Never hardcode credentials
Next Steps
- Assess your current build and deploy process
- Choose a CI/CD platform (GitHub Actions, GitLab CI, CircleCI)
- Create your first pipeline
- Add stages for build, test, and deploy
- Implement caching and parallel jobs
- Add quality gates and notifications
- Iterate and improve over time
The journey to continuous delivery starts now. Build your pipeline and start shipping faster.
Ready to automate your deployments? Start by creating a simple CI pipeline and iterate from there. Your team will thank you.