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.