Matrix Builds - Testing Across Versions and Architectures
Status: Active
Last Updated: 2026-08-26
Category: CI/CD - Phase 2: Pipelines
Prerequisites: woodpecker-first-pipeline, woodpecker-installation
Time: 2 hours
Tags: woodpecker, matrix, builds, testing, arm64, multi-arch
Summary
Woodpecker's matrix strategy fans a single pipeline definition out into multiple parallel executions โ one per combination of variables. Use it to test your code against several language versions, run on multiple architectures, or build multi-platform container images from one YAML file.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ
Define a matrix in
.woodpecker.ymlwithmatrix:andmatrix:include lists - โ Test against multiple language/runtime versions in parallel
- โ Build ARM64 images alongside AMD64 images
- โ Control concurrency so your CI runner isn't overwhelmed
Table of Contents
- Context / Why This Matters
- Implementation / Core Content
- Practical Examples
- Troubleshooting & Common Pitfalls
- Next Steps / Ops Actions
- Sources & Related
Context / Why This Matters
After completing woodpecker-first-pipeline you have a single pipeline that tests one configuration. Real projects rarely target exactly one runtime version โ a Python library may need to support 3.10 through 3.13, and homelab services increasingly need ARM64 images for Raspberry Pi or low-power mini PCs alongside x86_64.
Without matrix builds you duplicate pipeline steps manually ("test-310", "test-311", ...) and keep the copies in sync by hand. The matrix feature makes Woodpecker do that duplication for you: one step definition, N executions, each with different environment variables substituted into the image tag and commands.
Implementation / Core Content
Basic matrix syntax
Add a top-level matrix: block to .woodpecker.yml. Woodpecker generates one pipeline instance per combination of values and injects each combination as environment variables:
matrix:
PYTHON_VERSION:
- "3.11"
- "3.12"
- "3.13"
steps:
test:
image: python:${PYTHON_VERSION}
commands:
- pip install -r requirements.txt
- pytest -v
This produces three parallel pipelines: python:3.11, python:3.12, and python:3.13. The variable is substituted anywhere it appears in the step definition โ including the image: field.
Multi-dimensional matrices
Multiple keys produce the cartesian product. Three Node versions times two databases equals six pipelines:
matrix:
NODE_VERSION:
- "20"
- "22"
- "24"
DATABASE:
- postgres16
- sqlite
steps:
test:
image: node:${NODE_VERSION}
environment:
TEST_DB: ${DATABASE}
commands:
- npm ci
- npm test
Include-based matrices (skip unwanted combinations)
When the full product contains combinations you don't support, use the include form to list only the pairs you want:
matrix:
include:
- GO_VERSION: "1.22"
OS: linux
ARCH: amd64
- GO_VERSION: "1.22"
OS: linux
ARCH: arm64
- GO_VERSION: "1.23"
OS: linux
ARCH: amd64
Each list entry is one pipeline; there is no cross product. This is the recommended style once you have two or more dimensions, because it is explicit about what actually gets built.
Branch/event filtering per matrix element
You can limit heavy matrix legs to certain events using standard step-level when: conditions combined with ${MATRIX_*} checks, keeping quick configurations on every push and slow ones on tags only:
steps:
test:
image: golang:${GO_VERSION}
commands:
- go test ./...
integration-heavy:
image: golang:${GO_VERSION}
when:
event: [tag]
commands:
- go test -tags=integration ./...
Concurrency control
Every matrix leg counts as a separate build against your runner's WOODPECKER_MAX_WORKFLOWS (default per agent). With an 8-leg matrix on a single agent configured for 2 concurrent workflows, six legs will queue. Options:
- Raise agent concurrency if CPU/RAM allows (
WOODPECKER_MAX_WORKFLOWS=4in the agent container env). - Trim the matrix with
include:to what you genuinely support. - Keep expensive legs behind
when: event: [tag]as shown above.
Check current capacity on the runner host:
docker inspect woodpecker-agent --format '{{.Config.Env}}' | tr ' ' '\n' | grep MAX_WORKFLOWS
Naming and identifying matrix legs
In the Woodpecker UI, each leg appears as a separate workflow under the same commit, labeled with its matrix values (e.g. test - MATRIX_PYTHON_VERSION=3.12). Status checks reported back to Forgejo carry these labels, which matters for branch protection: protect on the step name, not the matrix value.
Practical Examples
Example 1: Multi-version library testing
when:
event: [push, pull_request]
matrix:
include:
- PYTHON_VERSION: "3.10"
- PYTHON_VERSION: "3.12"
- PYTHON_VERSION: "3.13"
steps:
lint:
image: python:${PYTHON_VERSION}
commands:
- pip install ruff
- ruff check .
test:
image: python:${PYTHON_VERSION}
commands:
- pip install -e . pytest
- pytest --tb=short
Expected result: three workflows per push, each visible independently. A failure isolated to 3.10 does not mask passing results on 3.12/3.13.
Example 2: Multi-arch image build
matrix:
include:
- PLATFORM: linux/amd64
GOARCH: amd64
- PLATFORM: linux/arm64
GOARCH: arm64
steps:
build:
image: plugins/docker
settings:
repo: registry.fogserv.cloud/homelab/myapp
tags: "${CI_COMMIT_TAG}-$$ARCH" # note $$ escaping in Woodpecker
platform: ${PLATFORM}
username:
from_secret: registry_user
password:
from_secret: registry_pass
manifest:
image: plugins/manifest
when:
event: [tag]
settings:
spec_manifest_template: >-
registry.fogserv.cloud/homelab/myapp:${CI_COMMIT_TAG}
username:
from_secret: registry_user
password:
from_secret: registry_pass
The manifest step runs after all matrix legs publish their arch-specific tags and assembles a single multi-arch manifest. See container-registry-integration for registry setup.
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| Only one pipeline runs | Matrix block has wrong indentation (must be top level, not inside steps) | Validate YAML nesting; matrix: sits at document root |
${VAR} literal in logs instead of substitution |
Woodpecker requires $${...} escaping where you want the shell to expand it |
Use $${} when the substitution belongs to the container shell, plain ${} for Woodpecker substitution |
| Runner queue backlog after adding matrix | More legs than agent concurrency | Increase WOODPECKER_MAX_WORKFLOWS or trim matrix via include: |
| Secrets missing on some legs | Secret restricted to specific events/images not matching the matrix image | Re-check secret restrictions in Woodpecker UI; see ci-secrets-management |
| Manifest step races arch builds | No dependency declared | Add depends_on: [build] (workflow graph mode) so manifest waits |
Next Steps / Ops Actions
- Wire required matrix status checks into Forgejo branch protection: branch-protection
- Cache dependencies between matrix legs to cut total time: caching-strategies
- Publish scanned, signed images from matrix builds: security-scanning
Sources & Related
External references consulted:
Related knowledge-base articles:
- woodpecker-first-pipeline
- woodpecker-pipeline-testing
- caching-strategies
- branch-protection
- container-registry-integration
Change Log
2026-08-26
- Initial creation by KB writing session.