GitHub Actions Tutorial — Matrix Builds Exercises

3 minute read

Practice Exercises — Matrix Builds

Test your understanding of matrix strategies, include/exclude, fail-fast, dynamic matrices, and real-world patterns with these hands-on exercises.


Exercise 1 — Basic Cross-Platform Matrix

Task: Create a workflow that tests a Node.js project on ubuntu-latest and macos-latest with Node versions 18 and 20. Print the OS and Node version in each job.

Show Solution

name: Cross-Platform Tests

on: push

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
        node: [18, 20]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: |
          echo "OS: ${{ matrix.os }}"
          echo "Node: ${{ matrix.node }}"
          node --version
      - run: npm ci
      - run: npm test

This creates 4 jobs (2 OS × 2 Node versions), all running in parallel.


Exercise 2 — Include and Exclude

Task: Create a matrix with OS [ubuntu-latest, windows-latest, macos-latest] and Python ['3.10', '3.11', '3.12'], but:

  • Exclude macos-latest + 3.10
  • Include ubuntu-latest + 3.13 marked as experimental: true
  • The experimental combination should use continue-on-error: true
Show Solution

name: Python Matrix

on: push

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python: ['3.10', '3.11', '3.12']
        exclude:
          - os: macos-latest
            python: '3.10'
        include:
          - os: ubuntu-latest
            python: '3.13'
            experimental: true
    runs-on: ${{ matrix.os }}
    continue-on-error: ${{ matrix.experimental || false }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
      - run: python --version
      - run: pip install -r requirements.txt
      - run: pytest

Total: 9 − 1 excluded + 1 included = 9 jobs. The 3.13 combo is allowed to fail.


Exercise 3 — Dynamic Matrix from Changed Directories

Task: Create a two-job workflow:

  1. detect — lists directories under services/ that have changed (using git diff)
  2. test — runs tests for each changed service using a dynamic matrix

Assume the monorepo has services/api/, services/auth/, services/web/.

Show Solution

name: Monorepo CI

on:
  push:
    branches: [main]

jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      services: ${{ steps.changes.outputs.services }}
      has-changes: ${{ steps.changes.outputs.has-changes }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Find changed services
        id: changes
        run: |
          SERVICES=$(git diff --name-only HEAD~1 HEAD \
            | grep '^services/' \
            | cut -d/ -f2 \
            | sort -u \
            | jq -R . \
            | jq -sc .)
          if [ "$SERVICES" = "[]" ]; then
            echo "has-changes=false" >> "$GITHUB_OUTPUT"
          else
            echo "has-changes=true" >> "$GITHUB_OUTPUT"
          fi
          echo "services=$SERVICES" >> "$GITHUB_OUTPUT"

  test:
    needs: detect
    if: needs.detect.outputs.has-changes == 'true'
    strategy:
      matrix:
        service: ${{ fromJSON(needs.detect.outputs.services) }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          echo "Testing ${{ matrix.service }}..."
          cd services/${{ matrix.service }}
          npm ci && npm test

Only changed services get tested — if api and auth changed, only 2 jobs run.


Exercise 4 — Fail-Fast and Max-Parallel

Task: Create a matrix that deploys to 4 regions (us-east-1, us-west-2, eu-west-1, ap-southeast-1), with:

  • Maximum 2 deployments at a time
  • All deployments continue even if one fails
  • Each job prints “Deploying to <region>”
Show Solution

name: Multi-Region Deploy

on:
  workflow_dispatch:

jobs:
  deploy:
    strategy:
      fail-fast: false
      max-parallel: 2
      matrix:
        region: [us-east-1, us-west-2, eu-west-1, ap-southeast-1]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ matrix.region }}..."
      - run: echo "Region ${{ matrix.region }} deploy complete"

This deploys in 2 batches of 2: [us-east-1, us-west-2] then [eu-west-1, ap-southeast-1].


Exercise 5 — Matrix with Per-Combination Artifacts

Task: Create a matrix that builds on ubuntu-latest and windows-latest with Node 20. Each job should:

  1. Create a file build-info.txt with the OS name
  2. Upload it as an artifact named build-<os>
  3. A final report job downloads all artifacts and lists them
Show Solution

name: Matrix Artifacts

on: push

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - run: echo "Built on ${{ matrix.os }}" > build-info.txt
      - uses: actions/upload-artifact@v4
        with:
          name: build-${{ matrix.os }}
          path: build-info.txt

  report:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: build-*
          merge-multiple: true
          path: ./all-builds
      - run: |
          echo "All build artifacts:"
          ls -la ./all-builds
          cat ./all-builds/build-info.txt

The merge-multiple: true merges both artifacts into ./all-builds/. Since both files are named build-info.txt, the last one wins — in production, use unique filenames like build-info-$OS.txt.


← Back to: Chapter 9: Matrix Builds

Next Chapter: Chapter 10: Reusable Workflows →

Updated: