Continuous Integration and Continuous Deployment (CI/CD) pipelines automate the entire software delivery process, from code commit to production deployment. This guide covers building robust, automated pipelines that improve development velocity and code quality.
What is CI/CD?
CI/CD automates the software delivery process:
- Continuous Integration (CI): Automatically test and build code on every commit
- Continuous Deployment (CD): Automatically deploy to production after successful tests
- Benefits: Faster releases, fewer bugs, consistent deployments
GitHub Actions CI/CD
Complete Pipeline Example
# .github/workflows/deploy.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to registry
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
docker push myapp:${{ github.sha }}
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
uses: azure/webapps-deploy@v2
with:
app-name: 'myapp'
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
package: ./dist
GitLab CI/CD
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
test:
stage: test
image: node:20
script:
- npm ci
- npm run lint
- npm test
- npm run test:e2e
coverage: '/Lines\s*:\s*\d+\.\d+%/'
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
- develop
deploy_production:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache curl
- curl -X POST $DEPLOY_WEBHOOK_URL
environment:
name: production
url: https://myapp.com
only:
- main
when: manual
Pipeline Stages
1. Code Quality Checks
- Linting (ESLint, Prettier)
- Type checking (TypeScript)
- Code formatting validation
- Security scanning (Snyk, OWASP)
2. Testing
- Unit tests
- Integration tests
- E2E tests (Playwright, Cypress)
- Performance tests
- Coverage reporting
3. Building
- Compile/transpile code
- Bundle assets
- Optimize images
- Generate production builds
- Create Docker images
4. Deployment
- Deploy to staging
- Run smoke tests
- Deploy to production
- Health checks
- Rollback on failure
Advanced CI/CD Patterns
Parallel Execution
jobs:
test:
strategy:
matrix:
node-version: [18, 20]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Conditional Deployment
deploy:
if: |
github.ref == 'refs/heads/main' &&
github.event_name == 'push' &&
!contains(github.event.head_commit.message, '[skip ci]')
steps:
- name: Deploy
run: ./deploy.sh
Docker in CI/CD
# Multi-stage Dockerfile for CI/CD
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
# Build and push in CI
- name: Build and push
run: |
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:${{ github.sha }} -t myapp:latest --push .
Environment Management
- Use secrets for sensitive data
- Separate configs for dev/staging/prod
- Feature flags for gradual rollouts
- Environment-specific variables
Best Practices
- Fail fast - stop pipeline on first error
- Cache dependencies to speed up builds
- Run tests in parallel when possible
- Use matrix builds for multiple versions
- Implement proper secrets management
- Add deployment approvals for production
- Monitor pipeline performance
- Keep pipelines idempotent
Real-World Example
Complete CI/CD pipeline for a Next.js application:
- Lint and type check: 2 minutes
- Run tests (parallel): 5 minutes
- Build application: 3 minutes
- Build Docker image: 4 minutes
- Deploy to staging: 2 minutes
- Smoke tests: 1 minute
- Total: ~17 minutes from commit to staging
- Result: 10x faster releases, 90% fewer deployment errors
Conclusion
CI/CD pipelines are essential for modern software development. Automate testing, building, and deployment to improve code quality, reduce manual errors, and accelerate delivery. Start with basic pipelines and gradually add more automation as your needs grow.