repomatic.matrix.model module¶

GitHub Actions job-matrix model.

Variations, includes, excludes, and their expansion into the JSON payload workflow strategy.matrix keys consume.

repomatic.matrix.model.GITHUB_MATRIX_JOB_LIMIT = 256¶

Most jobs GitHub expands one strategy.matrix into.

GitHub refuses a matrix past this size, so Matrix.solve() reports one before a workflow meets the refusal.

repomatic.matrix.model.JOB_STATE_KEY = 'state'¶

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

Matrix.pivot_counts() tallies it in each cell, absent a caller’s choice.

repomatic.matrix.model.OS_AXIS = 'os'¶

Job key naming the runner image: the default grid column.

repomatic.matrix.model.PYTHON_VERSION_AXIS = 'python-version'¶

Job key naming the interpreter version: the default grid row.

repomatic.matrix.model.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.matrix.model.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.matrix.model.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 a new dict 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()[source]¶

Returns a copy of the matrix, with its include and exclude directives.

A directive list that holds nothing is left out, as GitHub expects. The copy is detached from the model: changing it changes no axis here.

Return type:

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

add_variation(variation_id, values)[source]¶

Extend the axis variation_id with values, dropping duplicates.

Raises:

ValueError – When values holds nothing, or holds something other than a string.

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 later duplicate is removed.

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_includes=False)[source]¶

Collect every axis of the matrix, with its values.

Parameters:

with_includes (bool) – Also collect the keys and values the include directives name, after the declared axes.

Return type:

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

Returns:

Each key mapped to its distinct values, in first-seen order.

product()[source]¶

Yield every combination of the declared axes, before any directive.

Respects the order of the axes and of their values.

Return type:

Iterator[dict[str, str]]

solve()[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.

A matrix expanding past GITHUB_MATRIX_JOB_LIMIT jobs is logged as critical, since GitHub refuses to run it.

Return type:

Iterator[dict[str, str]]

job_keys()[source]¶

Every key a job of the solved matrix carries, sorted.

Read off the solved jobs rather than off the declared axes, so a key only an include directive sets is listed, and a key whose directive an exclude cancelled is not. That makes it the set of keys pivot_counts() can lay a grid out on.

Return type:

tuple[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.

A grid has two axes, so a matrix varying on a third lands several jobs on one intersection. The counts keep them apart: a cell standing for five jobs of one state and a cell standing for one job are told apart by their count.

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.