Matrix Axes, Labels, and Capacity
Scenario: Copied stages test several runtimes but silently omit one supported operating-system combination.
Testing every combination without writing every combination by hand
Imagine needing to test your application on 2 operating systems times 3 runtime versions - that's 6 separate test runs. Writing out 6 nearly-identical stage blocks by hand is tedious and error-prone (easy to accidentally miss one combination, or fix a bug in five copies but forget the sixth). matrix exists to generate exactly this kind of combinatorial grid automatically from a much shorter declaration.
Analogy: Think of a restaurant's "build your own bowl" menu: pick one of 2 bases and one of 3 proteins, and the kitchen effectively supports 6 distinct meal combinations without printing 6 separate menu items - it's just base times protein, run through every pairing. Jenkins' matrix works the same way: declare your "bases" (say, operating systems) and your "proteins" (say, runtime versions) as axes, and Jenkins automatically runs your defined stages once for every combination - the Cartesian product - without you writing out each combination by hand.
The catch, covered below, is that this convenience scales multiplicatively: adding one more axis value doesn't add one more test run, it multiplies the existing count - which is easy to underestimate until your "quick matrix" is suddenly running forty jobs.
Pipeline example
matrix { axes { axis { name 'RUNTIME'; values '18','20' } }; agent { label "node-${RUNTIME}" }; stages { stage('Test') { steps { sh 'npm test' } } } }
Axes create a Cartesian test plan
Declarative matrix defines named axes and their possible values, then runs its nested stages once for every combination of those values. Two operating systems times three runtimes creates six distinct cells, each running the full nested stage set. Axis values automatically become environment variables and can be used to drive an agent expression, such as agent { label "os-${OS} && node-${RUNTIME}" } - which means your label vocabulary (what you actually name your agent labels) and your axis values together form a real scheduling contract that needs to stay in sync.
[Pipeline] { (Matrix - RUNTIME = '18', OS = 'linux')
[Pipeline] sh
+ npm test
Node 18 on linux: 42 passed
[Pipeline] }
[Pipeline] { (Matrix - RUNTIME = '20', OS = 'linux')
[Pipeline] sh
+ npm test
Node 20 on linux: 42 passed
[Pipeline] }
Each matrix "cell" shows up in the log (and in the visual stage view) individually labeled with its own axis values - this is exactly what makes it possible to spot, at a glance, that (say) only the OS = 'windows' cells are failing, rather than a single ambiguous "tests failed" result covering every combination at once.
Excludes remove combinations that don't make sense
excludes removes unsupported combinations directly in the Pipeline's declared structure, before Jenkins even attempts to schedule them. An exclude combines axis filters together, while notValues can express "all values except these specific ones" concisely. Explicitly excluding, say, Windows paired with runtime 18 (because that combination isn't actually supported) is far clearer to a future reader than a shell-level condition buried inside a stage that silently no-ops for that combination while still technically "running" and reporting a misleading green result. Review the total generated cell count carefully whenever adding a new axis - Cartesian growth is genuinely multiplicative, and it's easy to go from "6 cells, totally reasonable" to "40 cells" by adding what felt like one small new dimension, quickly consuming far more executors and third-party service quota than intended.
Preserve each cell's own identity
Matrix cells can each have their own agent, environment, options, when, stages, and post behavior, independently. Put the actual axis values into report file names, stash names, and artifact names - otherwise two cells running concurrently can silently overwrite each other's output if they happen to write to the same unlabeled path. Decide on failFast deliberately, the same way as with plain parallel: is saving compute by stopping early worth losing the complete compatibility picture across every combination?
A pitfall: a missing cell that nobody noticed for months
For a cell that appears to be silently missing from a matrix run, inspect excludes and any when conditions first - a genuinely common cause. For a cell that's stuck queued rather than missing, read its fully-expanded label (with the actual axis values substituted in) and check whether any online agent actually satisfies it. Track results by combination over time, not just as one aggregate pass/fail count for the whole matrix run - a failure that's isolated consistently to, say, Windows paired with one specific JDK version is genuinely actionable evidence that a single aggregate "matrix passed" or "matrix failed" badge completely conceals from anyone glancing at the dashboard.
Goal: Practice the concrete behavior in jenkins-labeled-parallel-tests. Learn sections have no Jenkins terminal; the Jenkins lab opens the real shared service in a new full-screen tab inside your private folder.