Drone CI’s matrix builds are a powerful way to test your code against various environments and dependencies without duplicating your pipeline configuration.
Let’s see it in action. Imagine you have a Go application that needs to be tested with different versions of Go and potentially on different operating systems. Here’s a simplified drone.yml that accomplishes this:
kind: pipeline
type: docker
name: default
matrix:
go:
- 1.18
- 1.19
- 1.20
os:
- linux
- darwin
steps:
- name: build and test
image: golang:${go}-${os}
commands:
- go version
- go test ./...
When Drone processes this pipeline, it doesn’t just run it once. It creates a combination of the matrixed values. In this case, it will spin up six distinct build environments:
- Go 1.18 on Linux
- Go 1.18 on Darwin (macOS)
- Go 1.19 on Linux
- Go 1.19 on Darwin
- Go 1.20 on Linux
- Go 1.20 on Darwin
For each of these combinations, the image in the build and test step will be dynamically constructed using the current matrix values. So, for the first build, the image will be golang:1.18-linux, for the second golang:1.18-darwin, and so on. The go version command will then output the specific Go version for that build, and go test ./... will execute your tests within that isolated environment.
The core problem matrix builds solve is managing combinatorial complexity in testing. Instead of writing separate pipelines or duplicating test steps for every Go version or OS you want to support, you define the variations once in the matrix block. Drone then handles the orchestration, creating and running individual pipeline executions for each unique combination. This dramatically reduces configuration overhead and makes your CI setup much more maintainable.
Internally, Drone achieves this by expanding the matrix at the pipeline definition stage. Before any build actually starts, Drone parses the matrix block, generates all possible permutations of the defined variables, and then creates separate, independent pipeline executions for each permutation. Each of these individual pipelines receives the specific matrix values as environment variables (e.g., DRONE_MATRIX_GO and DRONE_MATRIX_OS in the example above, though here we used direct substitution in the image for brevity). This isolation ensures that tests run in one matrix configuration don’t affect others.
The matrix block can also include more than just simple lists. You can define complex objects, and Drone will create combinations based on the keys and values within those objects. For instance, if you had different database versions to test against:
matrix:
database:
- name: postgres
version: 14
- name: mysql
version: 8
Drone would iterate through each object in the database list. For each iteration, the entire object becomes available within the pipeline’s environment, allowing you to reference its properties like [[ .matrix.database.version ]] or [[ .matrix.database.name ]] in your steps. This provides fine-grained control over complex testing scenarios.
A common misconception is that matrix builds are a performance optimization. They are not. They are an expressiveness and maintainability feature. Each matrix combination represents a distinct, independent build. If you have a matrix with 10 items, you are effectively running 10 separate builds. The benefit comes from defining those 10 builds with a single, concise configuration, rather than repeating the same steps multiple times.
The next frontier in CI complexity often involves managing secrets and credentials across these multiple build environments.