Skill

ci-patterns

Install
1
Install the plugin
$
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-github-actions

Want just this skill?

Add to a custom plugin, then install with one command.

Description

This skill should be used when implementing CI pipelines, configuring caching, setting up matrix builds, creating composite actions, or optimizing workflow performance.

Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

CI Patterns for GitHub Actions

This skill covers advanced continuous integration patterns that improve build speed, reliability, and maintainability. These patterns are essential for scaling CI systems across large projects and teams.

Existing Repository Compatibility

When implementing CI patterns in existing repositories, careful analysis is required:

Assessment Process:

  1. Review existing workflow execution times and identify bottlenecks
  2. Check current caching strategy (if any) and measure hit rates
  3. Analyze matrix build configurations and success/failure patterns
  4. Identify duplicated step sequences across multiple workflows
  5. Review artifact usage and retention policies
  6. Check for existing composite actions or reusable workflows
  7. Measure current CI costs and resource usage

Compatibility Considerations:

  • Preserve existing cache keys unless you have a migration plan
  • Test new caching strategies on a feature branch before main
  • Don't break existing matrix configurations without team coordination
  • Ensure new composite actions are backwards compatible
  • Maintain existing artifact naming conventions for dependent workflows
  • Keep workflow run times stable during optimization (avoid regressions)
  • Document all changes to CI patterns in pull requests

Migration Strategies:

  • Introduce new patterns alongside old ones initially
  • Use feature flags or branch conditions to test new approaches
  • Migrate one workflow at a time to reduce risk
  • Monitor metrics before and after changes
  • Have rollback plans for performance-critical workflows
  • Communicate changes to team members who depend on CI artifacts

Caching Strategies

Effective caching dramatically reduces build times and costs:

Built-in Cache on Setup Actions:

CORRECT - Node.js with npm:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Set up Node.js
    uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'

  - name: Install dependencies
    run: npm ci

  - name: Run tests
    run: npm test

CORRECT - Python with pip:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Set up Python
    uses: actions/setup-python@v5
    with:
      python-version: '3.11'
      cache: 'pip'

  - name: Install dependencies
    run: pip install -r requirements.txt

  - name: Run tests
    run: pytest

CORRECT - Multiple Package Managers:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Set up Node.js for npm
    uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'
      cache-dependency-path: 'web/package-lock.json'

  - name: Install web dependencies
    working-directory: web
    run: npm ci

WRONG:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Set up Node.js
    uses: actions/setup-node@v4
    with:
      node-version: '20'
      # Missing cache configuration

  - name: Install dependencies
    run: npm ci

Actions with Built-in Caching:

Setup ActionCache ParameterCached Content
setup-nodecache: 'npm'~/.npm
setup-nodecache: 'yarn'~/.yarn/cache
setup-nodecache: 'pnpm'~/.pnpm-store
setup-pythoncache: 'pip'~/.cache/pip
setup-pythoncache: 'pipenv'~/.cache/pipenv
setup-javacache: 'maven'~/.m2/repository
setup-javacache: 'gradle'~/.gradle/caches
setup-gocache: true~/go/pkg/mod

Manual Caching with actions/cache:

CORRECT - Build Output Caching:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Cache build output
    uses: actions/cache@v4
    with:
      path: |
        dist/
        .next/cache
      key: build-${{ runner.os }}-${{ hashFiles('src/**', 'package-lock.json') }}
      restore-keys: |
        build-${{ runner.os }}-

  - name: Build application
    run: npm run build

CORRECT - Cargo/Rust Caching:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Cache Cargo registry
    uses: actions/cache@v4
    with:
      path: |
        ~/.cargo/bin/
        ~/.cargo/registry/index/
        ~/.cargo/registry/cache/
        ~/.cargo/git/db/
        target/
      key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
      restore-keys: |
        cargo-${{ runner.os }}-

  - name: Build project
    run: cargo build --release

CORRECT - Multiple Cache Paths:

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Cache dependencies and build
    uses: actions/cache@v4
    with:
      path: |
        node_modules/
        .npm/
        dist/
        .eslintcache
      key: ci-${{ runner.os }}-${{ hashFiles('package-lock.json', 'src/**/*.ts') }}
      restore-keys: |
        ci-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
        ci-${{ runner.os }}-

WRONG - Overly Broad Cache Key:

steps:
  - name: Cache dependencies
    uses: actions/cache@v4
    with:
      path: node_modules/
      key: deps-${{ runner.os }}
      # Missing hashFiles - cache won't update when dependencies change

WRONG - Caching Generated Files Without Dependencies:

steps:
  - name: Cache build
    uses: actions/cache@v4
    with:
      path: dist/
      key: build-${{ runner.os }}
      # No source code hash - stale builds will be reused

Cache Key Best Practices:

  1. Primary Key Structure: {type}-{os}-{hash}

    • Type: Purpose of cache (deps, build, test)
    • OS: Platform identifier
    • Hash: Content-based hash using hashFiles()
  2. Hash Selection:

    # Dependencies only
    key: deps-${{ hashFiles('package-lock.json') }}
    
    # Multiple lock files
    key: deps-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}
    
    # Source code + dependencies
    key: build-${{ hashFiles('src/**', 'package-lock.json') }}
    
    # Include configuration files
    key: build-${{ hashFiles('src/**', 'tsconfig.json', 'webpack.config.js') }}
    
  3. Restore Keys Priority:

    restore-keys: |
      deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
      deps-${{ runner.os }}-
    
    • Most specific first (exact version)
    • Progressively broader (same OS)
    • Never restore from different OS

Cache Management:

CORRECT - Cache Cleanup Strategy:

# Automatically handled by GitHub (7 days retention)
# Total size limit: 10 GB per repository

# Manual cache invalidation when needed:
# 1. Change cache key prefix
# 2. Clear via GitHub UI or API
# 3. Update hashFiles() pattern

Cache Hit Rate Monitoring:

steps:
  - name: Cache dependencies
    id: cache-deps
    uses: actions/cache@v4
    with:
      path: node_modules/
      key: deps-${{ hashFiles('package-lock.json') }}

  - name: Report cache status
    run: |
      if [ "${{ steps.cache-deps.outputs.cache-hit }}" = "true" ]; then
        echo "✓ Cache hit - dependencies restored from cache"
      else
        echo "✗ Cache miss - installing dependencies"
      fi

  - name: Install dependencies
    if: steps.cache-deps.outputs.cache-hit != 'true'
    run: npm ci

Matrix Builds

Matrix builds enable testing across multiple configurations efficiently:

CORRECT - Basic Matrix:

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest, windows-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

CORRECT - Matrix with Include/Exclude:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [18, 20]
        include:
          # Add specific combination with extra configuration
          - os: ubuntu-latest
            node-version: 20
            experimental: true
            coverage: true
        exclude:
          # Remove unsupported combination
          - os: macos-latest
            node-version: 18

    runs-on: ${{ matrix.os }}
    continue-on-error: ${{ matrix.experimental || false }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: Run tests
        run: npm test

      - name: Generate coverage
        if: matrix.coverage
        run: npm run test:coverage

CORRECT - Matrix with Multiple Dimensions:

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.9', '3.10', '3.11', '3.12']
        django-version: ['3.2', '4.2', '5.0']
        database: [postgres, mysql, sqlite]
        exclude:
          # Django 5.0 requires Python 3.10+
          - python-version: '3.9'
            django-version: '5.0'
          # Don't test all DB combinations for older versions
          - python-version: '3.9'
            database: mysql

    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install Django ${{ matrix.django-version }}
        run: pip install Django==${{ matrix.django-version }}

      - name: Run tests with ${{ matrix.database }}
        env:
          DATABASE_ENGINE: ${{ matrix.database }}
        run: pytest

WRONG - Too Many Combinations:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [16, 18, 20, 22]
        python-version: ['3.9', '3.10', '3.11', '3.12']
        database: [postgres, mysql, sqlite, mongodb]
    # This creates 3 * 4 * 4 * 4 = 192 jobs!
    # Way too many, will be slow and expensive

WRONG - Missing fail-fast Configuration:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
      # Missing fail-fast: false
      # One failure will cancel all other jobs

Matrix Strategy Guidelines:

FactorRecommendationRationale
Total combinationsKeep under 20Reduces cost and completion time
fail-fastfalse for matrix testsSee all failures, not just first
max-parallelUsually omitGitHub optimizes automatically
Required checksUse separate jobDon't require all matrix jobs

CORRECT - Matrix with Dynamic Configuration:

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set matrix configurations
        id: set-matrix
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            # Minimal matrix for PRs
            echo 'matrix={"node-version": [20], "os": ["ubuntu-latest"]}' >> $GITHUB_OUTPUT
          else
            # Full matrix for main branch
            echo 'matrix={"node-version": [18, 20, 22], "os": ["ubuntu-latest", "macos-latest", "windows-latest"]}' >> $GITHUB_OUTPUT
          fi

  test:
    needs: setup
    strategy:
      matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
    runs-on: ${{ matrix.os }}
    steps:
      - name: Run tests
        run: npm test

Matrix Caching Best Practices:

CORRECT - Separate Caches per Matrix Dimension:

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20]
        os: [ubuntu-latest, macos-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          # Cache key automatically includes OS and Node version

      - name: Cache test fixtures
        uses: actions/cache@v4
        with:
          path: test-fixtures/
          key: fixtures-${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('test/**') }}

Composite Actions

Composite actions reduce duplication by encapsulating reusable step sequences:

When to Create Composite Actions:

  • 3+ steps repeated across multiple workflows
  • Complex setup that requires specific configuration
  • Steps that need to stay in sync across workflows
  • Common patterns used by multiple teams

CORRECT - Simple Composite Action:

# .github/actions/setup-node-project/action.yml
name: Setup Node.js Project
description: Checkout code, setup Node.js, and install dependencies

inputs:
  node-version:
    description: Node.js version to use
    required: false
    default: '20'
  working-directory:
    description: Directory containing package.json
    required: false
    default: '.'

runs:
  using: composite
  steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
        cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json

    - name: Install dependencies
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm ci

Using the Composite Action:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Setup project
        uses: ./.github/actions/setup-node-project
        with:
          node-version: '20'

      - name: Run tests
        run: npm test

CORRECT - Composite Action with Outputs:

# .github/actions/build-info/action.yml
name: Build Information
description: Extract version and commit information

outputs:
  version:
    description: Version from package.json
    value: ${{ steps.version.outputs.version }}
  short-sha:
    description: Short commit SHA
    value: ${{ steps.sha.outputs.short-sha }}
  build-tag:
    description: Full build tag
    value: ${{ steps.tag.outputs.build-tag }}

runs:
  using: composite
  steps:
    - name: Extract version
      id: version
      shell: bash
      run: |
        VERSION=$(node -p "require('./package.json').version")
        echo "version=${VERSION}" >> $GITHUB_OUTPUT

    - name: Get short SHA
      id: sha
      shell: bash
      run: |
        SHORT_SHA=$(git rev-parse --short HEAD)
        echo "short-sha=${SHORT_SHA}" >> $GITHUB_OUTPUT

    - name: Create build tag
      id: tag
      shell: bash
      run: |
        TAG="v${{ steps.version.outputs.version }}-${{ steps.sha.outputs.short-sha }}"
        echo "build-tag=${TAG}" >> $GITHUB_OUTPUT

Using with Outputs:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Get build info
        id: info
        uses: ./.github/actions/build-info

      - name: Build with version tag
        run: |
          echo "Building version ${{ steps.info.outputs.version }}"
          echo "Build tag: ${{ steps.info.outputs.build-tag }}"
          docker build -t app:${{ steps.info.outputs.build-tag }} .

CORRECT - Composite Action with Error Handling:

# .github/actions/deploy-check/action.yml
name: Deployment Checks
description: Run pre-deployment validation

inputs:
  environment:
    description: Target environment
    required: true
  health-check-url:
    description: URL to check before deployment
    required: true

runs:
  using: composite
  steps:
    - name: Validate environment
      shell: bash
      run: |
        VALID_ENVS="staging production"
        if [[ ! " ${VALID_ENVS} " =~ " ${{ inputs.environment }} " ]]; then
          echo "Error: Invalid environment '${{ inputs.environment }}'"
          echo "Valid environments: ${VALID_ENVS}"
          exit 1
        fi

    - name: Check service health
      shell: bash
      run: |
        MAX_RETRIES=3
        RETRY=0

        while [ $RETRY -lt $MAX_RETRIES ]; do
          if curl -f -s "${{ inputs.health-check-url }}" > /dev/null; then
            echo "Health check passed"
            exit 0
          fi

          RETRY=$((RETRY + 1))
          echo "Health check failed (attempt $RETRY/$MAX_RETRIES)"

          if [ $RETRY -lt $MAX_RETRIES ]; then
            sleep 5
          fi
        done

        echo "Health check failed after $MAX_RETRIES attempts"
        exit 1

    - name: Verify deployment permissions
      shell: bash
      env:
        GITHUB_TOKEN: ${{ github.token }}
      run: |
        # Check if workflow has deployment permissions
        gh api repos/${{ github.repository }}/environments/${{ inputs.environment }} \
          || echo "Warning: Environment not configured"

WRONG - Missing Shell Specification:

# .github/actions/bad-example/action.yml
runs:
  using: composite
  steps:
    - name: Run command
      run: echo "This will fail"
      # Missing shell: bash

WRONG - Using Checkout in Composite Action:

# Don't checkout in composite actions - do it in the calling workflow
runs:
  using: composite
  steps:
    - uses: actions/checkout@v4 # WRONG - caller should checkout
    - name: Do something
      shell: bash
      run: ./script.sh

Composite Action Best Practices:

  1. Always specify shell in run steps
  2. Don't include checkout - let callers handle it
  3. Use inputs for configuration - don't hardcode values
  4. Provide sensible defaults for optional inputs
  5. Document outputs clearly in descriptions
  6. Handle errors explicitly - don't assume success
  7. Keep actions focused - single responsibility principle
  8. Version your actions if shared across repositories

Reusable Workflows

Reusable workflows enable sharing entire workflow logic:

CORRECT - Reusable Workflow Definition:

# .github/workflows/reusable-deploy.yml
name: Reusable Deployment

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      version:
        required: true
        type: string
      region:
        required: false
        type: string
        default: 'us-east-1'
    secrets:
      AWS_ACCESS_KEY_ID:
        required: true
      AWS_SECRET_ACCESS_KEY:
        required: true
      DEPLOY_TOKEN:
        required: false
    outputs:
      deployment-url:
        description: URL of the deployed application
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    outputs:
      url: ${{ steps.deploy.outputs.url }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        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: ${{ inputs.region }}

      - name: Deploy application
        id: deploy
        run: |
          echo "Deploying version ${{ inputs.version }} to ${{ inputs.environment }}"
          URL=$(./deploy.sh ${{ inputs.environment }} ${{ inputs.version }})
          echo "url=${URL}" >> $GITHUB_OUTPUT

      - name: Verify deployment
        run: |
          curl -f ${{ steps.deploy.outputs.url }}/health

Calling the Reusable Workflow:

# .github/workflows/deploy-staging.yml
name: Deploy to Staging

on:
  push:
    branches: [develop]

jobs:
  deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      version: ${{ github.sha }}
      region: us-west-2
    secrets:
      AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_KEY }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET }}

CORRECT - Reusable Workflow with Multiple Jobs:

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

on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'
      run-integration-tests:
        type: boolean
        default: false

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  unit-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run test:unit

  integration-test:
    if: inputs.run-integration-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run test:integration

WRONG - Nesting Too Deep:

# Workflow A calls Workflow B calls Workflow C calls Workflow D
# Maximum nesting depth is 4 - avoid going this deep
# Better: flatten the hierarchy or use composite actions

Reusable Workflow vs Composite Action Decision Matrix:

FactorReusable WorkflowComposite Action
ScopeMultiple jobsMultiple steps within a job
SecretsCan define required secretsUses caller's secrets
PermissionsCan define permissionsInherits from job
ConditionalsJob-level conditionalsStep-level conditionals
MatrixCan use matrixCannot use matrix
Use caseComplete CI/CD pipelineReusable step sequence
Calling syntaxuses: at job leveluses: at step level

Artifacts

Artifacts enable sharing data between jobs and workflow runs:

CORRECT - Basic Artifact Upload/Download:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Build application
        run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/

      - name: Deploy application
        run: ./deploy.sh dist/

CORRECT - Multiple Artifacts with Context-Aware Naming:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [18, 20]

    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: Run tests
        run: npm test

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results-${{ matrix.os }}-node${{ matrix.node-version }}
          path: |
            test-results/
            coverage/
          retention-days: 14

CORRECT - Conditional Artifact Upload:

steps:
  - name: Run tests with coverage
    run: npm run test:coverage
    continue-on-error: true

  - name: Upload coverage report
    if: success() || failure()
    uses: actions/upload-artifact@v4
    with:
      name: coverage-report-${{ github.sha }}
      path: coverage/
      if-no-files-found: warn
      retention-days: 30

CORRECT - Large Binary Artifacts:

steps:
  - name: Build release binaries
    run: cargo build --release

  - name: Upload Linux binary
    uses: actions/upload-artifact@v4
    with:
      name: binary-linux-amd64
      path: target/release/myapp
      retention-days: 90
      compression-level: 9 # Maximum compression for large files

WRONG - Uploading Sensitive Files:

steps:
  - name: Upload everything
    uses: actions/upload-artifact@v4
    with:
      name: all-files
      path: .
      # WRONG: Will include .env, secrets, node_modules, etc.

WRONG - No Retention Policy:

steps:
  - name: Upload temporary build
    uses: actions/upload-artifact@v4
    with:
      name: build
      path: dist/
      # Missing retention-days - uses default 90 days
      # Wastes storage for temporary artifacts

Artifact Best Practices:

AspectRecommendationRationale
NamingInclude context (OS, version, SHA)Avoid conflicts, enable debugging
RetentionSet appropriate days (7-90)Balance storage costs vs needs
SizeCompress or filter large filesFaster upload/download
PathsUse specific paths, not .Avoid uploading secrets
ConditionsUse if: always() for test resultsCapture failures

Monorepo Patterns

Optimize workflows for monorepo repositories:

CORRECT - Path Filters on Triggers:

name: Backend CI

on:
  push:
    branches: [main]
    paths:
      - 'backend/**'
      - 'shared/**'
      - '.github/workflows/backend-ci.yml'
  pull_request:
    paths:
      - 'backend/**'
      - 'shared/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Test backend
        working-directory: backend
        run: npm test

CORRECT - Dynamic Job Execution with Path Filters:

name: Monorepo CI

on:
  pull_request:

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      backend: ${{ steps.filter.outputs.backend }}
      frontend: ${{ steps.filter.outputs.frontend }}
      mobile: ${{ steps.filter.outputs.mobile }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Check for changes
        uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            backend:
              - 'apps/backend/**'
              - 'packages/shared/**'
            frontend:
              - 'apps/frontend/**'
              - 'packages/ui/**'
              - 'packages/shared/**'
            mobile:
              - 'apps/mobile/**'
              - 'packages/shared/**'

  backend:
    needs: changes
    if: needs.changes.outputs.backend == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test backend
        run: npm run test:backend

  frontend:
    needs: changes
    if: needs.changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test frontend
        run: npm run test:frontend

  mobile:
    needs: changes
    if: needs.changes.outputs.mobile == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test mobile
        run: npm run test:mobile

CORRECT - Selective Caching by Project:

jobs:
  test-backend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: 'backend/package-lock.json'

      - name: Install backend dependencies
        working-directory: backend
        run: npm ci

      - name: Test backend
        working-directory: backend
        run: npm test

CORRECT - Matrix Build for Multiple Projects:

jobs:
  test:
    strategy:
      matrix:
        project:
          - name: backend
            path: apps/backend
            test-command: npm run test:unit
          - name: frontend
            path: apps/frontend
            test-command: npm run test
          - name: api
            path: services/api
            test-command: cargo test

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Test ${{ matrix.project.name }}
        working-directory: ${{ matrix.project.path }}
        run: ${{ matrix.project.test-command }}

These CI patterns form the foundation of efficient, maintainable continuous integration pipelines. Apply them thoughtfully based on your project's specific needs and constraints.

Stats
Stars0
Forks0
Last CommitFeb 10, 2026
Actions

Similar Skills