Generates the prebuild matrix for CI: combinations of an nbpr_* package
under packages/ with a target system declared in the workspace mix.exs
@prebuild_systems map.
mix nbpr.matrix [--json] [--changed-since <ref>] [--root <path>]Scoping by change
Rebuilding an artefact whose cache key hasn't moved achieves nothing —
mix nbpr.publish treats a published tarball as immutable and
short-circuits — so CI passes --changed-since <ref> and gets only the
work the diff implies:
- a
packages/nbpr_<name>/file changed — that package, every target. Its version or metadata moved, so every target's artefact is stale. - a
@prebuild_systemspin in the workspacemix.exschanged — every package, but only the targets whose version moved. The system version is part of the cache key, so a bump invalidates that target's artefacts across the board and leaves every other target alone. - anything else that can change how a build runs — the
:nbprlibrary itself, this workflow — every package against--default-targetonly. A library change doesn't invalidate a cache key, so this is smoke coverage rather than a rebuild: broad across packages, one target deep.
Entries are deduplicated, so a package that qualifies twice is built once per target.
Without --changed-since the full cross-product is emitted, which is
what workflow_dispatch wants for a deliberate from-scratch rebuild.
GitHub's 256-configuration cap
GitHub refuses to expand a single job's strategy past 256 configurations. Two probes established what's actually enforced, since the documented "256 jobs per workflow run" isn't it:
- two sibling jobs of 200 and 100 configurations expanded all 300 — so the cap is per strategy, not per run;
- an outer matrix of 2 driving a reusable workflow with a 200-entry
inner matrix expanded all 400 — so
strategyis honoured on auses:job, and each instantiated inner strategy gets its own budget.
So --slices emits the outer half of a two-level matrix, one entry per
target, each carrying its own inner matrix. Capacity becomes 256 targets
× 256 packages instead of 256 in total, and adding packages never needs
this revisited.
--max still guards each slice, because an oversized strategy fails the
run at strategy-evaluation time with no failing check to show for it:
the run goes red while every check stays green and branch protection
waves it through.
Output
Without flags: human-readable lines, one per matrix entry.
With --json: a single line of GitHub Actions matrix JSON, ready to
feed into a job's strategy.matrix via dynamic-matrix:
jobs:
generate:
outputs:
matrix: ${{ steps.gen.outputs.matrix }}
steps:
- id: gen
run: echo "matrix=$(mix nbpr.matrix --json)" >> "$GITHUB_OUTPUT"
build:
needs: generate
strategy:
matrix: ${{ fromJson(needs.generate.outputs.matrix) }}
steps:
- run: MIX_TARGET=${{ matrix.target }} mix nbpr.build ${{ matrix.module }} -o out/An empty matrix is a legitimate result — a docs-only diff builds nothing.
{"include": []} makes GitHub skip the job, so gate it on --count
instead if the distinction matters to the workflow.
Flags
--json— emit{"include": [...]}JSON suitable for GHA dynamic matrix--slices— emit the outer matrix of a two-level fan-out: one entry per target, each carrying its target's inner matrix as a string. This is what CI feeds tobuild-slice.yml.--count— emit just the number of entries, for a workflow that needs to know whether there's work before defining a job--changed-since <ref>— scope to the work implied bygit diff <ref>...HEAD, per the rules above--default-target <target>— the target used for smoke coverage of library and workflow changes (defaults torpi4)--target <target>— restrict to one target--package <name>— restrict to one package, with or without thenbpr_prefix--max <n>— refuse to emit more thannentries (defaults to 256, GitHub's per-job cap).0disables the check. Failing here is the point: an oversized matrix makes GitHub fail the run at strategy-evaluation time, which produces no failing check and so doesn't block a merge.--root <path>— workspace root (defaults to current directory). Useful when running this task from a script that doesn'tcdfirst.
Summary
Functions
Returns the targets whose @prebuild_systems pin differs between two
mix.exs texts — the set whose artefacts a version bump invalidated.
Filters entries down to the work implied by a diff. Pure, so the scoping
rules are testable without a git repository.
Groups entries into one slice per target, each carrying its own inner
matrix JSON as a string.
Functions
Returns the targets whose @prebuild_systems pin differs between two
mix.exs texts — the set whose artefacts a version bump invalidated.
Filters entries down to the work implied by a diff. Pure, so the scoping
rules are testable without a git repository.
changed_paths are repo-relative paths. changed_targets are the targets
whose @prebuild_systems pin moved. default_target carries the smoke
coverage for changes that affect how builds run without invalidating any
cache key.
Groups entries into one slice per target, each carrying its own inner
matrix JSON as a string.
This is the outer half of a two-level matrix. GitHub caps a single job's strategy at 256 configurations, but a reusable workflow called from a matrix'd job instantiates a fresh inner job with its own budget — verified by probe: an outer matrix of 2 driving an inner matrix of 200 expanded all
- So capacity multiplies rather than adds, and the cap stops being something this repo has to plan around.
Slicing by target rather than by arbitrary shards means a slice is exactly
one (system, system_version), which is what a build job's caches are keyed
on anyway. The inner matrix is embedded as a string deliberately: the
workflow then reads ${{ matrix.matrix }} straight out of the outer matrix
context, with no JSON-object indexing in a with: block.