Skip to main content

Optimizing Workflow Performance

1. Using Caching Effectively

Example: Caching Node.js Dependencies

steps:
  - name: Setup Node.js
    uses: actions/setup-node@v4
    with:
      node-version: '18'
  - name: Cache npm dependencies
    uses: actions/cache@v3
    with:
      path: ~/.npm
      key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-node-
Option Values Description
path ~/.npm Directory to cache
key ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} Unique identifier for cache
restore-keys ${{ runner.os }}-node- Alternative keys to restore from
Note: The hashFiles function ensures cache invalidation when dependencies change.

Example: Caching with Multiple Paths

steps:
  - name: Cache multiple paths
    uses: actions/cache@v3
    with:
      path: |
        ~/.cache/yarn
        ~/.npm
      key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
Warning: Avoid caching paths that contain sensitive data or are OS-specific without proper key handling.

Best Practices for Caching

  • Use hashFiles to invalidate cache on dependency changes
  • Cache directories, not individual files
  • Set restore-keys for fallback cache restoration
  • Avoid caching sensitive information

2. Parallelizing Jobs

Example: Running Jobs in Parallel

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

  test:
    runs-on: ubuntu-latest
    steps:
      - name: Run tests
        run: npm test

  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        run: echo "Deploying..."
Job Runner Description
build ubuntu-latest Checks out the repository code
test ubuntu-latest Runs unit and integration tests
deploy ubuntu-latest Deploys the application to production
Note: Jobs run in parallel by default unless dependencies are specified using needs.

Best Practices for Parallel Jobs

  • Use needs to define job dependencies when necessary
  • Ensure jobs are independent to maximize parallelism
  • Use matrix strategies for testing across multiple environments

3. Reducing Checkout Depth

By default, GitHub Actions checks out your repository with a depth of 100 commits. For workflows that don't require the full history, reducing this depth can significantly speed up checkout times and reduce resource usage.

Option Values Description
fetch-depth 0, 1, 10, etc. Number of commits to fetch. Set to 0 to fetch all history.

Example: Fetch only the last 1 commit

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 1
Note: Setting fetch-depth to 0 will fetch the entire history, which may be necessary for some CI/CD operations but increases checkout time.

4. Using Sparse Checkout

Sparse checkout allows you to check out only a subset of files from a repository, which can significantly reduce the time and resources needed for workflows that don't require the entire repository.

Option Values Description
sparse-checkout true Enables sparse checkout in the workflow
sparse-checkout-directories ["src/", "docs/"] List of directories to include in the sparse checkout

Example: Setting up sparse checkout

steps:
  - name: Sparse Checkout
    uses: actions/checkout@v4
    with:
      sparse-checkout: true
      sparse-checkout-directories: 
        - src/
        - docs/
Note: Sparse checkout is supported in actions/checkout version 3 and above.

5. Minimizing Docker Layers

Layer Description Optimization Strategy
FROM Base image for the Docker container Use minimal base images like alpine or slim to reduce initial layer size
RUN Commands executed during build Combine multiple apt-get or yum commands into one layer using && to reduce number of layers
COPY / ADD Files copied into the image Use COPY instead of ADD when possible. Avoid copying unnecessary files or directories
CACHE Build cache mechanism Structure your Dockerfile to take advantage of layer caching by placing less frequently changing instructions earlier

Example: Optimized Dockerfile Layering

FROM node:18-alpine

# Install dependencies first to leverage caching
COPY package*.json ./
RUN npm ci --only=production

# Copy application code after dependencies
COPY . .

# Expose port and run app
EXPOSE 3000
CMD ["npm", "start"]
Note: The npm ci command ensures consistent dependency installation and is faster than npm install.
Warning: Avoid placing frequently changing instructions (like copying source code) before dependency installation to prevent unnecessary cache invalidation.

6. Using Matrix Efficiently

Field Description Example
strategy.matrix Defines the matrix of build configurations to run
{
  "strategy": {
    "matrix": {
      "os": ["ubuntu-latest", "windows-latest"],
      "node-version": ["18.x", "20.x"]
    }
  }
}
strategy.matrix.include Adds additional configurations to the matrix
{
  "strategy": {
    "matrix": {
      "os": ["ubuntu-latest"],
      "node-version": ["18.x"]
    },
    "include": [
      {
        "os": "macOS-latest",
        "node-version": "20.x"
      }
    ]
  }
}
strategy.matrix.exclude Removes specific configurations from the matrix
{
  "strategy": {
    "matrix": {
      "os": ["ubuntu-latest", "windows-latest"],
      "node-version": ["18.x", "20.x"]
    },
    "exclude": [
      {
        "os": "windows-latest",
        "node-version": "18.x"
      }
    ]
  }
}

Example: Efficient Matrix Usage

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node-version: [18.x, 20.x]
    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
Note: Using strategy.matrix allows you to run your workflow across multiple configurations in parallel, which can significantly reduce total execution time.
Warning: Avoid creating too many matrix combinations as it can lead to high resource usage and longer overall workflow times.

Best Practices for Matrix Usage

  • Limit the number of combinations to avoid resource exhaustion
  • Use strategy.matrix.exclude to remove problematic configurations
  • Combine strategy.matrix.include with strategy.matrix.exclude for fine-grained control
  • Use strategy.fail-fast to stop the job if any matrix job fails

7. Skipping CI

Option Values Notes
github.event_name push, pull_request Event that triggers the workflow
github.ref refs/heads/main, refs/pull/123/merge Branch or tag reference
github.event.head_commit.message Commit message content Used to check for skip keywords

Example: Skip CI with commit message

name: CI
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Run tests
        run: npm test
Note: The [skip ci] keyword is case-insensitive and can appear anywhere in the commit message.

How It Works

  • GitHub Actions evaluates the if condition before running jobs.
  • If the commit message includes [skip ci], the job is skipped entirely.
  • This is useful for documentation changes or other commits that don't require a full CI run.

8. Using Conditionals

Expression Description Usage
github.event Event data for the current workflow run if: github.event_name == 'push'
github.ref Branch or tag name that triggered the workflow if: github.ref == 'refs/heads/main'
github.actor Username of the person or app that triggered the workflow if: github.actor == 'octocat'
github.workflow Name of the workflow if: github.workflow == 'Build and Test'
github.action Name of the action being executed if: github.action == 'setup-node'

Example: Conditional Step Execution

steps:
  - name: Run on Main Branch
    if: github.ref == 'refs/heads/main'
    run: echo "Running on main branch"

Example: Conditional on Event Type

if: github.event_name == 'pull_request'
  run: echo "This runs only on pull requests"
Note: Use if expressions to control when steps or jobs run based on context variables.
Warning: Avoid complex logic in if conditions; keep them simple for better readability and maintainability.

9. Caching Docker Layers

Option Values Notes
uses actions/cache@v3 Cache action for GitHub Actions
key docker-${{ hashFiles('Dockerfile') }}-layer-${{ steps.build.outputs.layer }} Unique cache key based on Dockerfile and layer
path /root/.docker Path to Docker cache directory

Example: Caching Docker layers in a workflow

name: Build with Docker layer caching
on:
  push:
    branches: [ main ]

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

      - name: Cache Docker layers
        uses: actions/cache@v3
        id: cache-docker
        with:
          path: /root/.docker
          key: docker-${{ hashFiles('Dockerfile') }}-layer-${{ steps.build.outputs.layer }}

      - name: Build Docker image
        id: build
        run: |
          docker build -t my-app .
          echo "layer=$(docker inspect --format='{{.Id}}' my-app)" >> $GITHUB_OUTPUT

10. Using Prebuilt Actions

Property Description Usage
uses Specifies the action to use from a repository uses: actions/checkout@v4
with Provides input parameters to the action with: token: ${{ secrets.GITHUB_TOKEN }}

Example: Using a prebuilt action to checkout code

name: Checkout Code
on:
  push:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

Example: Using a prebuilt action with inputs

name: Deploy to Production
on:
  push:
    branches: [ main ]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy with GitHub Actions
        uses: actions/deploy@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          environment: production
Note: Prebuilt actions are available in the actions namespace and can be found on GitHub Marketplace or in public repositories.

Benefits of Using Prebuilt Actions

  • Reduce workflow setup time by reusing existing actions
  • Ensure consistency across workflows with standardized actions
  • Access to community-maintained actions with frequent updates

11. Optimizing Runner Selection

Runner Type Description Use Case Notes
ubuntu-latest Latest Ubuntu Linux runner General-purpose workflows, Linux-specific builds Includes latest tools and dependencies
macos-latest Latest macOS runner macOS-specific builds, Xcode projects High memory and CPU resources
windows-latest Latest Windows runner Windows-specific builds, .NET projects Supports PowerShell and CMD scripts
self-hosted Custom runner instance Specialized environments, secure builds Requires setup and maintenance

Example: Using specific runner

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
Note: Using latest runners ensures you get the most up-to-date tools but may introduce breaking changes. Pin to specific versions for stability.

Best Practices

  • Pin runner versions for stable builds
  • Use self-hosted runners for sensitive or custom environments
  • Choose ubuntu-latest for most Linux-based workflows
  • Use macos-latest or windows-latest for platform-specific builds

12. Monitoring Workflow Duration

Metric Description Collection Method Notes
workflow_duration Total time taken for a workflow to complete GitHub Actions UI or API Includes setup, job execution, and cleanup times
job_duration Time taken for an individual job to complete GitHub Actions UI or API Can be used to identify bottlenecks in specific jobs
step_duration Time taken for each step within a job GitHub Actions UI or API Helps pinpoint slow steps in a job

Example: Using GitHub Actions API to fetch workflow duration

const fetchWorkflowDuration = async () => {
  const response = await fetch(
    'https://api.github.com/repos/octocat/Hello-World/actions/workflows'
  );
  const workflows = await response.json();
  console.log(workflows);
};
Note: Workflow duration data is available via the GitHub Actions API and can be used to optimize performance.

13. Using Workflow Templates

Example: Reusing a workflow template

name: Reuse Template
on:
  push:
    branches:
      - main
jobs:
  build:
    uses: octocat/template-repo/.github/workflows/build.yml@v1
    with:
      node-version: "20"
Property Description Values
uses Path to the workflow template owner/repo/.github/workflows/workflow.yml@ref
with Input parameters to pass to the template Key-value pairs
Note: Templates must be stored in the .github/workflows directory of a repository.
Warning: Template workflows are executed in the context of the repository where they are used, not the template's repository.

Was this article helpful?