Repository navigation
ci(test): key mise-action's tool cache on mise.lock and raise mise's HTTP retries - #560
Conversation
… one mise-action keys its cache on a hash of every mise config file and has no restore-keys. An edit to a task in mise.toml missed the cache and every tool downloaded again. On #545's merge-group runs www.lua.org reset the connection, mise gave up after 3 retries and the unit-tests job failed before it ran a test. Cache the mise data dir with actions/cache, keyed on mise.lock, with the mise version as a restore-keys prefix, and turn mise-action's cache off. Raise mise's HTTP retries from 3 to 6. _Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking:
|
@codex review |
|
@codex security review |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (3)
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9e1656d3b
ℹ️ 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".
There was a problem hiding this comment.
🛡️ Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: c9e1656d3b
ℹ️ 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.
…e-keys step Codex found four problems in the separate actions/cache step: - Its path "~/.local/share/mise" gives another cache version than the absolute path that mise-action uses, so it could not restore the caches that mise-action saved. A cache version hashes the path as written. - Its restore-keys prefix restored installed tools across lock changes. mise does not check an installed tool against mise.lock, so a tool whose checksum changed at the same version stayed as it was. - That fallback also kept every old tool version in the next cache. - Its post step saved the cache after the PR's scripts had run. Use the cache_key input of mise-action instead. The key hashes only mise.lock, so an edit to a task in mise.toml keeps it. mise-action saves right after the install, from an exact key, as before. Keep the 6 HTTP retries. _Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
|
@codex review |
|
@codex security review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…to bump the cache key mise's schedule is about 200 ms, 1 s and 4 s for 3 retries, each cut by a random factor of 0.5 to 1, so the total is 2.6 to 5.2 s. A failed run waited 2.7 s. Also say that a change which alters an install without changing mise.lock keeps the key. _Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
|
@codex review |
|
@codex security review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
There was a problem hiding this comment.
🛡️ Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: d08f39530a
ℹ️ 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.
mise runs the postinstall hooks of the PR's own mise.toml during mise install, and mise-action saves after that. The key ignores mise.toml, so a PR could keep a tampered tool in its own cache after it removed the hook, and its later runs would restore it. Set cache_save to true only for the push event, which the workflow runs on main only. Pull requests, the merge queue and manual runs restore the cache and never write it. _Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh_
|
@codex review |
|
@codex security review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
There was a problem hiding this comment.
🛡️ Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 61d7ea13b2
ℹ️ 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.
|
Review-cap record (maintainer answer in the Xum chat, Option A, 2026-11-12T17:54Z). This PR used 8 Codex review requests (each code review request and each security review request counts as one), past the cap of 6. Requests 7 and 8 were posted without a waiver: the owner counted each code and security pair as one round. The maintainer approved recording the overrun and merging this head without further review requests:
Generated with |
|
Merge record (correction).
Generated with |
… on push (coder#579) ## Why coder#560 keyed mise-action's tool cache on `mise.lock`, saved it only on a push, and raised mise's download retries. Its independent review found two gaps, which this PR closes. 1. **No test or lint checked those three settings.** `actionlint` and `zizmor` stayed green when a later edit removed `cache_key` or `MISE_HTTP_RETRIES`, or loosened `cache_save`. A removed `cache_key` or `MISE_HTTP_RETRIES` can show up later as a slow or failed `Set up mise` step. A loosened `cache_save` shows nowhere: no step gets slow or fails. 2. **The Lua rocks cache still saved on pull requests and the merge queue.** After a miss, `actions/cache` saves in its post step on `pull_request` and `merge_group` runs too, after the PR's own scripts ran. GitHub scopes cache entries by ref, so an entry saved by a PR run cannot reach main. No run has shown a bad entry. This change removes the write path by construction, as coder#560 did for the tool cache. ## What changed 1. `tests/unit/workflow_cache_spec.lua` (new, 10 tests) reads `.github/workflows/test.yml` as text. It uses plain Lua string matching, no YAML library and no network. Comment lines do not count, so a setting that sits only in a comment fails. A step that is renamed fails closed (`no step is named ...`). The spec finds each setting in its place: a key of the step at eight spaces of indent, and an entry of the `with:` or `env:` block at ten. A setting that moves to another block fails. - **`Set up mise` step:** `cache_key` is under `with:` and matches `mise-v<n>-{{platform}}-{{version}}-${{ hashFiles('mise.lock') }}` as a whole (`<n>` can change). So it hashes only `mise.lock` (one `hashFiles(`, no `{{file_hash}}`, no `{{default}}`), and no text that changes between runs can be added to it. The step has no `restore-keys`. `cache_save` is under `with:` and is exactly `${{ github.event_name == 'push' }}`. `MISE_HTTP_RETRIES` is under `env:` and is an integer of at least 6. - **Rock cache:** every step that uses `actions/cache` (plain, `restore` or `save`, with or without a quote around the name) and has a line that contains the text `.luarocks` (the path as `.luarocks`, `./.luarocks`, `~/.luarocks`, an absolute path, or one line of a multi-line `path`) must be, in order, one `actions/cache/restore` and one `actions/cache/save`. So a second step for the tree written with a plain `uses:` key, or the old single `actions/cache@` step, fails. Both steps pin the same 40-character SHA and both use the path `.luarocks`. The restore key hashes `mise.toml`, and the restore step has no `restore-keys`. The save key is the restore step's `cache-primary-key` output. The save step's `if:` is exactly `github.event_name == 'push' && steps.luarocks-cache.outputs.cache-hit != 'true'`. 2. `.github/workflows/test.yml`: the `Cache Lua test rocks` step (`actions/cache`) becomes `Restore Lua test rocks` (`actions/cache/restore`, `id: luarocks-cache`, same key) before `mise run setup`, and `Save Lua test rocks` (`actions/cache/save`) after it. Both use the SHA that `actions/cache` had (`27d5ce7f...`, v5.0.5). The save step runs only on a push to main and only when the restore did not find the exact key. The comment above the restore step says what the old step did: it saved after a miss, on every event. README.md and DEVELOPMENT.md do not describe the CI cache, so they are unchanged. ## Evidence All numbers are from runs on this head (`cc903344`) unless a row says otherwise. | Claim | Evidence | | --- | --- | | The guard passes on the head | 10 successes, 0 failures | | It passes on the first head of this PR (`37efd357`), whose workflow is the same except for one comment | 10 successes, 0 failures | | It fails on main before coder#560 (`523ee200`) | 1 success, 9 failures: 3 of the 4 `Set up mise` tests (key, `cache_save`, retries) and all 6 rock-cache tests | | It fails on main now (`c7e2eb4e`, and `b43d1c23`, `7dbad04b` and `4005ab66` give the same) | 4 successes, 6 failures: the 6 rock-cache tests. The 4 `Set up mise` tests pass, as they should | | It fails on each of the 6 mutants of coder#560's review | 1 failing test per mutant, listed below. `actionlint` and `zizmor` exit 0 on all 6 | | It fails on a mutant of each other rule | 55 mutants in all (`m1` to `m12`, `L1` to `L12`, `X1` to `X23`, `Y1` to `Y8`): 47 red, 4 controls and 4 known gaps green. `actionlint` and `zizmor` exit 0 on 43 of the 47 red mutants | | A harmless edit stays green | Controls `m12` (retries `"7"`), `X8` (retries `6` without quotes), `X19` (an `actions/cache` step for another path), `X23` (a second `cache_save` under `env:` next to the right one): 10 successes each | | The workflows are valid | `actionlint` 1.7.12 on all 5 workflows: exit 0 on the head and on main (`b43d1c23` and `c7e2eb4e`). `zizmor` 1.25.2 `--offline` on all 5: no findings (25 suppressed), the same as main. `mise run format-check`: exit 0 (436 files, 0 changed) | | Gate | `mise run clean && mise run all` exits 0 in 1318 s: 1847 successes, 0 failures, 0 errors, 33 pending (seed 1794518856). `luacheck`: 0 warnings, 0 errors in 194 files. The stub `claude` and GNU `timeout` first on `PATH` | <details> <summary>Mutants and the test each one turns red</summary> Each mutant is the head's `test.yml` with one edit that applies exactly once. `m1` to `m6` are the six mutants of coder#560's review. `m7` to `m12` and `L1` to `L12` are mine from the first round. `X1` to `X15` came from the independent review of the first head, `X16` to `X23` are mine, and `Y1` to `Y8` came from the independent review of the second head. | Mutant | Edit | Failing test | | --- | --- | --- | | m1 | delete the `cache_key:` line | keys the tool cache on mise.lock only | | m2 | delete the `cache_save:` line | saves the cache only on a push | | m3 | `cache_save: ${{ github.event_name != 'pull_request' }}` | saves the cache only on a push | | m4 | delete the `env:` block with `MISE_HTTP_RETRIES` | retries a failed download at least 6 times | | m5 | `hashFiles('mise.lock', 'mise.toml')` | keys the tool cache on mise.lock only | | m6 | the key without the hash | keys the tool cache on mise.lock only | | m7 | add `restore-keys` to the step | restores from the exact key only (mise step) | | m8 | `MISE_HTTP_RETRIES: "5"` | retries a failed download at least 6 times | | m9 | `MISE_HTTP_RETRIES: "six"` | retries a failed download at least 6 times | | m10 | add `-{{file_hash}}` to the key | keys the tool cache on mise.lock only | | m11 | `{{default}}-${{ hashFiles('mise.lock') }}` | keys the tool cache on mise.lock only | | L1 | put the single `actions/cache@` step back | all 6 rock-cache tests (the steps are not restore then save) | | L2 | delete the save step's `if:` | saves only on a push, and only after a miss | | L3 | `if: github.event_name != 'pull_request' && ...` | saves only on a push, and only after a miss | | L4 | `if: github.event_name == 'push'` (no miss check) | saves only on a push, and only after a miss | | L5 | the save step's SHA differs in its last digit | restores and saves with the same pinned action | | L6 | the save key hashes `mise.lock` | saves under the key that the restore step tried | | L7 | the restore key hashes `mise.lock` | restores the key that the rock list in mise.toml decides | | L8 | the save step before the restore step | all 6 rock-cache tests | | L9 | delete the restore step's `id` | saves under the key that the restore step tried, and saves only on a push, and only after a miss | | L10 | `actions/cache/save@v5` | restores and saves with the same pinned action | | L11 | `actions/cache/restore@v5` | restores and saves with the same pinned action | | L12 | delete the save step | all 6 rock-cache tests | | X1 | `cache_save` only in a comment | saves the cache only on a push | | X2 | the step renamed `Setup mise` | all 4 `Set up mise` tests (no step is named ...) | | X4 | `if: ... cache-hit == 'false'` | saves only on a push, and only after a miss | | X5 | `if: always() && ...` | saves only on a push, and only after a miss | | X6 | a second `actions/cache@` step for `./.luarocks` | all 6 rock-cache tests | | X7 | `restore-keys` on the rock restore | restores from the exact key only (rock cache) | | X9 | the key without `{{version}}` | keys the tool cache on mise.lock only | | X11 | `cache_save: "true"` | saves the cache only on a push | | X12 | `hashFiles('mise.lock')` without `${{ }}` | keys the tool cache on mise.lock only | | X13 | `MISE_HTTP_RETRIES` moved under `with:` | retries a failed download at least 6 times | | X14 | `cache_save` moved under `env:` | saves the cache only on a push | | X15 | `cache_key` moved under `env:` | keys the tool cache on mise.lock only | | X16 | a second `actions/cache@` step for the absolute path of `.luarocks` | all 6 rock-cache tests | | X17 | a second `actions/cache@` step with `.luarocks` in a multi-line `path` | all 6 rock-cache tests | | X18 | a second `actions/cache/save@` step for `.luarocks` | all 6 rock-cache tests | | X20 | the save path `./.luarocks` | restores and saves the same path | | X21 | the restore path `./.luarocks` | restores and saves the same path | | X22 | `restore-keys: |` (multi-line) on the rock restore | restores from the exact key only (rock cache) | | Y1 | a second `actions/cache@` step for `.luarocks` with a double-quoted `uses:` | all 6 rock-cache tests | | Y2 | `-${{ github.run_id }}` added to the mise key | keys the tool cache on mise.lock only | | Y5 | the restore path written as `".luarocks"` | restores and saves the same path | | Y6 | a second `actions/cache@` step for `~/.luarocks` | all 6 rock-cache tests | | Y7 | `MISE_HTTP_RETRIES` set in the job's `env:` instead of the step's | retries a failed download at least 6 times | | Y8 | a second `actions/cache@` step for `.luarocks` with a single-quoted `uses:` | all 6 rock-cache tests | Green on purpose: `m12`, `X8`, `X19` and `X23` are harmless edits. Green because the spec does not check it: `X3` (a second `cache_save: true` key: `actionlint` rejects a duplicate key, and `mise run format-check` runs `actionlint`), `X10` (the save step moved after the tests), `Y3` (a second `jdx/mise-action` step with the default cache settings) and `Y4` (`-${{ github.sha }}` added to the rock key). `Y5`, `Y6` and `Y7` are harmless edits that the spec fails on purpose. `actionlint` exits non-zero on L8 and L9 (an undefined step id) and X3, and `zizmor` exits 14 on L10 and L11 (an unpinned action). Both exit 0 on every other mutant. </details> ## Not covered - The spec reads text. It does not show that GitHub restores or saves as the settings say. It pins the `if:` as an exact string, so a reformatted but equal expression fails it on purpose: edit the spec with the workflow. It does not check that the save step stays right after `mise run setup` (`X10`). - The spec fails some harmless edits on purpose, so a maintainer edits it with the workflow: the restore path in quotes (`Y5`), a cache step for `~/.luarocks` (`Y6`), `MISE_HTTP_RETRIES` set in the job's `env:` (`Y7`), and the `cache_key` value in YAML quotes. - The spec finds a step only in block style, and finds `uses:` only as a plain key. Two spellings of a second `.luarocks` cache step are not seen: a quoted key (`"uses": actions/cache@…`), which actionlint reads as `uses`, and a step in YAML flow style (`- { uses: actions/cache@…, with: { path: .luarocks, key: … } }`). A follow-up PR in this run lets the spec accept an optional quote around the key. - A second `jdx/mise-action` step with the default cache settings (`Y3`) would save the tool cache on a pull request, and the spec does not look for it. - The rock restore key is checked only for `hashFiles('mise.toml')`, so text that changes on each run, such as `-${{ github.sha }}` (`Y4`), keeps the spec green and the cache never hits. - A rock path without the text `.luarocks` (a glob such as `.luaroc*`, or a parent folder) is not seen as a rock cache step. - A PR that edits `mise.toml` (a task edit included) changes the rock cache key, misses, and now rebuilds the rocks on every one of its runs, because a PR no longer saves. Before, its first run saved and later runs hit. `mise run setup` took 64 s on one host and 195 s on another. A PR that leaves `mise.toml` alone restores main's entry as before. - Eviction has the same effect. GitHub removes a cache entry that nobody accessed for 7 days, and removes the least recently accessed entries when the repository passes its cache limit (10 GB by default). When main's entry for a key is gone, every PR run misses and rebuilds until the next push to main saves the key again. Before this PR, the first PR run after an eviction saved the entry for its own later runs. The mise tool cache from coder#560 behaves the same way: each PR run downloads the tools again until a push to main saves the key. - The save step runs right after `mise run setup`, not at the end of the job, so a later red step does not stop the save. The rock tree is complete at that point, and a failed `setup` fails the job before the save step. - `covered-lock` still caches the TLC jar with plain `actions/cache`. `check.sh` checks the sha256 of a restored jar on every run and exits 2 before TLC runs on a mismatch. I left it alone. - The spec fails on a tree made with `git archive`: `.gitattributes` export-ignores `.github`. The failure message says so. A checkout or a `git worktree` has the file. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
…tep it cannot read (coder#591) ## Why coder#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 coder#579 (10 tests, merged as `b450eb91`), after is this branch (12 tests). Each mutant is the workflow of coder#579 with one edit that applies exactly once. `m`, `L`, `X` and `Y` are the mutants of coder#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 coder#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 coder#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 coder#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 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`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
Why
On #545's merge-group runs (43206505037 and 43206955049) the
unit-testsjob failed inSet up mise, before it ran a test.www.lua.orgreset the connection forlua-5.5.0.tar.gz, and mise gave up after its 3 retries (4 attempts, 10.6 s). Other PRs passed because they hit the tool cache. #545 edited a task inmise.toml, so its runs missed the cache (mise cache not found for mise-v1-linux-x64-2026.11.5-bd6141...) and every tool downloaded again.A
mise.tomledit that does not change the tool set must not download tools again.Cause
From
src/index.tsofjdx/mise-actionat the SHA thattest.ymlpins (v4.0.1): the default key here ismise-v1-<platform>-<mise version>-<file_hash>. The template addsMISE_ENVand a hash of the tool names ininstall_argsonly when they are set.--lockednames no tool, so this key has neither.file_hashhashes everymise.toml,mise.lock,.mise.tomland.tool-versionsin the repo, so anymise.tomledit, a task edit included, changes the key. Of the 10 commits on main that touchedmise.toml, 5 leftmise.lockalone. #545 was one of them: itsmise.lockblob is the same before and after the merge, and itsmise.tomlis not.What changed
Only
.github/workflows/test.yml, in theSet up misestep of theunit-testsjob:cache_key: mise-v1-{{platform}}-{{version}}-${{ hashFiles('mise.lock') }}. The key keeps the shape of the default key (mise-v1-linux-x64-2026.11.5-<hash>) and hashes onlymise.lock, which pins the tool set. mise-action restores and saves from this exact key, as before.cache_save: ${{ github.event_name == 'push' }}. Only a push to main saves the cache. A pull request, the merge queue and a manual run restore it and never write it. mise runs thepostinstallhooks of a PR's ownmise.tomlduringmise install, mise-action saves after that, and the key ignoresmise.toml. Without this line, a PR could keep a tampered tool in its own cache after it removed the hook.MISE_HTTP_RETRIES: "6". mise retries a failed HTTP request 3 times by default, with a backoff of about 0.2, 1 and 4 s (2.6 to 5.2 s in total). 6 retries add three more backoffs of about 15 s each, so the total is 25 to 50 s.release-notes.yamlandrelease-please.yamlalready setcache: false(they publish), andintegration-testsandcovered-lockdo not install mise. They are unchanged. The pinned mise version and itssha256are unchanged.What I did not do, and why
The first head of this PR used a separate
actions/cachestep with arestore-keysprefix, so that a lock change would restore the previous cache and download only the changed tools. Codex raised four valid findings on it, and I dropped the design:~/.local/share/misegives another cache version than the absolute path that mise-action uses (e61abf9f359dfor~/.local/share/miseand7a8f38effbb6for the absolute path: each is the first 12 characters of thesha256of<path>|zstd-without-long|1.0), so it could not restore the caches that mise-action had saved. The first CI run of that head printedCache not found for input keys: mise-v1-linux-x64-2026.11.5-cb7d66..., mise-v1-linux-x64-2026.11.5-.restore-keysfallback restores installed tools across lock changes. mise does not check an installed tool againstmise.lock(its docs: "Restored caches and installed tools"), so a tool whose checksum changed at the same version would stay as it was.mise's own advice is to cache the download store (
downloads-cas) instead of the installs. That would not have prevented this failure. With the pinned mise 2026.11.5, a--lockedinstall oflua@5.5.0downloads the tarball through the vfox plugin, unchecked, and neither my run nor an independent rerun made adownloads-casdir.So a lock change still downloads every tool, as it does today, and a PR that changes
mise.lockdownloads every tool on each of its runs, because a PR does not save. The 6 retries cover a short reset of a download host. They do not cover an outage of several minutes.Evidence
www.lua.orgresetunit-tests:mise cache not found for mise-v1-linux-x64-2026.11.5-bd6141..., threeHTTP https://www.lua.org/ftp/lua-5.5.0.tar.gz attempt N failed (transient)warnings with retries in 184 ms, 780 ms and 3.56 s, thenlua@5.5.0 ... failed: ... Connection reset by peer. Run 43206955049 fails the same way, 3 minutes later, with the same keysrc/index.tsat the pinned SHA:processCacheKeyTemplatehashesMISE_CONFIG_FILE_PATTERNSgit diff --statof #545's merge showsmise.tomlchanged andmise.locknot. Themise.lockblob isf3df30fdon both sides, sohashFiles('mise.lock')is the samemise-v1-{{platform}}-{{version}}-<hash>withlinux-x64and2026.11.5asmise-v1-linux-x64-2026.11.5-<hash>mise-v1-linux-x64-2026.11.5-cb7d66...) has version7a8f38effbb6, the same as the caches of main. The first head's entry hase61abf9f359dcrates/mise-util/src/http.rs): the schedule is about 200 ms, 1 s, 4 s, then 15 s, each with a random factor of 0.5 to 1. A local probe against a refused URL with a scratch mise dir: defaulthttp_retriesmade 4 attempts in 4 s.MISE_HTTP_RETRIES=5made 6 attempts in 29 sunit-tests, stepSet up mise:www.lua.orgfailed 4 times (resets and a 500). The 5th attempt, after a 10.5 s backoff, downloaded Lua and the job passed. With mise's default of 3 retries it would have stopped after the 4th failed attempt, as on #545's runscache_key: mise-v1-{{platform}}-{{version}}-cb7d66..., the action printsmise cache not found for mise-v1-linux-x64-2026.11.5-cb7d66...(the first run with this key), thenCache saved from /home/runner/.local/share/mise with key: mise-v1-linux-x64-2026.11.5-cb7d66...actionlint1.7.12 on every workflow: exit 0.zizmor1.25.2 (--offline) on every workflow: no findings (25 suppressed), the same as main.mise run format-check: exit 0. Each of these ran on every headmise run clean && mise run allexits 0 in 848 s on the second head: 1737 successes, 0 failures, 0 errors, 28 pending,luacheck0 warnings in 185 files. No test readstest.yml, and the later heads change only comments and thecache_saveline, so the lint checks above cover themconventional-title,unit-tests,covered-lock,integration-tests (stable),integration-tests (v0.8.0)andRequiredd08f3953(fixed in61d7ea13: the cache now saves only on push), and 1 on the final head (rotate the cache key, because an earlier head could have saved a cache under it), declined with evidence. That head ran with the samemise.tomlandmise.lockas main, andmise.tomlhas no hook. 0 threads are unresolved. This PR used 8 Codex review requests, past the cap of 6. The maintainer's record is in the PR conversationNot covered
cache_key,cache_saveorMISE_HTTP_RETRIESkeeps CI green. A follow-up PR adds a workflow guard.mise cache restored from key: mise-v1-linux-x64-2026.11.5-cb7d661d..., but from an entry that run 43220955113 saved underrefs/pull/560/merge, which only this PR can read. The merge-queue run of this PR and every run until the first push to main whoseSet up misestep succeeds miss and download all 15 tools, with the 6 retries. The first push to main saves, and the next run with the samemise.lockrestores. The saved entry has the cache version of main's caches (see above).mise.lockedit that leaves the 15 tools of CI alone (an entry for macOS only, or for a tool that only a task installs) changes the hash and misses the cache.mise.toml,install_argsandMISE_ENV. A change that alters an install without changingmise.lock(a tool option inmise.toml) keeps the key. The comment intest.ymlsays to bumpv1then.mise.lockchange downloads every tool again. A download host that is down for longer than the 25 to 50 s of backoff still fails that run.hashFiles('mise.toml')on purpose: the rock list is in thesetuptask ofmise.toml.Generated with
xum• Model:anthropic:claude-sonnet-5-5• Thinking:xhigh