GitHub Actions runs-on From a Matrix Expression Not Resolving
A dynamic runs-on driven by a matrix value or expression fails to match a runner because the expression resolves to the wrong shape - a string where a label array was needed, or an unresolved value.
What this error means
A job using runs-on: ${{ matrix.os }} (or a fromJSON expression) never matches a runner or errors at start, because the resolved value is not a valid label or label array.
strategy:
matrix:
runner: ['["self-hosted","gpu"]']
runs-on: ${{ matrix.runner }} # a string, not an array of labelsCommon causes
Expression resolves to the wrong type
runs-on accepts a string, a label array, or a group object. An expression that yields a JSON string instead of an array does not become a label set.
Matrix value missing for some legs
If the matrix key feeding runs-on is absent in some combinations (e.g. only added via include), those legs get an empty runs-on and cannot match.
How to fix it
Resolve to a real label or array
Use a plain string for a single label, or fromJSON to produce a real label array.
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }} # single label string
# for an array of labels:
runs-on: ${{ fromJSON('["self-hosted","gpu"]') }}Ensure the matrix key is always present
- Define the runs-on key in the base matrix so every leg has it.
- Use fromJSON when you need an actual array of labels from a string.
- Echo the resolved value in a debug step to confirm its shape.
How to prevent it
- Drive runs-on from a matrix key present in every combination.
- Use fromJSON to turn a JSON string into a real label array.
- Verify the resolved runs-on shape before relying on it.