Continuous integration
mtest is built for a machine to read: deterministic ordering, a failure and a crash that stay distinct all the way to the exit code, and report formats a CI system already understands. Wiring it into a hosted pipeline is one job.
The workflow
Add the package to the workspace, commit the manifest and lock file that produces, then paste this. It installs the locked environment, runs the suite with inline annotations and a JUnit report, and keeps the report as an artifact even when the run fails:
name: Tests
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0
with:
locked: true
- run: >-
pixi run mtest tests
--gh-annotations auto
--junit-xml build/test-results.xml
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: test-results
path: build/test-results.xml
Annotations are requested in the mode that emits them on GitHub Actions and
does nothing anywhere else, so the same invocation is the one to run locally.
GitHub allows ten error and ten warning annotations per step, so a run with
more failures than that renders the first nine of each and one
... and N more errors line in place of the rest; the summary, the JUnit
report, and the exit code still count every failure. The report is written
whether or not tests failed, which is why the upload step is unconditional.
Each action above is pinned to a commit rather than to a tag, the same way this repository pins its own workflows, and yours should be too: a tag can be moved onto different code, a commit cannot.
Spreading one suite across a matrix
Give each cell a shard and a distinct report name, in the hash:M/N syntax
defined under
Sharding a CI matrix.
The union of every shard's selection is exactly the unsharded selection, and no
test runs twice:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
# ... checkout and setup-pixi as above ...
- run: >-
pixi run mtest tests
--shard "hash:${{ matrix.shard }}/4"
--gh-annotations auto
--junit-xml "build/test-results-${{ matrix.shard }}.xml"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: test-results-${{ matrix.shard }}
path: build/test-results-${{ matrix.shard }}.xml
Running it as an action
The same run is available as a composite action, if you would rather not repeat the invocation. It runs mtest and nothing else: the workflow above still installs the locked environment, and the action replaces only the step that invokes the runner.
- uses: mikeleppane/mtest@v1
with:
paths: tests
args: --gh-annotations auto --junit-xml build/test-results.xml
The second input is appended to the command verbatim, so every flag the runner accepts stays reachable without the action growing an input of its own. Both inputs reach the runner through the environment and are split on whitespace, so a value containing a space cannot be held together by quoting it here. The major-version tag floats onto each 1.x release; reference a commit instead if you would rather adopt each release deliberately.
One thing not to do
Do not restore the build cache between hosted runners. The failure it produces is a valid cache hit that dies the moment it executes, and the reasoning is specific enough to be worth reading in full rather than paraphrased here: it is in the Run it in CI section of the README. Cache the package downloads instead.