Basic CI/CD works. Advanced CI/CD is fast, efficient, and cost-effective. Matrix builds test across multiple versions in parallel. Caching makes builds 10x faster. Self-hosted runners save money and give you control. In this lesson, you'll learn the patterns that optimize your pipelines for speed and scale.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Use matrix builds to test multiple versions in parallel
  • Implement dependency caching for faster builds
  • Set up self-hosted runners for cost savings
  • Optimize workflow execution with concurrency
  • Use GitHub Actions reusable workflows
  • Implement workflow security best practices

2. Why This Matters

Real-world scenario: Your CI pipeline takes 15 minutes to run. With caching, it's 3 minutes. With matrix builds, you test 6 combinations in parallel instead of sequentially. Your team ships faster, and you save money on CI minutes.

3. Core Concepts

Matrix Builds

# matrix-build.yaml
name: Matrix Build

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x, 20.x, 22.x]
        os: [ubuntu-latest, windows-latest, macos-latest]
        include:
          # Additional combinations
          - node-version: 20.x
            os: ubuntu-latest
            experimental: true
        exclude:
          # Exclude specific combinations
          - node-version: 18.x
            os: windows-latest
      fail-fast: false  # Don't cancel all jobs if one fails
      max-parallel: 4   # Run at most 4 jobs at once
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test

  # Python matrix example
  python-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12']
        django-version: ['4.2', '5.0']
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      
      - name: Install dependencies
        run: |
          pip install django==${{ matrix.django-version }}
          pip install -r requirements.txt
      
      - name: Run tests
        run: pytest
Matrix builds for multiple versions

Dependency Caching

# caching.yaml
name: Optimized with Caching

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # NPM cache
      - name: Cache npm dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
      
      # pip cache
      - name: Cache pip dependencies
        uses: actions/cache@v3
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-
      
      # Docker layers
      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-
      
      # Go modules
      - name: Cache Go modules
        uses: actions/cache@v3
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-
      
      # Rust cargo
      - name: Cache cargo
        uses: actions/cache@v3
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      
      # Build
      - name: Build
        run: |
          npm ci
          npm run build
Dependency caching strategies

Self-Hosted Runners

# Set up self-hosted runner

# 1. Create EC2 instance (t3.medium recommended)
# 2. Install Docker, Python, Node.js, etc.
# 3. Add runner

mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.308.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.308.0/actions-runner-linux-x64-2.308.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.308.0.tar.gz

# Configure (get token from GitHub org/repo settings)
./config.sh --url https://github.com/owner/repo --token TOKEN --labels production,linux,x64

# Run as service
sudo ./svc.sh install
sudo ./svc.sh start

# Check status
sudo ./svc.sh status
Self-hosted runner setup
# self-hosted-runner.yaml
name: Self-Hosted Runner Jobs

on: [push]

jobs:
  # Use self-hosted runner for large builds
  large-build:
    runs-on: [self-hosted, linux, x64]
    steps:
      - uses: actions/checkout@v4
      - name: Build large application
        run: make build
      - name: Run heavy tests
        run: make test-heavy
  
  # Mix of self-hosted and GitHub-hosted
  hybrid:
    runs-on: ubuntu-latest
    steps:
      - name: Quick test
        run: npm test
      
      - name: Deploy
        runs-on: [self-hosted, production]
        run: ./deploy.sh
Using self-hosted runners in workflows

Workflow Optimization

# optimization.yaml
name: Optimized Workflow

on: [push]

# Concurrency - cancel in-progress runs on same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

# Conditional job execution
jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      backend: ${{ steps.filter.outputs.backend }}
      frontend: ${{ steps.filter.outputs.frontend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            backend:
              - 'backend/**'
              - 'shared/**'
            frontend:
              - 'frontend/**'
  
  backend-test:
    needs: changes
    if: needs.changes.outputs.backend == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Running backend tests"
  
  frontend-test:
    needs: changes
    if: needs.changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Running frontend tests"

  # Job timeout
  long-job:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
      - run: ./long-script.sh

  # Retry on failure
  flaky-test:
    runs-on: ubuntu-latest
    steps:
      - name: Flaky test
        uses: nick-fields/retry@v2
        with:
          timeout_minutes: 10
          max_attempts: 3
          command: npm run test:flaky
Workflow optimization techniques

Reusable Workflows

# .github/workflows/reusable-test.yml
name: Reusable Test Workflow

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20.x'
      test-command:
        required: true
        type: string
    secrets:
      API_KEY:
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
      - run: ${{ inputs.test-command }}
        env:
          API_KEY: ${{ secrets.API_KEY }}

# Using the reusable workflow
# .github/workflows/ci.yml
name: CI

on: [push]

jobs:
  test-node-18:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '18.x'
      test-command: 'npm run test:unit'
  
  test-node-20:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '20.x'
      test-command: 'npm run test:integration'
    secrets:
      API_KEY: ${{ secrets.API_KEY }}

  test-node-22:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '22.x'
      test-command: 'npm run test:e2e'
Reusable workflows

4. Complete Project: Production-Ready Pipeline

# .github/workflows/production-ready.yml
name: Production-Ready Pipeline

on:
  push:
    branches: [main, develop]
    tags: ['v*']
  pull_request:
    branches: [main]
  workflow_dispatch:

env:
  NODE_VERSION: 20.x
  CACHE_KEY_PREFIX: v1

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      app: ${{ steps.filter.outputs.app }}
      api: ${{ steps.filter.outputs.api }}
      infra: ${{ steps.filter.outputs.infra }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            app:
              - 'app/**'
            api:
              - 'api/**'
            infra:
              - 'terraform/**'

  lint-and-test:
    runs-on: ubuntu-latest
    if: needs.changes.outputs.app == 'true' || needs.changes.outputs.api == 'true'
    needs: changes
    steps:
      - uses: actions/checkout@v4
      
      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-npm-
      
      - name: Install dependencies
        run: npm ci
      
      - name: Lint
        run: npm run lint
      
      - name: Type check
        run: npm run type-check
      
      - name: Unit tests
        run: npm run test:unit -- --coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

  integration-tests:
    runs-on: ubuntu-latest
    needs: lint-and-test
    if: github.event_name != 'pull_request'
    steps:
      - uses: actions/checkout@v4
      
      - name: Start services
        run: docker-compose -f docker-compose.test.yml up -d
      
      - name: Wait for services
        run: sleep 10
      
      - name: Run integration tests
        run: npm run test:integration
      
      - name: Stop services
        run: docker-compose -f docker-compose.test.yml down

  e2e-tests:
    runs-on: ubuntu-latest
    needs: integration-tests
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      
      - name: Build and run
        run: |
          docker build -t myapp:e2e .
          docker run -d -p 3000:3000 --name e2e-app myapp:e2e
      
      - name: Run Playwright tests
        uses: microsoft/playwright-github-action@v1
      
      - name: Run E2E tests
        run: npm run test:e2e
      
      - name: Upload screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-screenshots
          path: test-results/

  performance-tests:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      
      - name: Run k6 load tests
        uses: grafana/k6-action@v0.3.0
        with:
          filename: tests/performance/load-test.js
          flags: --out json=results.json
      
      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: performance-results
          path: results.json

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Run CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: javascript, typescript
      
      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2

  build:
    runs-on: ubuntu-latest
    needs: [lint-and-test, security-scan]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    environment: staging
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy to staging
        run: |
          kubectl set image deployment/myapp myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
          kubectl rollout status deployment/myapp

  deploy-production:
    runs-on: ubuntu-latest
    needs: [deploy-staging, performance-tests, e2e-tests]
    environment:
      name: production
      url: https://myapp.example.com
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Approve deployment
        uses: trstringer/manual-approval@v1
        with:
          secret: ${{ github.TOKEN }}
          approvers: admin,lead-engineer
      
      - name: Deploy to production
        run: |
          kubectl set image deployment/myapp myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
          kubectl rollout status deployment/myapp

  notify:
    runs-on: ubuntu-latest
    needs: [deploy-staging, deploy-production]
    if: always()
    steps:
      - name: Slack notification
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "Pipeline ${{ job.status }}: ${{ github.repository }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Complete production-ready pipeline

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next category: Terraform & IaC - Advanced Topics (Lessons 8.2-8.6)

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 17, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment