Skip to content

Stop falling back to a _build cache from a different mix.lock - #13

Merged
benwilson512 merged 1 commit into
mainfrom
no-build-cache-fallback
Sep 19, 2026
Merged

benwilson512 merged 1 commit into
mainfrom
no-build-cache-fallback

Conversation

@benwilson512

Copy link
Copy Markdown
Member

Problem

The _build cache step has a restore-keys prefix that omits the mix.lock hash. When the exact key misses, actions/cache restores the most recently created _build in the namespace — compiled from some other mix.lock.

That is unsafe. If a dependency's version has moved outside its new requirement, Mix reads the stale .app file in _build and mix deps.loadpaths fails hard rather than recompiling:

Unchecked dependencies for environment dev:
* leto (Hex package)
  the dependency does not match the requirement "~> 0.3", got "0.2.0"
** (Mix) Can't continue due to errors on dependencies

And because actions/cache saves in a post-if: success() step, a job that fails this way never saves a good _build under the new key — so it falls back to the same stale entry next run, forever.

sensetra hit this on 2026-09-18/19 after leto went ~> 0.1 → ~> 0.3 on main. Caches are visible only to the ref that created them plus the default branch, and the Lint workflow never runs on main, so every PR with an earlier green Lint run fell back to its own pre-bump _build/dev and failed on every push until the caches were purged by hand. Guides on the same commits passed only because ci.yml had been workflow_dispatched on main after the bump, seeding a main-scoped entry for its namespace.

Consumers previously avoided this by passing cache-key: v1-${{ hashFiles('mix.lock') }}, which put the lock hash into the shared prefix and made the fallback lock-specific. That was removed in a cleanup on 2026-09-16 because it looked redundant with the primary key — the fallback was the part it was protecting.

Change

Remove restore-keys from the _build cache only, with a comment explaining why so it isn't re-added as tidy-up.

  • _build is restored on an exact mix.lock match, otherwise compiled from scratch. On a lock change that is one cold dependency compile per namespace and scope; every run after it gets an exact hit. In sensetra a fully cold Lint run (deps fetch + deps compile + lint + app compile) was about 3½ minutes on a 4-vCPU runner.
  • deps/, ~/.hex and ~/.cache/rebar3 keep their fallbacks. Those are safe: mix deps.get reconciles deps/ against the lock and a re-fetched dependency is recompiled rather than validated against its old build, and the other two are download caches.

This is the same trade CargoSense's CircleCI config makes — every restore key there ends in {{ checksum "mix.lock" }}, with no cross-lock fallback.

Release

v1 currently points at the v1.6.0 commit. After merge this needs a v1.7.0 tag and v1 moved to it for consumers on @v1 to pick it up.

Related

  • CargoSense/sensetra#3833 bumps cache-key to v2 as a workaround; it becomes unnecessary once v1 carries this change.

🤖 Generated with Claude Code

The `_build` cache had a `restore-keys` prefix without the lock hash, so a
run whose exact key missed restored the most recent `_build` compiled from
some other mix.lock. That is unsafe: when a dependency's version has moved
outside its new requirement, Mix reads the stale `.app` file and fails
`mix deps.loadpaths` with a hard "does not match the requirement" error
rather than recompiling, and because `actions/cache` only saves on
success the job can never repair its own namespace.

sensetra hit exactly this after a leto `~> 0.1` -> `~> 0.3` bump: every PR
with an earlier green Lint run fell back to its own pre-bump `_build/dev`
and failed on `leto ... got "0.2.0"` until the caches were purged by hand.

Drop the fallback for `_build` only. A lock change now compiles
dependencies from scratch once per namespace; every run after it gets an
exact hit. The deps, Hex and Rebar caches keep their fallbacks, which are
safe because `mix deps.get` reconciles them against the lock and a
re-fetched dependency is recompiled rather than validated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@benwilson512
benwilson512 requested a review from a team as a code owner September 19, 2026 02:23
@benwilson512
benwilson512 merged commit f302b39 into main Sep 19, 2026
2 checks passed
@benwilson512
benwilson512 deleted the no-build-cache-fallback branch September 19, 2026 02:27
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