In a GitHub Actions strategy matrix, what do include and exclude actually do?
answer
- arrays expand to every pairing
- one removes, one adds or enriches
- the adder has two different behaviours
- the order between them surprises people
- the adder can work with no arrays at all
basics
~20 sThe matrix's array variables expand to the cross product of every combination, one job each. exclude removes combinations from that product, and include then adds extra values to matching combinations or appends whole new ones. Exclude is processed first, so include can add a combination back.
solid answer
~40 sA `strategy.matrix` block with two arrays produces one job per combination — three OS values by two Java versions is six jobs, each with the values available in the `matrix` context. `exclude` deletes specific combinations from that product, which is how you drop a pairing that is not supported. `include` does two different things depending on the object: if all its matrix keys match an existing combination, its extra keys are merged into that job (adding, say, a flag or a label to just one leg); if it introduces values that would overwrite an original matrix value or it has no matrix keys at all, it appends a brand-new job. The ordering rule matters in interviews: exclusions are applied first and inclusions afterwards, so an `include` can restore a combination that `exclude` removed.
code
yaml · 25 linesjobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
java: [17, 21]
exclude:
- os: macos-latest
java: 17 # unsupported pairing: 6 -> 5 jobs
include:
- os: ubuntu-latest
java: 21
coverage: true # merges into that existing job
- os: ubuntu-latest
java: 25 # not in the array: appends a 6th job
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
- run: ./gradlew test
- if: matrix.coverage
run: ./gradlew jacocoTestReportgo deeper
Know that matrix arrays produce one job per combination and that the values are read through the matrix context, for example in runs-on: ${{ matrix.os }}.
Explain both include behaviours — merging extra keys into matching combinations versus appending a new one — and state that exclusions are processed before inclusions.
Discuss the consequences: distinct job names feeding required checks, matrix values belonging in cache keys, the 256-job ceiling, and preferring an include-only list when the supported set is sparse.
Own what the matrix is for: which dimensions genuinely reduce risk, what each extra dimension costs in queue time and feedback latency, and how full coverage is shifted to a scheduled run rather than every pull request.
## The cross product strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] java: [17, 21] Each key holds an array; the matrix expands to every combination — six jobs here. Each job is a copy of the job definition with `matrix.os` and `matrix.java` bound to its own values, usable anywhere expressions are allowed, including `runs-on: ${{ matrix.os }}` and in step inputs. A single workflow run is capped at 256 jobs generated by one matrix, which is a real ceiling for large multi-dimensional matrices. ## exclude `exclude` removes combinations. Every key/value pair listed must match for a combination to be dropped, and a partial specification removes every combination that matches it: exclude: - os: macos-latest java: 17 # drops exactly one job - os: windows-latest # drops every windows job This is how you express "this pairing is unsupported" without restructuring the arrays. ## include `include` is the more subtle one, and the rule has two branches. **Merging into existing combinations.** If an include object's keys that also exist in the matrix all match a generated combination, its remaining keys are added to that job — as long as they do not overwrite an original matrix value: include: - os: ubuntu-latest java: 21 coverage: true # only that one job gets matrix.coverage An object with no matrix keys at all merges into **every** combination, which is a handy way to add a shared value. **Appending new combinations.** If the object cannot be merged — typically because it sets a matrix key to a value not in the original array — it becomes an additional job: include: - os: ubuntu-latest java: 25 # 25 is not in the java array: a seventh job This is the standard way to add one experimental leg without widening the product. ## Order of processing Exclusions are applied to the expanded product first; inclusions are processed afterwards. The practical consequence is that an `include` can add back a combination that `exclude` just removed, which reads as contradictory in a diff but is well-defined. Interviewers ask because engineers routinely assume the opposite ordering and then cannot explain why an excluded combination still ran. ## Using a matrix without a matrix `include` alone — with no array keys — is a legitimate and common pattern: it defines an explicit list of job configurations rather than a product. strategy: matrix: include: - os: ubuntu-latest java: 21 label: fast - os: windows-latest java: 17 label: compat This is clearer than a product plus a wall of exclusions when the supported combinations are sparse. ## Practical notes - The generated jobs get distinct names built from their matrix values, which is what makes a failing leg identifiable in required-check lists; renaming keys changes those job names. - Matrix values feed cache keys too — include the values that affect dependencies, or matrix legs will fight over one cache entry. - Matrix values are also available in `if:` conditions, so a step can be skipped on one leg without a separate job. - Keep the product honest: every combination is a full job with its own checkout, setup, and dependency install, so an extra dimension multiplies queue time and cost rather than adding it. ## The concise answer "The arrays expand to the cross product; `exclude` prunes combinations from it; `include` either enriches matching combinations or appends new ones; and because exclude runs first, include can put a combination back."
- If a combination is both excluded and included in a GitHub Actions matrix, does it run?Yes. Exclusions are applied to the expanded product first, then inclusions are processed, so an `include` entry re-adds the combination. It is well-defined but confusing to read, so it is better to restructure the arrays than to rely on the interaction.
- What happens when an include object has no keys in common with the matrix arrays?Its keys are merged into every generated combination, so each job gains those extra values in the `matrix` context. It is a compact way to attach a shared setting to the whole matrix, though a job-level `env:` is usually clearer unless the value is genuinely per-combination.
- When would you define a matrix using only include, with no array keys?When the supported configurations are sparse rather than a clean product — for instance one Linux job on the newest toolchain and one Windows job on the oldest. Listing them explicitly is easier to read than a full cross product pruned by several exclusions, and it avoids accidentally generating unsupported pairings.
- How do matrix values interact with cache keys?They must appear in the key whenever they change the cached content. If two legs with different toolchain versions share a key, one leg saves an entry the other then restores, producing wrong dependencies or cross-platform breakage. Put `runner.os` and the relevant matrix values into both the key and the restore-key prefix.
saying these in an interview costs you the question
- Thinks include is processed before exclude
- Believes include always appends a new job
- Assumes exclude needs every matrix key specified
- Expects matrix jobs to share one workspace
- Reuses one cache key across all matrix legs