Testing Across Everything: Mastering Matrix Builds & Parallelism in GitLab CI/CD
When maintaining an open-source library, cross-platform CLI tool, or multi-tenant backend, you can never assume that code passing on Node.js 22 or Python 3.12 on Linux will run without errors on Node 18 or Python 3.10 on macOS.
Historically, testing multiple versions in CI meant one of two terrible options:
- The Sequential Slow-Down: Running tests for Python 3.10, then 3.11, then 3.12 one after another inside a single job. A 5-minute test suite turns into a 25-minute crawl.
- The Copy-Paste YAML Nightmare: Duplicating 40 lines of CI job configuration 6 times with different image tags.
Fortunately, GitLab CI/CD features native Parallel Matrix Builds (parallel: matrix:).
In this practical guide, I will show you how to write clean, concise matrix configurations that fan out across dozens of language versions, database engines, and CPU architectures in parallel.
1. How Parallel Matrix Builds Work
+-------------------------------------------------------------+
| Single Job Definition in YAML |
| |
| parallel: |
| matrix: |
| - RUNTIME: [3.10, 3.11, 3.12] |
| DATABASE: [postgres, mysql] |
+-------------------------------------------------------------+
|
v (GitLab fans out 6 parallel jobs)
+-------------------------------------------------------------+
| [Job 1] Python 3.10 + Postgres (Runs on Runner A) |
| [Job 2] Python 3.10 + MySQL (Runs on Runner B) |
| [Job 3] Python 3.11 + Postgres (Runs on Runner C) |
| [Job 4] Python 3.11 + MySQL (Runs on Runner D) |
| [Job 5] Python 3.12 + Postgres (Runs on Runner E) |
| [Job 6] Python 3.12 + MySQL (Runs on Runner F) |
| |
| * All 6 jobs execute simultaneously! Total time: ~3 mins! |
+-------------------------------------------------------------+
2. Basic Matrix: Testing Multi-Version Runtimes
Here is how to test a Node.js library across LTS releases (18-alpine, 20-alpine, 22-alpine) in parallel:
stages:
- test
test_node_matrix:
stage: test
image: node:$NODE_VERSION
parallel:
matrix:
- NODE_VERSION:
- "18-alpine"
- "20-alpine"
- "22-alpine"
script:
- node -v
- npm ci
- npm test
What You See in GitLab:
In the pipeline graph, GitLab automatically labels the dynamically created jobs:
test_node_matrix: [18-alpine]test_node_matrix: [20-alpine]test_node_matrix: [22-alpine]
Each job runs concurrently on available runners, cutting test feedback latency by 66%.
3. Multi-Dimensional Matrix: Runtimes × Databases
When your backend supports multiple database backends, combine multiple variables into a 2D matrix:
test_backend_grid:
stage: test
image: python:$PYTHON_VERSION-slim
services:
- name: $DB_IMAGE
alias: db
parallel:
matrix:
- PYTHON_VERSION: ["3.11", "3.12"]
DB_IMAGE: ["postgres:16-alpine", "mariadb:11.4"]
variables:
POSTGRES_DB: test_db
POSTGRES_PASSWORD: secretpassword
MARIADB_DATABASE: test_db
MARIADB_ROOT_PASSWORD: secretpassword
script:
- python -V
- pip install -r requirements.txt
- pytest tests/
GitLab computes the Cartesian product ($2 \times 2 = 4$ jobs), provisioning isolated ephemeral database service containers for each permutation.
4. Complex Permutations & Specific Exclusions
What if a specific runtime combination is unsupported (e.g., Python 3.10 is incompatible with a modern library requiring Python 3.11+)?
You can define multiple independent matrix arrays within the same block:
test_compatibility:
stage: test
image: $BASE_IMAGE
parallel:
matrix:
# Block 1: Linux x86_64 across full stack
- BASE_IMAGE: ["ubuntu:24.04", "debian:12-slim"]
ARCH: ["amd64"]
# Block 2: ARM64 targets (scoped only to Alpine)
- BASE_IMAGE: ["alpine:3.20"]
ARCH: ["arm64"]
script:
- echo "Compiling binary for $BASE_IMAGE on $ARCH..."
5. Parallel Count vs. Matrix (Fast Test Sharding)
If you don't have multiple language versions, but your test suite has 2,000 slow unit tests, you can use integer test sharding (parallel: N):
shard_tests:
stage: test
image: ruby:3.2
parallel: 4 # Spawns 4 parallel runners: 1/4, 2/4, 3/4, 4/4
script:
# Use knapsack or rspec test sharding
- bundle exec rspec $(bin/split_specs --index $CI_NODE_INDEX --total $CI_NODE_TOTAL)
$CI_NODE_INDEX (e.g. 1) and $CI_NODE_TOTAL (e.g. 4) are injected automatically by GitLab, allowing your test runner to slice the test directory evenly across runners.
6. Key Takeaways
-
Eliminate Duplicated YAML: Let GitLab generate Cartesian product permutations dynamically using
parallel: matrix:. - True Concurrent Feedback: Multi-version validation finishes in the time of a single run.
-
Combine with Caching: Ensure dependency caches are partitioned by runtime version (
key: "$CI_JOB_NAME") to prevent cross-version cache poisoning.
Resources & Links
- GitLab CI/CD Matrix Documentation: docs.gitlab.com/ee/ci/yaml/#parallelmatrix
- GitLab Profile: gitlab.com/hrlpavan
- GitLab Contributor Profile: contributors.gitlab.com/users/hrlpavan
- GitHub: @hrlpavan
How many permutations does your test suite run across? Share your matrix optimization tricks in the comments below!
Top comments (0)