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:


Table of Contents

  1. Context / Why This Matters
  2. Implementation / Core Content
  3. Practical Examples
  4. Troubleshooting & Common Pitfalls
  5. Next Steps / Ops Actions
  6. 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:

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

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse