Skip to main content

Advanced GitHub Actions Workflows

Master advanced patterns and optimization techniques for sophisticated GitHub Actions workflows.

Matrix Builds

Matrix builds allow you to run the same job across multiple configurations simultaneously, enabling comprehensive testing across different environments, versions, and configurations.

Basic Matrix Configuration

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

jobs:
test:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test

Complex Matrix Strategies

Multi-Dimensional Matrix

strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest]
database: [postgresql, mysql, sqlite]
include:
# Include specific combinations
- node-version: 18
os: macos-latest
database: postgresql
exclude:
# Exclude specific combinations
- node-version: 16
os: windows-latest
database: mysql

Dynamic Matrix Generation

jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
# Generate matrix dynamically based on conditions
MATRIX='{"node-version":["16","18"]}'
if [ "${{ github.event_name }}" == "pull_request" ]; then
MATRIX='{"node-version":["18"]}'
fi
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT

test:
needs: generate-matrix
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

Matrix Optimization Techniques

Fail-Fast Strategy

strategy:
fail-fast: true # Stop all matrix jobs if one fails
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]

# Or disable fail-fast for independent testing
strategy:
fail-fast: false # Continue all matrix jobs even if one fails
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]

Maximum Parallel Jobs

strategy:
max-parallel: 3 # Limit concurrent matrix jobs
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]

Conditional Execution

Advanced conditional logic enables smart workflow behavior based on various conditions and contexts.

Conditional Jobs

Branch-Based Conditions

jobs:
test:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: echo "Running tests on main branch"

deploy-staging:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- run: echo "Deploying to staging"

deploy-production:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: echo "Deploying to production"

Event-Based Conditions

jobs:
ci:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- run: echo "Running CI for pull request"

release:
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
steps:
- run: echo "Creating release"

Complex Conditions

jobs:
deploy:
runs-on: ubuntu-latest
if: |
github.ref == 'refs/heads/main' &&
github.event_name == 'push' &&
contains(github.event.head_commit.message, '[deploy]')
steps:
- run: echo "Deploying to production"

Conditional Steps

Step-Level Conditions

steps:
- name: Install dependencies
run: npm ci
if: github.event_name == 'pull_request'

- name: Run tests
run: npm test
if: always() && steps.install-deps.outcome == 'success'

- name: Deploy
run: npm run deploy
if: github.ref == 'refs/heads/main' && steps.test.outcome == 'success'

Environment-Based Conditions

steps:
- name: Deploy to staging
run: npm run deploy:staging
if: github.ref == 'refs/heads/develop'
env:
DEPLOY_ENV: staging

- name: Deploy to production
run: npm run deploy:production
if: github.ref == 'refs/heads/main'
env:
DEPLOY_ENV: production

Advanced Conditional Logic

Multiple Conditions

steps:
- name: Security scan
run: npm run security:scan
if: |
(github.event_name == 'pull_request' && github.actor != 'dependabot[bot]') ||
(github.event_name == 'push' && github.ref == 'refs/heads/main')

Function-Based Conditions

steps:
- name: Build
run: npm run build
if: contains(github.event.head_commit.modified, 'src/') || contains(github.event.head_commit.added, 'src/')

- name: Test
run: npm test
if: contains(github.event.head_commit.modified, 'src/') || contains(github.event.head_commit.added, 'tests/')

Parallel vs Serial Execution

Understanding when to use parallel or serial execution is crucial for workflow optimization.

Parallel Job Execution

Independent Jobs

jobs:
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run unit tests
run: npm run test:unit

test-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run integration tests
run: npm run test:integration

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run linting
run: npm run lint

security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run security scan
run: npm audit

Parallel with Dependencies

jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: Setup
run: echo "Setup complete"

test:
runs-on: ubuntu-latest
needs: setup
steps:
- name: Test
run: echo "Testing"

build:
runs-on: ubuntu-latest
needs: setup
steps:
- name: Build
run: echo "Building"

deploy:
runs-on: ubuntu-latest
needs: [test, build]
steps:
- name: Deploy
run: echo "Deploying"

Serial Job Execution

Sequential Dependencies

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build
run: echo "Building"

test:
runs-on: ubuntu-latest
needs: build
steps:
- name: Test
run: echo "Testing"

deploy-staging:
runs-on: ubuntu-latest
needs: test
steps:
- name: Deploy to staging
run: echo "Deploying to staging"

e2e-test:
runs-on: ubuntu-latest
needs: deploy-staging
steps:
- name: E2E tests
run: echo "Running E2E tests"

deploy-production:
runs-on: ubuntu-latest
needs: e2e-test
steps:
- name: Deploy to production
run: echo "Deploying to production"

Hybrid Execution Patterns

Conditional Parallel Execution

jobs:
ci:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: CI tests
run: npm test

cd:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: ci
steps:
- name: CD deployment
run: npm run deploy

Caching Strategies

Effective caching significantly improves workflow performance by reducing build times and resource usage.

Basic Caching

Node.js Dependencies

- name: Cache node modules
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install dependencies
run: npm ci

Python Dependencies

- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

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

Advanced Caching Patterns

Multi-Level Caching

- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-
${{ runner.os }}-

- name: Cache build artifacts
uses: actions/cache@v3
with:
path: |
dist/
build/
key: ${{ runner.os }}-build-${{ hashFiles('**/src/**') }}
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-

Conditional Caching

- name: Cache dependencies
uses: actions/cache@v3
if: github.event_name == 'pull_request'
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

- name: Cache build artifacts
uses: actions/cache@v3
if: github.ref == 'refs/heads/main'
with:
path: dist/
key: ${{ runner.os }}-build-${{ github.sha }}

Custom Caching Logic

Dynamic Cache Keys

- name: Generate cache key
id: cache-key
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
CACHE_KEY="${{ runner.os }}-pr-${{ github.event.pull_request.number }}"
else
CACHE_KEY="${{ runner.os }}-${{ github.ref }}"
fi
echo "key=$CACHE_KEY" >> $GITHUB_OUTPUT

- name: Cache artifacts
uses: actions/cache@v3
with:
path: dist/
key: ${{ steps.cache-key.outputs.key }}-${{ hashFiles('**/src/**') }}

Cache Invalidation

- name: Invalidate cache
run: |
# Invalidate cache when dependencies change
if [ -f "package-lock.json" ]; then
echo "CACHE_VERSION=$(date +%s)" >> $GITHUB_ENV
fi

- name: Cache with version
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ env.CACHE_VERSION }}

Performance Optimization

Workflow Optimization Techniques

Job Optimization

jobs:
fast-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Quick lint
run: npm run lint:quick
- name: Quick test
run: npm run test:unit

comprehensive-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Full test suite
run: npm run test
- name: Integration tests
run: npm run test:integration

Step Optimization

steps:
- name: Setup environment
run: |
# Combine multiple commands in single step
npm ci --prefer-offline --no-audit
npm run build
npm run test:unit

- name: Parallel operations
run: |
# Run independent operations in parallel
npm run lint &
npm run type-check &
npm run security-scan &
wait

Resource Optimization

Runner Selection

jobs:
lightweight-tests:
runs-on: ubuntu-latest
steps:
- name: Quick tests
run: npm run test:unit

heavy-builds:
runs-on: ubuntu-latest-4-cores # Use larger runners for heavy operations
steps:
- name: Build and test
run: npm run build:all

Timeout Configuration

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10 # Set job timeout
steps:
- name: Long-running test
run: npm run test:integration
timeout-minutes: 5 # Set step timeout

Build Optimization

Incremental Builds

- name: Cache build artifacts
uses: actions/cache@v3
with:
path: |
.next/cache
node_modules/.cache
key: ${{ runner.os }}-build-${{ hashFiles('**/src/**') }}

- name: Incremental build
run: |
# Only build changed files
npm run build:incremental

Parallel Builds

strategy:
matrix:
package: [frontend, backend, shared]

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build ${{ matrix.package }}
run: npm run build:${{ matrix.package }}

Advanced Patterns

Workflow Composition

Reusable Workflows

# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [ main ]

jobs:
test:
uses: ./.github/workflows/test.yml
secrets: inherit

# .github/workflows/test.yml
name: Test
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '18'

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ inputs.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
- name: Run tests
run: npm test

Workflow Dependencies

jobs:
build:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.build.outputs.tag }}
steps:
- name: Build image
id: build
run: |
TAG="app:${{ github.sha }}"
docker build -t $TAG .
echo "tag=$TAG" >> $GITHUB_OUTPUT

test:
needs: build
runs-on: ubuntu-latest
steps:
- name: Test image
run: docker run --rm ${{ needs.build.outputs.image-tag }} npm test

deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- name: Deploy
run: docker push ${{ needs.build.outputs.image-tag }}

Error Handling

Retry Logic

steps:
- name: Flaky test with retry
uses: nick-invision/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
command: npm run test:flaky

- name: Network operation with retry
run: |
for i in {1..3}; do
npm run deploy && break
echo "Attempt $i failed, retrying..."
sleep 5
done

Graceful Degradation

steps:
- name: Optional test
run: npm run test:optional
continue-on-error: true

- name: Check optional test result
run: |
if [ "${{ steps.optional-test.outcome }}" == "failure" ]; then
echo "Optional test failed, but continuing..."
fi

Key Takeaways

Advanced Workflow Principles

  1. Matrix Builds: Use for comprehensive testing across multiple configurations
  2. Conditional Execution: Implement smart logic based on context and events
  3. Parallel Processing: Maximize efficiency with parallel job execution
  4. Intelligent Caching: Reduce build times with strategic caching
  5. Performance Optimization: Continuously optimize for speed and efficiency

Best Practices Summary

  • Use Matrix Builds: For testing across multiple environments and versions
  • Implement Conditions: Smart workflow behavior based on context
  • Optimize Caching: Strategic caching for dependencies and artifacts
  • Parallel Execution: Run independent jobs simultaneously
  • Monitor Performance: Track and optimize workflow execution times

Common Patterns

  • Fail-Fast vs Continue: Choose appropriate strategy for your use case
  • Dynamic Matrix: Generate matrix configurations based on conditions
  • Multi-Level Caching: Cache dependencies and build artifacts separately
  • Conditional Steps: Skip unnecessary steps based on context
  • Error Recovery: Implement retry logic and graceful degradation

Next Steps: Ready to create custom actions? Continue to Section 2.3: Custom Actions Development to learn how to build reusable workflow components.


Mastering these advanced patterns will help you create sophisticated, efficient, and maintainable GitHub Actions workflows. In the next section, we'll explore how to build custom actions for reusable automation.