Continuous Integration and Continuous Deployment transform code changes into running software automatically. Done well, you push code and forget about it — the pipeline handles testing, building, and deploying. Done poorly, you spend more time fighting the pipeline than writing code.

The Pipeline Stages

CommitBuildTestSecurityArtifactDeployVerify

Each stage is a gate. Fail any stage, stop the pipeline.

Build

Compile code, resolve dependencies, create artifacts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# GitHub Actions
build:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build
      run: npm run build
    
    - name: Upload artifact
      uses: actions/upload-artifact@v4
      with:
        name: build
        path: dist/

Test

Run the test suite. Fast feedback is critical:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
test:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    
    - name: Run unit tests
      run: npm test
    
    - name: Run integration tests
      run: npm run test:integration
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3

Parallelize when possible:

1
2
3
4
5
6
test:
  strategy:
    matrix:
      test-group: [unit, integration, e2e]
  steps:
    - run: npm run test:${{ matrix.test-group }}

Security Scanning

Catch vulnerabilities before production:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
security:
  needs: build
  steps:
    - name: Dependency audit
      run: npm audit --audit-level=high
    
    - name: SAST scan
      uses: github/codeql-action/analyze@v2
    
    - name: Container scan
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: myapp:${{ github.sha }}
        severity: 'HIGH,CRITICAL'

Deploy

Push to environment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
deploy-staging:
  needs: [test, security]
  environment: staging
  steps:
    - name: Deploy to staging
      run: |
        kubectl set image deployment/myapp \
          myapp=myregistry/myapp:${{ github.sha }}

deploy-production:
  needs: deploy-staging
  environment: production
  steps:
    - name: Deploy to production
      run: |
        kubectl set image deployment/myapp \
          myapp=myregistry/myapp:${{ github.sha }}

Environment Promotion

Code flows through environments:

feature-branchmainstagingproduction
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Different triggers for different stages
on:
  push:
    branches: [main]      # CI on every push
  
  workflow_dispatch:       # Manual production deploy

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/main'
    
  deploy-production:
    if: github.event_name == 'workflow_dispatch'
    environment:
      name: production
      url: https://myapp.com

Approval Gates

Require human approval for production:

1
2
3
4
5
6
7
8
deploy-production:
  needs: deploy-staging
  environment:
    name: production
  # GitHub requires reviewers configured on environment
  steps:
    - name: Deploy
      run: ./deploy.sh production

Configure in GitHub: Settings → Environments → production → Required reviewers

Caching for Speed

Slow pipelines kill productivity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('package-lock.json') }}
    restore-keys: npm-

- name: Cache build
  uses: actions/cache@v4
  with:
    path: dist
    key: build-${{ github.sha }}

Docker layer caching:

1
2
3
4
5
- name: Build and push
  uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

Secrets Management

Never hardcode secrets:

1
2
3
4
5
6
7
8
deploy:
  steps:
    - name: Configure AWS
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1

Use OIDC for cloud providers when possible — no static credentials:

1
2
3
4
5
6
7
8
9
permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions
      aws-region: us-east-1

Rollback Strategy

Deployments fail. Plan for it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
deploy:
  steps:
    - name: Deploy
      id: deploy
      run: ./deploy.sh
      
    - name: Smoke test
      id: smoke
      run: ./smoke-test.sh
      
    - name: Rollback on failure
      if: failure() && steps.deploy.outcome == 'success'
      run: ./rollback.sh

Or use deployment strategies that enable easy rollback:

1
2
3
4
5
# Blue-green: switch back to blue
- run: kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'

# Canary: scale down new version
- run: kubectl scale deployment myapp-canary --replicas=0

Branch Protection

Prevent direct pushes to main:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# .github/workflows/pr-check.yml
on:
  pull_request:
    branches: [main]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - run: npm test
      - run: npm run lint

Configure branch protection:

  • Require PR reviews
  • Require status checks to pass
  • Require branches to be up to date
  • No force pushes

Monorepo Considerations

Only build what changed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
on:
  push:
    paths:
      - 'services/api/**'
      - '.github/workflows/api.yml'

# Or use path filters
jobs:
  changes:
    outputs:
      api: ${{ steps.filter.outputs.api }}
      web: ${{ steps.filter.outputs.web }}
    steps:
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            api:
              - 'services/api/**'
            web:
              - 'services/web/**'
  
  build-api:
    needs: changes
    if: needs.changes.outputs.api == 'true'

Notifications

Know when things break:

1
2
3
4
5
6
7
8
- name: Notify on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    channel-id: 'deployments'
    slack-message: '❌ Deploy failed: ${{ github.repository }}@${{ github.sha }}'
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_TOKEN }}

Pipeline as Code

Version your pipeline alongside your code:

.giwtohrcdsukiecbf.ph/lyleomodwlyus.l/yemdl.yml###BDNueiipgllhdotylayntdotatesenksvstironments

Changes to the pipeline go through the same review process as code changes.

Common Anti-Patterns

Flaky tests: Tests that sometimes pass, sometimes fail. Fix or delete them.

Too slow: If CI takes 30 minutes, developers stop waiting for it.

Too many manual steps: If deploy requires manual intervention, it’s not CD.

No rollback plan: “We’ll figure it out” isn’t a strategy.

Secrets in logs: Mask sensitive output.

1
- run: echo "::add-mask::${{ secrets.API_KEY }}"

Metrics to Track

  • Lead time: Commit to production
  • Deploy frequency: How often you ship
  • Change failure rate: Deploys that cause incidents
  • MTTR: Time to recover from failures

These are the DORA metrics. Track them to improve.


A good CI/CD pipeline is invisible when working and informative when failing. It catches bugs before production, deploys reliably, and gives you confidence to ship frequently.

Start simple: build, test, deploy. Add security scanning, environment gates, and rollback automation as you grow. The goal is shipping code safely and often — everything else is implementation detail.