Skip to content

fix: include runner image in cache key to prevent cross-provider collisions#456

Merged
jdx merged 1 commit intomainfrom
fix/cache-key-include-runner-image
Apr 30, 2026
Merged

fix: include runner image in cache key to prevent cross-provider collisions#456
jdx merged 1 commit intomainfrom
fix/cache-key-include-runner-image

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 30, 2026

Problem

The default cache key was mise-v1-{os}-{arch}-{file_hash} — no runner-image discriminator. Any repo whose CI runs on multiple runner providers with the same os/arch shares one cache slot:

  • github-hosted macos-latest
  • namespace.so nscloud-macos-sequoia-arm64-* / namespace-profile-*-macos-arm64
  • self-hosted M-series macs
  • BuildJet, blacksmith, etc.

When a repo migrates from one provider to another, the new run restores the previous provider's tool installs (~200 MB of ~/.local/share/mise/installs/*), and tools that loaded fine in the original image break in the new one.

Concrete failures observed

Discovered while migrating jdx/hk from github-hosted to namespace.so. Same mise-v1-macos-arm64-<hash> cache hit on namespace; tool resolution fails everywhere:

mise ERROR Tool 'ubi:koalaman/shellcheck' does not have an executable named 'shellcheck'
mise ERROR Tool 'gem:asciidoctor' does not have an executable named 'asciidoctor'
mise ERROR Tool 'aqua:betterleaks/betterleaks' does not have an executable named 'betterleaks'
mise ERROR Tool 'biome' does not have an executable named 'biome'
mise ERROR Tool 'buf' does not have an executable named 'buf'
mise ERROR Tool 'github:google/google-java-format' does not have an executable named 'google-java-format'

— installs are present (cache restored 185 MB) but the executable layout from the github-hosted macOS-15 image doesn't match what mise expects on namespace's macOS arm64 image.

On Linux, cached binaries built against the github-hosted ubuntu glibc/CPU featureset SIGILL on namespace's image (e.g. swiftlint exit code 132).

Fix

Append the GitHub Actions hosted-runner ImageOS env var (e.g. macos15, ubuntu24) to the platform segment of the default cache key. Other runners pool under self-hosted.

const imageOS = process.env.ImageOS || 'self-hosted'
return `${base}-${imageOS}`

After this change:

  • mise-v1-macos-arm64-macos15-<hash> (github-hosted)
  • mise-v1-macos-arm64-self-hosted-<hash> (namespace, self-hosted, etc.)

Users with multiple self-hosted profiles that need finer scoping can set cache_key_prefix per workflow. The README's docs for {{platform}} are updated to reflect the new format.

Trade-offs

  • One-time cache miss for everyone on the next run after upgrade. Cache rebuilds and stays scoped per-image after that.
  • Hosted-runner image rolls (e.g. macos15macos16) will invalidate cache, which is desirable — that's exactly when stale binaries cause problems.
  • Self-hosted users with mixed runner pools all share one self-hosted slot. They'd need cache_key_prefix per pool, same as before. This PR doesn't make that worse.

Test plan

  • Verify dist/index.js rebuilt cleanly (yes, npm run package succeeded with the change visible at getTarget() callsite).
  • Run on a github-hosted runner — confirm ImageOS is read from env (e.g. macos15) and shows up in the mise cache restored from key: log line.
  • Run on a non-hosted runner — confirm fallback to -self-hosted.
  • Verify a workflow that switched providers no longer pulls a poisoned cache.

Note

Medium Risk
Changes cache-key generation and will cause a one-time cache miss plus different cache partitioning, which can affect build times and cache reuse across runners.

Overview
Updates the default cache-key {{platform}} value to append a runner image discriminator (process.env.ImageOS on GitHub-hosted runners, otherwise self-hosted), reducing cross-provider/image cache collisions that can restore incompatible tool installs.

Implements this via a new getRunnerImageId() helper used during cache-key template processing, and documents the new {{platform}} format in the README; dist/index.js is rebuilt accordingly.

Reviewed by Cursor Bugbot for commit ef1bd0e. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request updates the {{platform}} template variable to include the runner's operating system image (e.g., ubuntu24), aiming to prevent cache collisions across different runner environments. However, the current implementation in getTarget() is problematic as it breaks the construction of download URLs for the mise binary, which do not include these suffixes. It is recommended to separate the platform string used for downloads from the one used for cache keys. Additionally, the documentation for the {{platform}} variable should be simplified to be more user-friendly.

Comment thread src/index.ts Outdated
// images export ImageOS (e.g. "macos15", "ubuntu24"); other runners
// typically don't, so they pool under "self-hosted".
const imageOS = process.env.ImageOS || 'self-hosted'
return `${base}-${imageOS}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

This change will break the installation of the mise binary. The getTarget() function is used in setupMise() (lines 315 and 317) to construct the download URL for the mise executable. Since mise releases do not include the ImageOS suffix (e.g., -ubuntu24 or -self-hosted) in their asset names, the resulting URLs will be invalid (e.g., https://mise.jdx.dev/mise-latest-linux-x64-ubuntu24.tar.gz), causing the action to fail during the setup phase.

You should separate the platform string used for binary downloads from the one used for the cache key. A better approach would be to keep getTarget() returning the base platform and append the ImageOS discriminator directly in processCacheKeyTemplate() (where platform is assigned at line 521).

Comment thread README.md
- `{{version}}` - The mise version (from the `version` input)
- `{{cache_key_prefix}}` - The cache key prefix (from `cache_key_prefix` input or default)
- `{{platform}}` - The target platform (e.g., "linux-x64", "macos-arm64")
- `{{platform}}` - The target platform, including the runner image (e.g., "linux-x64-ubuntu24", "macos-arm64-macos15", "linux-x64-self-hosted"). The trailing segment is `process.env.ImageOS` on github-hosted runners and falls back to `"self-hosted"` elsewhere — preventing cache collisions when the same repo runs on different runner providers (github-hosted, namespace.so, self-hosted).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The description for {{platform}} is quite technical. Instead of mentioning process.env.ImageOS, it might be clearer to describe it as the runner's operating system image version. This makes the documentation more accessible to users who aren't familiar with GitHub Actions internal environment variables.

Suggested change
- `{{platform}}` - The target platform, including the runner image (e.g., "linux-x64-ubuntu24", "macos-arm64-macos15", "linux-x64-self-hosted"). The trailing segment is `process.env.ImageOS` on github-hosted runners and falls back to `"self-hosted"` elsewhere — preventing cache collisions when the same repo runs on different runner providers (github-hosted, namespace.so, self-hosted).
- `{{platform}}` - The target platform, including the runner image version (e.g., "linux-x64-ubuntu24", "macos-arm64-macos15", "linux-x64-self-hosted"). This prevents cache collisions when the same repository runs on different runner providers (GitHub-hosted, namespace.so, self-hosted).

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

This PR fixes cross-provider cache collisions in mise-action by appending a getRunnerImageId() suffix (sourced from process.env.ImageOS, falling back to "self-hosted") to the {{platform}} template variable in processCacheKeyTemplate. Importantly, the discriminator is only applied in the cache-key path — getTarget() itself is untouched and download URLs (lines 315/317) continue to use the bare os-arch[-musl] form, so the previous P0 concern from the Cursor Bugbot review does not apply to the current implementation.

Confidence Score: 5/5

Safe to merge — the image discriminator is correctly scoped to cache keys only, leaving download URLs intact.

No P0 or P1 issues present. The previously flagged P0 (image suffix leaking into download URLs) does not apply to this implementation — getTarget() is unchanged and the getRunnerImageId() suffix is applied exclusively inside processCacheKeyTemplate. The fix is minimal, well-documented, and trade-offs are acknowledged in the PR description.

No files require special attention.

Important Files Changed

Filename Overview
src/index.ts Adds getRunnerImageId() helper returning ImageOS env var or "self-hosted"; appends it only in processCacheKeyTemplate — download URL calls to getTarget() are unaffected. Minor refactor of arch variable in getTarget() is functionally equivalent.
README.md Updates {{platform}} description to reflect new os-arch-<image> format; accurate and informative.
dist/index.js Compiled artifact rebuilt via npm run package; no manual review needed beyond verifying it reflects the source changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Action runs] --> B{cache_key input set?}
    B -- yes --> C[Custom template]
    B -- no --> D[Default template]
    C & D --> E[processCacheKeyTemplate]

    E --> F[getTarget returns os-arch]
    E --> G[getRunnerImageId returns ImageOS or self-hosted]
    F & G --> H[platform = os-arch-imageId]
    H --> I[Cache key scoped per runner image]

    J[Download binary] --> K[getTarget only - no image suffix]
    K --> L[Download URL unchanged by this PR]
Loading

Reviews (2): Last reviewed commit: "fix: include runner image in cache key t..." | Re-trigger Greptile

…isions

The cache key included only os and arch (`mise-v1-macos-arm64-<hash>`),
so any repo running CI on multiple runner providers with the same
os/arch — github-hosted, namespace.so, BuildJet, self-hosted M-series
macs — would clobber each other's caches. The first run on a new
provider would restore tool installs built for the previous provider's
image, leading to "does not have an executable named X" errors or
SIGILL crashes on cached binaries built against a different glibc.

Append the GitHub Actions hosted-runner ImageOS env var (e.g.
"macos15", "ubuntu24") to the cache key's platform segment. Other
runners pool under "self-hosted"; users running multiple self-hosted
profiles can scope further with cache_key_prefix.

Implementation note: `getTarget()` is also used to construct the mise
binary download URL (must match the release-asset filename, e.g.
`mise-v2026.4.0-macos-arm64.tar.gz`). Adding the image suffix there
would 404 the download. So `getTarget()` keeps its current shape and
a new `getRunnerImageId()` helper composes the image discriminator
into the cache-key platform segment only.

This is a one-time cache miss for existing users; the cache rebuilds
on the next run and stays scoped per-image after that.
@jdx jdx force-pushed the fix/cache-key-include-runner-image branch from 315e733 to ef1bd0e Compare April 30, 2026 14:10
@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Apr 30, 2026

Refactored after spotting that getTarget() is also used to construct the mise binary download URL — appending an image suffix there would 404 the asset name (mise-v2026.4.0-macos-arm64-macos15.tar.gz doesn't exist).

Split into two responsibilities:

  • getTarget() keeps its current shape — os-arch[-musl], matching the mise release-asset filename. Also tightened the mutable-let into a const.
  • New getRunnerImageId() returns the ImageOS || 'self-hosted' discriminator.
  • processCacheKeyTemplate composes ${getTarget()}-${getRunnerImageId()} for {{platform}}.

Download URLs are unchanged; only the cache key gets the new suffix.

This comment was generated by Claude Code.

jdx added a commit to jdx/hk that referenced this pull request Apr 30, 2026
Workaround for jdx/mise-action#456: the action's default cache key
was os+arch only, so github-hosted and namespace caches collide and
restoring one onto the other corrupts tool installs (e.g. swiftlint
SIGILL on Linux, "no executable named X" across backends on macOS).

Set cache_key_prefix to include runner.environment ("github-hosted"
or "self-hosted") so fork-PR (github-hosted) and main/non-fork
(namespace) runs use separate cache pools.

Drop once mise-action#456 lands and we re-pin to the new version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx merged commit b287efd into main Apr 30, 2026
21 checks passed
@jdx jdx deleted the fix/cache-key-include-runner-image branch April 30, 2026 14:15
@jdx jdx mentioned this pull request Apr 30, 2026
jdx added a commit to jdx/hk that referenced this pull request Apr 30, 2026
Re-pin jdx/mise-action to b287efd (post jdx/mise-action#456 merge),
which builds runner.environment-style discrimination into the cache
key automatically. Drop the cache_key_prefix workaround everywhere
except release-plz.yml, where the with: block keeps experimental:
true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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