IntersectMBO / IntersectMBO/cardano-api
haskell-wasm.yml: cabal store cache key hashes a file that is never written, so the cache is frozen and master saves always fail
- Dominant language
- Haskell
- Stars
- 40
- Forks
- 30
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 30
Description
## Summary
The wasm cabal store cache key in `haskell-wasm.yml` is meant to vary with the dependency set, but its `hashFiles(...)` component always evaluates to the empty string.
The computed key is therefore the constant `wasm-cache-2026-05-29-Linux-9.14.1-`, with a bare trailing dash where the hash should be.
GitHub Actions cache entries are immutable per scope, so each scope's cache froze the first time it was saved and can never be refreshed.
Every run since then restores that same stale snapshot, rebuilds the drift (8 to 18 minutes per run at present), and the rebuilt store is thrown away because the save step can no longer create a new entry under the frozen key.
## Root cause
The "Record dependencies" step writes `dependencies.txt` to the repository root, because the step sets no `working-directory`:
https://github.com/IntersectMBO/cardano-api/blob/42ef3b19dc98cbbcbf7d6a0402eac7f45e3ddfe3/.github/workflows/haskell-wasm.yml#L104
The cache key hashes `cardano-wasm/dependencies.txt`, a path that never exists:
https://github.com/IntersectMBO/cardano-api/blob/42ef3b19dc98cbbcbf7d6a0402eac7f45e3ddfe3/.github/workflows/haskell-wasm.yml#L125
`hashFiles` returns an empty string when no file matches, instead of failing, so the mistake is silent.
The computed key then equals the `restore-keys` prefix exactly:
https://github.com/IntersectMBO/cardano-api/blob/42ef3b19dc98cbbcbf7d6a0402eac7f45e3ddfe3/.github/workflows/haskell-wasm.yml#L126-L127
The save step reuses the same broken key via `steps.cache.outputs.cache-primary-key`:
https://github.com/IntersectMBO/cardano-api/blob/42ef3b19dc98cbbcbf7d6a0402eac7f45e3ddfe3/.github/workflows/haskell-wasm.yml#L138
https://github.com/IntersectMBO/cardano-api/blob/42ef3b19dc98cbbcbf7d6a0402eac7f45e3ddfe3/.github/workflows/haskell-wasm.yml#L144-L145
`CABAL_CACHE_VERSION` is currently `"2026-05-29"`:
https://github.com/IntersectMBO/cardano-api/blob/42ef3b19dc98cbbcbf7d6a0402eac7f45e3ddfe3/.github/workflows/haskell-wasm.yml#L33
## Evidence
From the "Restore cached dependencies" and "Cache Cabal store" steps of https://github.com/IntersectMBO/cardano-api/actions/runs/32740897668/job/97474832341 (PR #1318, pull_request event, 2026-08-24):
```
Cache hit for: wasm-cache-2026-05-29-Linux-9.14.1-
Cache restored from key: wasm-cache-2026-05-29-Linux-9.14.1-
Cache saved with key: wasm-cache-2026-05-29-Linux-9.14.1-
```
Note the bare trailing dash: `hashFiles` contributed nothing.
On `master`, and on any branch scope that already holds this key, the save step can no longer create a fresh entry: GitHub Actions cache scopes are immutable, so once the constant key exists there, every later save attempt logs a reservation failure instead of storing a new snapshot (the step itself does not hard-fail the job, since `actions/cache/save` treats this as a warning).
From https://github.com/IntersectMBO/cardano-api/actions/runs/35039810633/job/104616933344 (push to `master`, 2026-09-16):
```
Failed to save: Unable to reserve cache with key wasm-cache-2026-05-29-Linux-9.14.1-, another job may be creating this cache.
```
The repository's live cache listing (`GET /repos/IntersectMBO/cardano-api/actions/caches`) currently shows exactly three `wasm-cache` entries, one per branch scope (`master`, a merge queue ref, a PR merge ref), and all three share the identical key text `wasm-cache-2026-05-29-Linux-9.14.1-`, differing only in scope.
"Install dependencies" durations despite an exact cache hit on every one of these runs:
| Job | Event | Install dependencies |
|---|---|---|
| [97474832341](https://github.com/IntersectMBO/cardano-api/actions/runs/32740897668/job/97474832341) | pull_request (#1318) | 18.2 min |
| [104714884204](https://github.com/IntersectMBO/cardano-api/actions/runs/35071827394/job/104714884204) | merge_group | 9.8 min |
| [104616933344](https://github.com/IntersectMBO/cardano-api/actions/runs/35039810633/job/104616933344) | push to master | 12.1 min |
| [104597739601](https://github.com/IntersectMBO/cardano-api/actions/runs/35033664464/job/104597739601) | pull_request (reused branch) | 8.7 min |
A working cache would make this step a near no-op, as the comment above the step expects.
## Fix
### Minimal fix
Either one-line change restores a varying key:
1. Point the key at the real file: `hashFiles('dependencies.txt')`.
2. Or write the file where the key looks: `... | sort | uniq > cardano-wasm/dependencies.txt`.
Additionally, gate the save step with `if: steps.cache.outputs.cache-hit != 'true'` so exact-hit runs stop tripping the reservation-conflict warning.
No `CABAL_CACHE_VERSION` bump is needed.
The first run after the fix misses the new exact key, falls back through `restore-keys` to the frozen snapshot, rebuilds only the drift, and saves under a proper hashed key.
Later runs then prefer the fresher hashed entries, and the frozen blob ages out on its own after 7 days of disuse.
### Ideal fix
`haskell.yml` in this repository already avoids this whole class of bug by delegating dependency caching to the composite action [`input-output-hk/cardano-dev/actions/cabal-cache@cabal-cache-0.0.1.0`](https://github.com/input-output-hk/cardano-dev/blob/cabal-cache-0.0.1.0/actions/cabal-cache/action.yml).
That action writes its dependency list under `runner.temp` and hashes it with `sha256sum`, so there is no `hashFiles(...)` path to get wrong in the first place.
`haskell-wasm.yml` should replace its five hand-rolled steps ("Record dependencies", "Store month number...", "Restore cached dependencies", "Install dependencies", "Cache Cabal store") with this same composite action, so both workflows share one tested caching implementation.
This is not a drop-in replacement today, so it should not block the minimal fix above.
The action's `cabal-store` input already accommodates a non-default store path, so the wasm build's `wasm32-wasi-cabal path --store` output can be passed straight through with no changes needed there.
But the action's three internal steps ("Cabal update", "Build dry run", "Install dependencies") hardcode the literal `cabal` executable and the `all --enable-tests --minimize-conflict-set` build target, which is how `haskell.yml` itself uses it; `haskell-wasm.yml` needs `wasm32-wasi-cabal` and a single-package, no-tests target (`cardano-wasm --dry-run` / `cardano-wasm --only-dependencies`), and the action exposes no input to override either the executable name or the build target/flags.
Using it for `haskell-wasm.yml` therefore needs a small upstream change to `cardano-dev` first, for example a `cabal-executable` input (default `cabal`) plus a way to override the dry-run and install-dependencies command.
Separately, the action's own save step has no `cache-hit` gate either, so adopting it would not by itself remove the reservation-conflict warning noise described above; that gate is worth adding to the composite action too, for the benefit of both workflows.
Until that upstream work lands, apply the minimal fix above.
Contributor guide
Assessment
This issue has not been assessed yet.