Advanced CI/CD Pipelines

Principles of a mature pipeline

An advanced CI/CD pipeline goes beyond just building and deploying. It integrates quality, security, testing, and progressive deployment.

graph LR
    A[Commit] --> B[Lint & Format]
    B --> C[Unit Tests]
    C --> D[Build & Security Scan]
    D --> E[Integration Tests]
    E --> F[Deploy Staging]
    F --> G[E2E Tests]
    G --> H[Deploy Canary 5%]
    H --> I{Metrics OK?}
    I -->|Yes| J[Rollout 100%]
    I -->|No| K[Automatic Rollback]

GitHub Actions: complete pipelines

Multi-environment deployment pipeline

name: Deploy Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  lint-and-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 run lint
      - run: npm run test -- --coverage

      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  security-scan:
    runs-on: ubuntu-latest
    needs: lint-and-test
    steps:
      - uses: actions/checkout@v4

      - name: Dependency audit
        run: npm audit --audit-level=high

      - name: SAST scan with Semgrep
        uses: semgrep/semgrep-action@v1
        with:
          config: p/typescript

      - name: Secret scanning
        uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

  build-and-push:
    runs-on: ubuntu-latest
    needs: [lint-and-test, security-scan]
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Image vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          exit-code: 1
          severity: CRITICAL,HIGH

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to staging
        run: |
          kubectl set image deployment/api \
            api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
            -n staging
          kubectl rollout status deployment/api -n staging --timeout=300s

  deploy-production:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Canary deployment (5%)
        run: |
          kubectl apply -f k8s/canary.yaml
          kubectl set image deployment/api-canary \
            api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
            -n production

      - name: Metrics verification (5 min)
        run: |
          sleep 300
          ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~'5..'}[5m])" | jq '.data.result[0].value[1]')
          if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
            echo "Error rate too high: $ERROR_RATE"
            kubectl rollout undo deployment/api-canary -n production
            exit 1
          fi

      - name: Full rollout
        run: |
          kubectl set image deployment/api \
            api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
            -n production
          kubectl rollout status deployment/api -n production --timeout=600s
          kubectl delete -f k8s/canary.yaml

GitLab CI/CD: advanced pipeline

stages:
  - validate
  - test
  - build
  - deploy
  - verify

variables:
  DOCKER_TLS_CERTDIR: "/certs"

.node-cache: &node-cache
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

lint:
  stage: validate
  <<: *node-cache
  script:
    - npm ci
    - npm run lint
    - npm run type-check

test:
  stage: test
  <<: *node-cache
  services:
    - postgres:16-alpine
  variables:
    POSTGRES_DB: test
    POSTGRES_PASSWORD: test
    DATABASE_URL: postgres://postgres:test@postgres:5432/test
  script:
    - npm ci
    - npm run test:ci
    - npm run test:e2e
  coverage: '/All files[^|]*\|[^|]*\s+([\d.]+)/'
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

deploy_production:
  stage: deploy
  environment:
    name: production
    url: https://api.example.com
  when: manual
  only:
    - main
  script:
    - helm upgrade --install api ./charts/api
      --set image.tag=$CI_COMMIT_SHA
      --namespace production
      --wait --timeout 10m

smoke_test:
  stage: verify
  needs: [deploy_production]
  script:
    - curl -f https://api.example.com/health || exit 1
    - npm run test:smoke

Deployment strategies

Blue/Green

Two identical environments: blue (current) and green (new). Traffic is switched all at once.

# Service pointing to the active environment
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
    version: green  # Switch between blue and green
  ports:
    - port: 3000

Canary

Progressive deployment: 5% -> 25% -> 50% -> 100%.

Feature Flags

Decouple deployment from feature activation:

// The code is deployed but the feature is disabled
if (await featureFlags.isEnabled('new-checkout', { userId })) {
  return newCheckoutFlow(order);
}
return legacyCheckoutFlow(order);

CI/CD best practices

  1. Fail fast: run the fastest steps first (lint, types)
  2. Parallelize independent jobs
  3. Cache dependencies between runs
  4. Immutable artifacts: build once, deploy everywhere
  5. Ephemeral environments for PRs (preview deployments)
  6. Automatic rollback if metrics degrade
  7. Secrets managed by CI, never in code

We use Microsoft Clarity to understand how the site is used and improve it. By continuing to browse, you accept it. You can disable it at any time.