Skip to content

test(ci): make the workflow spec read quoted settings and fail on a step it cannot read - #591

Merged
ThomasK33 merged 4 commits into
mainfrom
ci/workflow-spec-reader-gaps
Nov 13, 2026
Merged

ThomasK33 merged 4 commits into
mainfrom
ci/workflow-spec-reader-gaps

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

#579 added a spec that reads .github/workflows/test.yml as text and checks the cache settings. The independent reviews of its heads found gaps in how the spec reads the file. Each gap lets a risky edit pass or fails a harmless one.

  1. A step written with a quoted key ("uses": actions/cache@...) was not seen, so a second step that caches .luarocks passed.
  2. A second jdx/mise-action step with the default cache settings would save the tool cache on a pull request. Nothing failed.
  3. The rock restore key was only checked to contain hashFiles('mise.toml'). Text that changes on each run (-${{ github.sha }}) kept the spec green and the cache would never hit.
  4. The spec failed edits that YAML reads the same: a quoted cache_key, a quoted restore path, a quoted uses:.
  5. A step in flow style (- { uses: actions/cache@..., with: { path: .luarocks, key: ... } }) was not seen.

What changed

Only tests/unit/workflow_cache_spec.lua (+98, -19). The workflows are unchanged. The spec still has no YAML parser.

  1. field() accepts a quote around the key ("uses": or 'uses':).
  2. A new test: every jdx/mise-action step turns its cache off (cache: false or cache_save: false) or saves only on a push.
  3. The rock restore key must be luarocks-${{ runner.os }}-${{ hashFiles('mise.toml') }} as a whole. A version after the prefix (luarocks-v2-) is allowed, as v1 is in the mise key.
  4. field() strips one pair of YAML quotes from a value, and reads '' in a single-quoted value as '.
  5. A new test fails closed. It counts the lines of the file that are not comment lines and name actions/cache or jdx/mise-action (a trailing comment stays in the line), and the steps that have the name as their uses:. The two counts must be equal. So the spec fails for a flow-style step or list of steps, even with a # inside a quoted value, and for the name on any other line (a step name:, a run: line, a trailing comment). The line count ignores the letter case and the uses: match does not, so an upper-case spelling such as Actions/cache@... fails too. The one exception is a name written with a YAML escape ("actions\x2fcache@..."), which the spec does not decode (see Not covered).
  6. The helper that tells whether a step uses an action requires @ or / after the action name. A similarly prefixed repository such as jdx/mise-action-fork@... no longer counts as jdx/mise-action, so it cannot satisfy the line count or the check that a jdx/mise-action step exists.

Evidence

Before is the spec of #579 (10 tests, merged as b450eb91), after is this branch (12 tests). Each mutant is the workflow of #579 with one edit that applies exactly once. m, L, X and Y are the mutants of #579, Z1 to Z12 are the probes of its last review, N1 to N20 are mine (there is no N6), P1 to P26 are the probes of the review of this PR (the ones in the tables below), and Q1 to Q4 come from a Codex finding on the second head.

Item Mutant Before After
1 Z9 and N5: a second actions/cache@ step for .luarocks with a quoted key "uses": or 'uses': green red (6 rock-cache tests)
2 Y3: a second jdx/mise-action step with the default cache settings. N10: it sets cache_save: true. N11: it sets cache_save to != 'pull_request' green red (1 test)
2 Controls N7 (cache: false), N8 (cache_save: false), N9 (the push-only expression in quotes) green green
3 Y4: -${{ github.sha }} on the rock key. N14: an extra segment in it green red (1 test)
3 Controls N12 (the key in quotes), N13 (luarocks-v2-) green green
4 Z1 (quoted cache_key), Y5 (quoted restore path), Z7 (quoted uses: on the restore step), N15 (quoted cache_save), N16 (quoted save if:), N17 (single-quoted mise key with ''), N19 (quoted save path), N20 (quoted step name) red green
4 Control N18 (MISE_HTTP_RETRIES: '6') green green
5 N1: a flow-style second cache step for .luarocks. N2: the same for another path, which fails closed on purpose. N3: a flow-style second jdx/mise-action step. Z6: Actions/cache@ green red (1 test)
5 P23 and P25: a flow-style second cache step for .luarocks with a # inside a quoted value. P24: the same for a second jdx/mise-action step. P26: the same step as P23 without the # green red (1 test)
6 Q1: the setup step uses jdx/mise-action-fork@.... Q2: a step actions/cache-extra@... for another path. Q3: the same step for .luarocks green red (1 test, and 2 for Q1)
6 Control Q4: the setup step uses jdx/mise-action/sub@... green green
5 N4: a flow-style with: on the rock restore. It fails the path and key tests, as it did before, and not the new test red red
5 Harmless edits that the spec fails on purpose: P3 (a trailing comment that names actions/cache on a run: line), P4 (the name in a step name:), P5 (the name in a run: line), P13 (cache: False on a second jdx/mise-action step) green red (1 test)

Counts over all 102 mutants (the 86 of the first head, P1, P1b, P2, P3, P4, P5, P13, P14, P23 to P26, and Q1 to Q4). Before, 59 are red and 43 are green. After, 73 are red and 29 are green. 51 stay red, 22 turn red, 8 turn green and 21 stay green. Of the 29 green mutants, 8 are harmless edits that used to fail, 13 are controls (m12, X8, X19, X23, Z2, Z3, N7, N8, N9, N12, N13, N18, Q4), and 8 are gaps (X3, X10, Z10, Z11, and the four YAML escapes P1, P1b, P2, P14, see below). Five of the 51 red mutants (m2, m3, X1, X11, X14) and the tree before #560 now also fail the new jdx/mise-action test next to the test that failed before. No other test result changed.

Claim Evidence
The spec passes on main (b450eb91) with this change 12 successes, 0 failures
It still fails on the trees before #579 (b43d1c23, c7e2eb4e) 6 successes, 6 failures: the 6 rock-cache tests
The workflows are valid The workflows are identical to main. actionlint 1.7.12 on all 5: exit 0. zizmor 1.25.2 --offline on all 5: no findings (25 suppressed). On the mutants, actionlint exits non-zero only on L8, L9 and X3, and zizmor only on L10 and L11, as for #579. mise run format-check: exit 0 (441 files, 0 changed)
Gate mise run clean && mise run all exits 0 in 540 s: 1904 successes, 0 failures, 0 errors, 35 pending (seed 1794532755). It ran on the merge of main (692b460c) into this branch. luacheck: 0 warnings, 0 errors in 198 files. The stub claude and GNU timeout first on PATH

Not covered

  • The spec does not see a rock path without the text .luarocks (a glob such as .luaroc*, Z10, or a parent folder, Z11). The save step is not checked for its place in the job, so moving it after the tests keeps the spec green (X10). A second cache_save: true key is caught by actionlint and not by the spec (X3).
  • The new tests also fail harmless edits, so a maintainer edits the spec with the workflow. Examples are a flow-style step for another path (N2), the name of an action in a step name: (P4), in a run: line (P5) or in a trailing comment (P3), and cache: False on a second jdx/mise-action step (P13). The action reads False as false and the spec compares the text false. A flow-style with: on a step the spec reads fails the path and key tests (N4), as it did before.
  • A name written with a YAML escape ("actions\x2fcache@...", "actions\u002fcache@...", "jdx\x2fmise-action@...", or a path "\x2eluarocks") is not decoded, so the spec does not see it (P1, P1b, P2, P14). The spec guards against accidental edits, and an escape spelling is not an accident, so it has no reader for it. A double-quoted uses: split by an escaped line break ("actions/\ and then cache@...") falls in the same class.
  • GitHub ignores the letter case in the owner and the repository of uses:, as the runner log in Download action repository called repeatedly for actions that have to be the same actions/runner#3731 shows. So Actions/cache@... is a real cache step, and the line-count test rejects it. The spec does not read it as a rock-cache step.
  • The spec reads the rock path as text, so a path that comes from a YAML alias (with: *rocks on a second actions/cache step, where the restore step's with: has the anchor &rocks) or from an expression such as ${{ format(...) }} is not seen. GitHub supports anchors and aliases. An alias is a deliberate structure, like an escape, not an accidental edit, so the spec has no reader for it.
  • field() also accepts a quote on one side of a key (cache_save": ..., cache': false). YAML reads that as another key (cache_save"), so the action ignores it and uses its default (true). The spec reads it as cache_save, so a Set up mise step written this way passes this spec (the spec on main fails it), and so does a second jdx/mise-action step with cache': false. actionlint 1.7.12 exits 0 on both. A follow-up PR requires a matching pair of quotes.
  • The spec fails on a tree made with git archive, because .gitattributes export-ignores .github. The failure message says so.

Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh

…tep it cannot read

The spec read a key only when it was written bare and a value only when it was
written without quotes. A step written with a quoted key (`"uses": actions/cache@...`)
was not seen, and a quoted value (`cache_key: "..."`) failed the spec although
YAML reads it the same. field() now accepts a quote around the key and strips
one pair of quotes from the value.

Three more checks:
- Every jdx/mise-action step must turn its cache off or save only on a push. A
  second step with the default settings would save the tool cache on a pull
  request.
- The rock restore key must be luarocks-${{ runner.os }}-${{ hashFiles('mise.toml') }}
  as a whole, with an optional version after the prefix, as the mise key is.
- The spec fails when actions/cache or jdx/mise-action is named on a line that is
  not the uses: of a step it reads. That covers a flow-style step
  (`- { uses: actions/cache@..., with: { ... } }`) and an upper-case spelling.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking:
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T01:32:35.638530Z 313cb6f Manual request
🔒 Security Review ✅ Completed 2026-11-13T01:34:17.557697Z 313cb6f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: b74cf4fad9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: b74cf4fad9

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…rkflow spec checks

The line count cut each line at its first " #" as a trailing comment. A " #" can sit
inside a quoted value of a flow-style step, such as { name: "Cache #2", uses:
actions/cache@... }, so the cut hid the name and the step passed. The count now
keeps every line that is not a comment line as it is. A trailing comment that
names an action on another line now fails the count too.

The spec comments now say what the spec does: it fails when actions/cache or
jdx/mise-action is named on a line that is not the uses: of a block-style step,
and it does not decode YAML escapes.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking:
_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking:
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: d9ae8f78e1

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9ae8f78e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/unit/workflow_cache_spec.lua Resolved
uses_action matched any value that began with the action name, so a similarly
prefixed repository (jdx/mise-action-fork@...) counted as jdx/mise-action. The
line-count guard and the "at least one mise-action step" check then passed with no
real jdx/mise-action step left. The value must now continue with @ or /.

_Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking:
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 313cb6fb4e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 313cb6fb4e

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit b44dab4 Nov 13, 2026
9 checks passed
@ThomasK33
ThomasK33 deleted the ci/workflow-spec-reader-gaps branch November 13, 2026 02:34
hareki pushed a commit to hareki/claudecode.nvim that referenced this pull request Nov 14, 2026
…ey (coder#596)

## Why

coder#591 made `field()` in the workflow spec accept a quote around a key.
Its pattern `^ *["']?([%w_%-]+)["']?:` also accepts a quote on one side
of the key only. YAML reads `cache_save": ...` as the key `cache_save"`,
so mise-action ignores it and keeps its default `true`, which saves the
tool cache on a pull request. The spec read the same line as
`cache_save` and passed.

The same gap hides a second `jdx/mise-action` step with `cache': false`,
a `cache_key": ...` and a `MISE_HTTP_RETRIES": "6"`. `actionlint` and
`zizmor` pass all four.

A second gap is older and main has it too. A second `jdx/mise-action`
step with `with:` and then `env :` or `"env" :` holding `cache: false`
passes the spec. No pattern read the `env` line, so the spec still
thought it was inside `with:` and counted that `cache` as an input. YAML
puts it under `env`, so the action keeps its defaults and saves its
cache on a pull request. `actionlint` and `zizmor` pass it.

## What changed

Only `tests/unit/workflow_cache_spec.lua` (+99, -16). The workflows are
unchanged. The spec still has no YAML parser.

1. `field()` accepts a key written bare, as `"key":` or as `'key':`. A
quote on one side only is not a key.
2. Any non-blank line at the step-key indent now sets the current block,
even when no pattern reads its key. A line such as `env :` ends the
`with:` block.
3. Four new tests check how the spec reads keys and blocks, so that a
full revert of these changes makes `mise run test` fail. A partial
change to the quoted-key patterns can still pass (a follow-up PR adds a
test for it). `parse_workflow` now takes an iterator over lines, and the
tests feed it small workflow texts as inline strings. They assert that
an `env :` or `"env" :` line after `with:` ends the block, that a line
of spaces does not end it, that `cache': false` and `cache_save":` are
not read as keys, and that `"key":`, `'key':` and a bare key are read.
4. The comments say what the spec does. The header says that full-line
comments are skipped, so a comment can name an action (the one above the
rock cache steps does). The comment of the line-count test says that `@`
or `/` must follow the action name in `uses:`. The comment above the
block line says that a non-blank line opens a new block and a line of
spaces only does not.

The reader reads no new YAML form. Aliases, YAML escapes and expression
spellings stay under "Not covered" in coder#591, with their reason.

## Evidence

Before is the spec of coder#591 (12 tests, merged as `b44dab46`), after is
this branch (16 tests). Each mutant is the workflow of coder#591 with one
edit that applies exactly once.

| Mutant | Edit | Before | After |
| --- | --- | --- | --- |
| `SQ1` | the setup step has `cache_save": ${{ github.event_name ==
'push' }}` | green | red (2 tests) |
| `SQ2` | a second `jdx/mise-action` step has `cache': false` | green |
red (1 test) |
| `SQ3` | the setup step has `cache_key": ...` | green | red (1 test) |
| `SQ4` | the setup step has `MISE_HTTP_RETRIES": "6"` | green | red (1
test) |
| `SQ7` | `"cache_save: ...` (a quote on the left only) | green | red (2
tests) |
| `SQ5`, `SQ6` | controls: `"cache_save":` and `'cache_save':` | green |
green |
| `K30` | a second `jdx/mise-action` step with `with:`, then `env :` and
`cache: false` under it | green | red (1 test) |
| `K31` | the same with `"env" :` | green | red (1 test) |
| `K32` | the same with `env":` (a quote on one side only) | red | red
(1 test) |
| `K33` | control: the same step with `"env":` right after `with:` | red
| red |

The failing tests are the ones that read the key: the `cache_save`,
`cache_key` and `MISE_HTTP_RETRIES` tests of the setup step, and the
test that every `jdx/mise-action` step keeps its cache off a pull
request.

Counts over all 112 mutants (the 102 of coder#591, `SQ1` to `SQ7` and `K30`
to `K32`). Before, 74 are red and 38 are green. After, 81 are red and 31
are green. 74 stay red, 7 turn red and 31 stay green. `K32` is red with
coder#591's spec, which read `env":` as the key `env`. The first commit of
this PR made it pass, and `36feb3b7` makes it red again. The 102 mutants
of coder#591 give the same result as before, so no test result changed for
them. The 31 green mutants are the 29 controls and known gaps of coder#591
and the two controls `SQ5` and `SQ6`. `SQ7` is not valid YAML, and
`actionlint` and `zizmor` reject it. They pass `SQ1` to `SQ4`, `K30` and
`K31`. actionlint rejects `K32`, and zizmor passes it. I also ran all
169 workflows of the verifier's table (the 112 mutants, the other probes
and the base trees) with the spec of `b558d22d` and of `36feb3b7`. 111
stay red, 3 turn red (`K30`, `K31`, `K32`) and 55 stay green. No
workflow turns green, and no failing test changed for the red ones. I
ran the 169 workflows again with the four new tests. 114 are red and 55
are green, and the result and the failing test names are the same for
all 169.

The four new tests are checked against changes of the reader itself.
Each change is one edit of the spec, and the spec runs on the workflow
of main. Before is the spec without the new tests, after is the spec
with them.

| Change of the reader | Before | After (failing test) |
| --- | --- | --- |
| `S1`: the block line back to `if name and indent == 8` (the code of
coder#591) | green | red (the `env :` test) |
| `S2`: the block line without its `%S` check | green | red (the line of
spaces test) |
| `S3`: the key pattern of coder#591, which takes a quote on one side | green
| red (the one-sided quote test) |
| `S4`, `S5`: no pattern for `"key":`, or none for `'key':` | green |
red (the quoted keys test) |
| `S7`: the loop over the key patterns without its `break` | green | red
(the quoted keys test) |
| `S8`: the bare pattern takes a trailing quote | green | red (the
one-sided quote test) |
| `S9`: the `"key":` pattern takes the left quote as optional | green |
red (the one-sided quote test) |
| `S10`: the block of an entry is not compared | green | red (the `env
:` test) |
| `S6`: no bare pattern | red | red |

Each of the 10 changes fails exactly one new test, except `S6`, which
fails 15. Without the new tests, only `S6` fails.

| Claim | Evidence |
| --- | --- |
| The spec passes on main with this change | 16 successes, 0 failures |
| The workflows are valid | The workflows are identical to main.
`actionlint` 1.7.12 on all 5: exit 0. `zizmor` 1.25.2 `--offline` on all
5: no findings (25 suppressed). `mise run format-check`: exit 0 (444
files, 0 changed) |
| Gate | `mise run clean && mise run all` exits 0 in 495 s: 1908
successes, 0 failures, 0 errors, 36 pending (seed 1794545933). It ran on
the merge of main (`bcf1f084`) into this branch. `luacheck`: 0 warnings,
0 errors in 200 files. The stub `claude` and GNU `timeout` first on
`PATH` |

---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant