GitHub Actions Tutorial — Matrix Builds Exercises
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.13marked asexperimental: 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:
detect— lists directories underservices/that have changed (usinggit diff)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:
- Create a file
build-info.txtwith the OS name - Upload it as an artifact named
build-<os> - A final
reportjob 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 →