repomatic.github.matrix module

GitHub Actions job-matrix model: variations, includes, excludes, and their expansion into the JSON payload workflow strategy.matrix keys consume.

repomatic.github.matrix.JOB_STATE_KEY = 'state'

Job key carrying whether a cell runs as stable or unstable.

Matrix.pivot() reads it into each cell, absent a caller’s choice.

repomatic.github.matrix.OS_AXIS = 'os'

Job key naming the runner image, laid out as Matrix.pivot() columns.

repomatic.github.matrix.PYTHON_VERSION_AXIS = 'python-version'

Job key naming the interpreter version, laid out as Matrix.pivot() rows.

repomatic.github.matrix.PIVOT_CELL_SEPARATOR = ', '

Joins the distinct states Matrix.pivot() finds at one intersection.

Exposed so a caller rendering the grid can split a cell back into its states and decorate each one, instead of hard-coding the separator on its side and letting the two drift.

repomatic.github.matrix.RESERVED_MATRIX_KEYWORDS = ('include', 'exclude')

Keys GitHub reserves inside a strategy.matrix block.

Neither can name a variation axis, since both already mean something to the matrix expander. Matrix._check_ids() rejects them.

repomatic.github.matrix.stale_axis_values(entry, axes)[source]

Return the entry key/value pairs absent from the matrix axes.

A non-empty result means an exclude directive can never match a combination: one of its keys is not a live axis, or its value is absent from that axis. Matrix.prune() drops such a directive silently, since GitHub rejects a matrix whose excludes name unknown keys; this is the predicate behind that decision, exposed so callers can also report the drift instead of only absorbing it (see Metadata.stale_test_matrix_excludes).

Return type:

dict[str, str]

class repomatic.github.matrix.Matrix[source]

Bases: object

A matrix as defined by GitHub’s actions workflows.

See GitHub official documentation on how-to implement variations of jobs in a workflow.

Note

Why matrices are pre-computed in the metadata job

GitHub Actions matrix outputs are not cumulative — the last job in a matrix wins (community discussion). This makes a matrix-based job terminal in a dependency graph: no downstream job can depend on its aggregated outputs.

The workaround is a single preliminary metadata job that computes all matrices upfront. Downstream jobs depend on that job and consume the pre-built matrices, rather than computing them themselves.

A matrix starts empty and is populated through its own methods, never through the constructor:

matrix() renders the result as an immutable FrozenDict for serialization, and __getitem__() reads a single axis, but the object itself is not a mapping: it holds axes, includes and excludes as separate state.

The implementation respects the order in which items were inserted. This provides a natural and visual sorting that should ease the inspection and debugging of large matrix.

variations: dict[str, tuple[str, ...]]
include: tuple[dict[str, str], ...]
exclude: tuple[dict[str, str], ...]
matrix(ignore_includes=False, ignore_excludes=False)[source]

Returns a copy of the matrix.

The special include and excludes directives will be added by default. You can selectively ignore them by passing the corresponding boolean parameters.

Return type:

FrozenDict[str, tuple[str, ...] | tuple[dict[str, str], ...]]

add_variation(variation_id, values)[source]
Return type:

None

replace_variation_value(variation_id, old, new)[source]

Replace a single value within a variation axis.

The new value takes the position of the old value. If the new value already exists elsewhere in the axis, the duplicate is removed by boltons.iterutils.unique().

Silently skips if the axis does not exist or does not contain the old value, making the operation idempotent.

Return type:

None

remove_variation_value(variation_id, value)[source]

Remove a single value from a variation axis.

If the axis becomes empty after removal, it is deleted entirely.

Silently skips if the axis does not exist or does not contain the value, making the operation idempotent.

Return type:

None

add_includes(*new_includes)[source]

Add one or more include special directives to the matrix.

Return type:

None

add_excludes(*new_excludes)[source]

Add one or more exclude special directives to the matrix.

Return type:

None

prune()[source]

Remove no-op exclude directives and log about them.

An exclude is a no-op when it references a key that is not a variation axis at all, or when the key exists but the value is not present in that axis. Either way the exclude can never match any combination produced by product(), and GitHub Actions rejects excludes that reference non-existent matrix keys.

Return type:

None

all_variations(with_matrix=True, with_includes=False, with_excludes=False)[source]

Collect all variations encountered in the matrix.

Extra variations mentioned in the special include and exclude directives will be ignored by default.

You can selectively expand or restrict the resulting inventory of variations by passing the corresponding with_matrix, with_includes and with_excludes boolean filter parameters.

Return type:

dict[str, tuple[str, ...]]

product(with_includes=False, with_excludes=False)[source]

Only returns the combinations of the base matrix by default.

You can optionally add any variation referenced in the include and exclude special directives.

Respects the order of variations and their values.

Return type:

Iterator[dict[str, str]]

solve(strict=False)[source]

Expand the matrix to explicit jobs, applying exclude then include.

Reproduces GitHub’s documented matrix algorithm:

  1. Build the cross-product of the base variations.

  2. Drop every combination matching an exclude directive. A directive matches when all of its keys equal the combination’s, so a partial directive removes a whole slice.

  3. Process include directives in order. Each is merged into every product combination it does not conflict with (it conflicts when it would overwrite an original axis value). A directive merging into no combination is appended as a new standalone job.

Note

include directives augment combinations from the base cross-product only, never jobs created by an earlier include. An excluded combination is resurrected solely when an include fully re-specifies it, so it merges into nothing and is appended: a partial include that augments surviving jobs does not bring excluded slices back. GitHub remains the authoritative expander, but this follows its documented rules so downstream full-include job lists (which matrix() serializes verbatim) match what GitHub would run.

Return type:

Iterator[dict[str, str]]

pivot_counts(row_axis='python-version', col_axis='os', cell_key='state')[source]

Tally, per grid intersection, the jobs carrying each cell_key value.

The grouping pivot() renders, kept as counts rather than collapsed into a string. A caller cannot recover them from the rendered grid, where a cell holding five identical states and one holding a single job read alike, which is the whole reason a matrix varying on a third axis needs this.

Parameters:
  • row_axis (str) – Job key whose values become grid rows.

  • col_axis (str) – Job key whose values become grid columns.

  • cell_key (str) – Job key whose values are tallied.

Return type:

dict[tuple[str, str], Counter[str]]

Returns:

A (row_value, col_value) to Counter mapping, in first-seen job order, each counter keyed in first-seen order too. An intersection no job occupies is absent from the mapping rather than present with an empty counter.

pivot(row_axis='python-version', col_axis='os', cell_key='state', missing='—')[source]

Pivot the solved matrix into a 2D grid keyed by two axes.

Expands the matrix with solve(), then arranges the resulting jobs into a grid: one row per distinct row_axis value, one column per distinct col_axis value. Each cell holds the job’s cell_key value at that intersection (its state, by default), or missing when no job occupies it (an excluded combination).

Parameters:
  • row_axis (str) – Job key whose values become grid rows.

  • col_axis (str) – Job key whose values become grid columns.

  • cell_key (str) – Job key whose value fills each cell.

  • missing (str) – Placeholder for an empty (row, col) intersection.

Return type:

tuple[tuple[str, ...], tuple[tuple[str, ...], ...]]

Returns:

A (col_values, rows) pair. col_values is the ordered tuple of column values (distinct col_axis values). Each entry in rows is (row_value, cell, …), with one cell per col_values entry.

Note

Axis values keep first-seen order in the solved job stream. That matches the declared axis order for a base cross-product matrix, and the emitted job order for a flattened full-include matrix.

When several jobs share one (row, col) intersection (a matrix carrying extra axes, such as a click-version variation), their distinct cell_key values are joined with PIVOT_CELL_SEPARATOR. A matrix with only the os and python-version axes has exactly one job per cell.