Repository navigation
Description
🤖 Open Actions Strategist Agent @gjkim42
Latest verdict
- Status:
VALID - Checked at:
2026-11-03T00:20:38Z - Evidence: The default branch moved from
dcdfdf2toe9134c6. The new commits are #201 (v1 cache protocol) and #202 (paths/paths-ignore, part of #116). Neither touches matrix code:git diff dcdfdf2 e9134c6changes no line mentioning a matrix ininternal/workflow/workflow.go, and nothing underinternal/controller,internal/runner, orapi.- The failures still reproduce on
e9134c6. A temporary probe ran throughmake test TEST_PACKAGES=./internal/workflow TEST_FLAGS="-count=1 -v -run ZZProbe"and was then deleted; the working tree is clean.- GitHub's official
os×nodeexample fails withjob "test" matrix axis "node" values must be scalars. - An axis of arrays fails with
job "fuzz" matrix axis "fuzz_test" values must be scalars. - An object axis fails with
job "test" matrix axis "config" values must be scalars. - An
includeentry with an object value fails withjob "test" matrix include entry 1 values must be scalars. EvaluateMatrixon afromJSONmatrix producing{"target": [{"triple": ..., "os": ...}]}fails withjob "build" matrix axis "target" values must be scalars.include: [{OS: a, extra: "yes"}]withos: [a, b]still returns[{OS: a, extra: yes, os: a}, {OS: a, extra: yes, os: b}], so the entry is matched case-sensitively and added to both combinations.
- GitHub's official
- The code is unchanged; only line numbers moved.
validateMatrixValue,validateMatrixEntries,resolveMatrixValue, andmatrixValuesEqualmoved down by five lines.WorkflowJob.spec.matrix.valuesis stillmap[string]string.matrixStringValues/matrixDisplayNameandrerunMatrixValuesMatchstill usefmt.Sprint.PlanVersionis still 10, and the runner still exposes"matrix": plan.Matrix. The87db1d2permalinks below still show the current matrix code. - The official docs are unchanged. In github/docs, the latest commits are still
workflow-syntax.mdatc67a936,contexts.mdanddata/reusables/actions/workflows/matrix-keys-are-case-insensitive.mdat03d2e24, andexpressions.mdatb32e08f. - The corpus was rescanned after Support workflow path filters for push and pull request events #202. This candidate still ranks first among untracked gaps, but only narrowly. See the 2026-11-03 rescan.
- Structured matrix values are now the only parse-level blocker for 27 files in 13 repositories, up from 20 files in 12. Ten of the 27 use
paths/paths-ignore, which Support workflow path filters for push and pull request events #202 unblocked. - Step
timeout-minutes, this slot's earlier candidate, rose to 19 files in 13 repositories. It now leads on directly triggered repositories, 12 to 9. - The two are tied on repositories, with no clear margin either way, so the candidate is kept.
- Step
background/wait/wait-all/cancel/parallel, generally available since 2026-06-25, appear in 23 files across 7 repositories. They are the only blocker in just 1 repository.
- Structured matrix values are now the only parse-level blocker for 27 files in 13 repositories, up from 20 files in 12. Ten of the 27 use
- There is no overlap. #65 and draft #68 have not been updated since 2026-08-12. Open PR #196 is still at head
2ecffc8and touches no matrix code. Searches of open and closed issues and PRs formatrixfind no new tracking issue or PR. This issue still has no comments or assignees.
- The failures still reproduce on
Selected compatibility roadmap increment
Support structured values, meaning objects and arrays, in jobs.<job_id>.strategy.matrix axes, include, exclude, and expression-produced matrices. Carry them unchanged into the matrix context. Match include and exclude against them as documented.
The assessed area is matrix strategy values: how the parser, planner, controller, and runner represent matrix values and match include/exclude entries against them. The coverage pattern is a mature scalar implementation: axes, include/exclude, dynamic matrices, max-parallel, fail-fast, and reruns all work. But a single scalar-only representation runs through every layer, and it rejects a pattern the official reference documents with its own example. Rust and C++ projects use this pattern heavily for build targets and configuration matrices.
Real workloads, pinned to the commits that were scanned. The open-webui, whisper.cpp, tauri, and bat workflows are triggered directly by events. The Prometheus and protobuf workflows are reusable workflows triggered only by workflow_call, so running them unchanged also requires support for their callers (#23).
- open-webui/open-webui
docker.yamlruns onpushand is Linux-only. It crosses two object axes and reads them throughruns-on: ${{ matrix.platform.runner }}, the step conditionif: matrix.variant.free_disk, andwithvalues. - ggml-org/whisper.cpp
docker.ymlruns onscheduleand is Linux-only. It uses an axis of objects inruns-on: ${{ matrix.config.runs_on }}and inwithvalues such asmatrix.config.dockerfile. - tauri-apps/tauri
docker.ymlruns onworkflow_dispatchand is Linux-only. It uses an axis of objects read throughmatrix.target.name. - sharkdp/bat
CICD.ymluses the commonmatrix.job: [{target, os, use-cross}]pattern inname,runs-on, stepif, andwith. Its Linux combinations depend only on this gap. Its Windows and macOS combinations also need matching Runners. - prometheus/prometheus
fuzzing.ymlis a Linux-only reusable workflow. It uses an axis of arrays and consumes it withjoin(matrix.fuzz_test, ' '). - protocolbuffers/protobuf
test_cpp.ymlis a reusable workflow with an object axisconfigand anincludeentry whoseconfigvalue is an object (Bazel8). That entry must become a new combination. The workflow reads the values throughmatrix.config.runnerinruns-onandmatrix.config.continuous-onlyinif.
Prioritization evidence
On 2026-11-03, a GitHub search selected the 20 most-starred repositories for each of Go, TypeScript, Python, Rust, Java, JavaScript, and C++. Each had at least 5,000 stars, was not archived, and had been pushed since 2026-10-01. Of the 140 repositories, 133 have top-level .github/workflows/*.y{a,}ml files, 2,332 files in total, each pinned to its repository's HEAD commit at download time.
A temporary make test probe ran workflow.Parse from e9134c6 on every file, and 1,029 parse. For each failing file, the probe repeated three steps until the file parsed (1,283 files) or hit an unclassified error (20 files):
- Classify the parser's error.
- Remove or replace only the construct behind that gap.
- Parse again.
A file's blockers are all the gaps it hit. "Only blocker" counts files that have exactly one blocker and parse once it is rewritten.
| Parse-level gap | Tracking | Files | Repos | Only blocker: files (repos) | workflow_call-only |
Directly triggered: files (repos) |
|---|---|---|---|---|---|---|
Reusable workflow calls and workflow_call secrets/outputs |
#23, #119 | 379 | 56 | 123 (43) | 24 | 99 (38) |
Shells and defaults |
#117 | 294 | 66 | 91 (41) | 6 | 85 (37) |
| Other permission keys | #118 | 151 | 64 | 51 (36) | 1 | 50 (35) |
Repository-local ./ actions |
#112 | 518 | 39 | 157 (27) | 35 | 122 (26) |
id-token permission |
#31, #118 | 230 | 76 | 23 (19) | 0 | 23 (19) |
| Environments | #29 | 236 | 52 | 34 (18) | 3 | 31 (16) |
| Structured matrix values | this issue | 57 | 24 | 27 (13) | 7 | 20 (9) |
Containers, services, and docker:// actions |
#113 | 98 | 30 | 20 (13) | 1 | 19 (12) |
Step timeout-minutes |
untracked | 105 | 33 | 19 (13) | 1 | 18 (12) |
run-name |
untracked | 131 | 23 | 18 (6) | 0 | 18 (6) |
$/ self-repository action references |
untracked | 45 | 4 | 20 (4) | 8 | 12 (3) |
runs-on expressions over 128 characters before evaluation |
untracked | 50 | 9 | 9 (4) | 3 | 6 (3) |
strategy without matrix |
untracked | 9 | 6 | 5 (4) | 3 | 2 (2) |
repository_dispatch trigger |
untracked | 18 | 9 | 4 (3) | 0 | 4 (3) |
concurrency.queue |
untracked | 40 | 12 | 3 (3) | 0 | 3 (3) |
Scalar branches/types event filters |
untracked | 4 | 3 | 3 (3) | 0 | 3 (3) |
branches-ignore |
#116 | 13 | 5 | 2 (2) | 0 | 2 (2) |
Step background, wait, wait-all, cancel, and parallel |
untracked | 23 | 7 | 5 (1) | 0 | 5 (1) |
Each remaining rejected construct is the only blocker in at most two repositories:
- The
label,discussion,delete, andstatustriggers. tags-ignore(#21).runs-ongroup mappings.- One unsupported activity type.
- A top-level
descriptionkey.
The following never appear as the only blocker:
- Concurrency-group expressions over 256 characters before evaluation (25 files).
- The
branch_protection_ruletrigger (12 files). - Expressions in
strategy.fail-fast/max-parallel(7 files). - The
case()expression function (7 files). scheduletimezonekeys (2 files).
The tracked gaps dominate and already have owners. Among untracked gaps, structured matrix values and step timeout-minutes tie at 13 repositories:
- Structured matrix values block more files, 27 to 19.
- Step
timeout-minutesblocks more directly triggered repositories, 12 to 9, because 7 of the 27 structured-matrix files are reusable workflows that also need #23. - Path filter support (#202) moved both up: 10 of the 27 files and 9 of the 19 use
pathsorpaths-ignore.
Earlier scans ranked structured matrix values first among untracked gaps, at 20 files in 12 repositories, ahead of step timeout-minutes at 8 to 10 files. Those scans ran on 2026-09-30 against 0586283 (2,125 files) and on 2026-10-20 against 87db1d2 (2,274 files), using the same repository criteria with an earlier push cutoff.
These counts exclude failures that happen during job planning. A matrix built from expressions that produces objects fails only at planning, which a parse scan cannot see.
Documentation contract and current coverage
| Contract | Current default-branch evidence | Next action |
|---|---|---|
Matrix variables may hold "an array of objects". GitHub's os × node example yields four jobs exposing matrix.node.version and, only where defined, matrix.node.env. |
validateMatrixValue rejects the example during parsing. |
Accept nested mappings and sequences as axis items. |
include is a list of objects. An entry augments combinations whose original values it does not overwrite, and otherwise creates a new combination. |
validateMatrixEntries rejects structured entry values. matrixValuesEqual compares only scalar strings. |
Match structured values as described below. |
exclude removes combinations on a partial match. |
Same scalar-only comparison. | Treat nested filter values as partial matches. |
Matrix variable names are case-insensitive ("OS and os will be treated as the same variable"), documented in 03d2e24. |
The probe shows include keys compared case-sensitively, so OS/extra are added to every combination. |
Compare variable names case-insensitively in include/exclude matching. |
The matrix context contains the properties defined in the workflow. fromJSON produces the dynamic matrices. |
The runner plan's Matrix map[string]any and the controller's planning context can already carry typed values. Dynamic structured results end in JobPlanningFailed. |
Remove the scalar restriction at planning. |
| Kubernetes representation | WorkflowJob.spec.matrix.values is map[string]string with a 1,024-character limit per value. matrixStringValues/matrixDisplayName and rerunMatrixValuesMatch all use fmt.Sprint. |
Use a single canonical string form. No schema change is needed. |
GitHub's open-source workflow parser offers supplementary implementation evidence for nested comparison. MatrixBuilder.MatrixFilter walks every nested mapping and sequence in an include/exclude entry and compares each leaf path, for example matrix.versions.node-version == 12. MatrixInclude splits entries into keys that filter original variables and extra keys.
Minimal proposal and acceptance criteria
No new CRD fields or manifest changes. The change touches only the workflow model, matrix expansion, and the controller's string form for WorkflowJob matrix values. Existing scalar matrices and existing WorkflowJobs keep their exact current representation.
-
Parsing and limits. Accept mappings and sequences at any depth in axis items and in
include/excludeentry values, alongside scalars. GitHub's official array-of-objects example parses and expands to its four documented combinations. Keep the existing limits on axes, entries, and job count. Bound each structured value by the length of its canonical encoding under the existing 1,024-character per-value limit, and document the rule; the largest structured value in the 2026-11-03 corpus encodes to 457 bytes. Keep rejecting empty or non-mappinginclude/excludeentries. Explicitly reject axes whose names differ only by case, rather than creating two variables, as GitHub'sTemplateReaderdoes for duplicate mapping keys. -
Expansion semantics. The Cartesian product includes structured items.
includeandexcludecompare a structured filter value leaf by leaf, as GitHub's parser does:- A nested
excludefilter is a partial match. - An
includeentry whose nested leaves conflict with an original value creates a new combination; protobuf'sBazel8entry is the example. - Variable names compare case-insensitively:
include: [{OS: a, extra: yes}]augments only theos: acombination ofos: [a, b].
- A nested
-
Dynamic matrices. Whole-matrix, axis,
include, andexcludeexpressions may produce structured values, for example${{ fromJSON(needs.setup.outputs.matrix) }}with{"target": [{"triple": "...", "os": "..."}]}. These plan successfully. Missing outputs, invalid JSON, non-array axes, and oversized results still fail withJobPlanningFailed, as documented. -
Context propagation. Each expanded job's
matrixcontext keeps the typed structure:- In controller planning expressions: job
name,runs-on,timeout-minutes,continue-on-error,concurrency, andif. - In runner step expressions such as
matrix.job.use-crossin a stepif,matrix.target.nameinwith,join(matrix.fuzz_test, ' '), andtoJSON(matrix).
- In controller planning expressions: job
-
Resource identity and reruns. Keep
spec.matrix.valuesasmap[string]string. Encode structured values in one deterministic canonical JSON form that does not depend on source key order. Use that same form for default display names (never Gomap[...]formatting), matrix identity, andrerunMatrixValuesMatch, so a structured combination can be rerun from the Console. Update the field's API comment and regenerate the CRD. No job-plan version change is needed: runners that accept plan version 9 or later (the currentPlanVersionis 10) already decodematrixasmap[string]anyand evaluate nested properties (see Assessment validation). Add a runner test that pins this behavior. -
Conformance coverage and docs. Add tests for:
- The official example.
- Axes of arrays (Prometheus) and of objects (tauri, bat, protobuf).
- Structured
includeentries that augment versus create combinations. - Nested partial
exclude. - Case-insensitive
include/excludekeys. - Structured dynamic matrices.
- WorkflowJob values and display names.
- Rerunning a structured combination.
- Runner step
if,with, andrunreading nested properties.
Update the strategy, context, and validation-limit sections of
docs/reference.mdto describe the implemented behavior. Verify withmake update,make verify, andmake test.
Out of scope:
runs-onexpressions that evaluate to arrays. ComfyUI'sruns-on: ${{ matrix.runner_label }}relies on this; GitHub's parser accepts sequences, but the runs-on reference documents only "a single variable containing a string". Today Open Actions fails witharrays and objects cannot be interpolated as strings.runs-ongroup/labelsmappings.- String coercion of objects and arrays.
- Non-Linux runners for mixed-OS matrices.
- The other untracked gaps in the table above.
Assessment validation
-
The 2026-09-30 and 2026-10-20 scans attributed failures with a YAML feature scanner. The 2026-11-03 rescan classifies the parser's own error messages instead. It left 20 files unresolved, for example:
- A
runs-onlabel such as1ES.Pool=.... - More than 25
workflow_callinputs. needson a missing job.
Runtime gaps such as non-Linux runners, Docker actions, and caches were not assessed, so these counts are upper bounds on workflows that would run unchanged.
- A
-
In the 2026-11-03 rescan on
e9134c6, every one of the 27 files blocked only by structured matrix values failed with... values must be scalars. They come from 13 repositories: ComfyUI, llama.cpp, ollama, open-webui, codex, openinterpreter, prometheus, protobuf, RuView, rustdesk, tauri, tesseract, and whisper.cpp. Seven of them areworkflow_call-only:- codex and openinterpreter
rust-release-argument-comment-lint.yml. - prometheus
fuzzing.yml. - protobuf
test_cpp.yml,test_hpb.yml, andtest_objectivec.yml. - rustdesk
bridge.yml.
The earlier scans also counted files from uv, bat, and
storytold/photocraft. - codex and openinterpreter
-
Contract probe on
0586283:- Official array-of-objects example:
job "test" matrix axis "node" values must be scalars. includeentry with an object value:matrix include entry 1 values must be scalars.fromJSONmatrix with an axis of objects passed toEvaluateMatrix:job "build" matrix axis "target" values must be scalars.include: [{OS: a, extra: yes}]withos: [a, b]: two combinations, both carryingOS: aandextra: yes.
- Official array-of-objects example:
-
The probe on
e9134c6in Latest verdict reproduces each of these failures and also covers an axis of arrays and an object axis. The runner probe below covers nested runner expressions. -
The 2026-11-03 corpus contains 309 structured matrix values. Each encodes to at most 457 bytes of compact JSON; the largest is in protobuf
test_upb.yml. -
None of the three corpora, including the 2026-11-03 corpus, has a file with
include/excludekeys whose case differs from the axis name, or with axis names that differ only by case. The case-insensitivity contract is documented but has not been seen blocking a real workload. -
Runner probe on
87db1d2, repeated on5c8d765with a version-10 plan. A plan whosematrixis{job: {target, use-cross: true}, fuzz_test: [FuzzA, FuzzB]}was decoded as JSON intoPlan.Matrixand evaluated in the runner's step and step-condition expression contexts ("matrix": plan.Matrix):${{ matrix.job.target }},${{ join(matrix.fuzz_test, ' ') }},${{ toJSON(matrix) }}, and the conditionmatrix.job.use-crossreturn the expected values.- Only bare
${{ matrix.job }}fails, witharrays and objects cannot be interpolated as strings, and string coercion is out of scope. - The controller already passes the typed combination to planning expressions, to
plan.Matrix, and to reruns. The remaining work is ininternal/workflowand in thefmt.Sprint-based string form described in the table above.
-
The temporary probes ran through
make testand were deleted. Repository files are unchanged.
Activity
[-][open-actions-fake-strategist] Support step timeout-minutes across scripts and actions[/-][+][open-actions-fake-strategist] Support structured matrix values in workflow strategies[/+]