pre-commit-advisory.yml saves the Go module + build + golangci-lint caches under a key containing github.sha:
# Restore Go hook caches (line ~248)
key: go-hook-caches-${{ runner.os }}-${{ github.sha }}
restore-keys: |
go-hook-caches-${{ runner.os }}-
# Save Go hook caches (cache seed) (line ~327)
- uses: actions/cache/save@v6
with:
path: |
~/go/pkg/mod
~/.cache/go-build
~/.cache/golangci-lint
key: go-hook-caches-${{ runner.os }}-${{ github.sha }}
This works as designed — every commit has a unique SHA, so the exact key never hits, the restore always comes from the prefix, and what it gets is the most recently saved entry. The workflow comments state that intent explicitly ("the previously saved cache is still exact — the restore-keys prefix serves it regardless of which sha it was saved under"). I am not proposing to change the behaviour.
The issue is the cost: each save writes a brand-new entry rather than updating one, and the previous entry is never deleted. Old entries linger until GitHub's LRU evicts them against the 10 GB per-repo cap.
Measured in pinpredict/k5s
The entry is ~2000 MB and it's written on every push to main that touches Go files (the save is gated on github.event_name == 'push' + go-changes.changed == 'true', so it is not literally every commit — but it is frequent on an active Go repo).
That repo was sitting at 9.63 GB of the 10 GB cap across 64 entries, evicting continuously. Concretely, within about one hour a 2 GB go-hook-caches-Linux-<sha> entry was written and then evicted, and it was one of the pressures that had been silently killing a different cache: the release job's cross-compile cache, written every release and read by none, because it was always the least-recently-used thing in the repo.
That cost that repo ~9 minutes on every release until it was found (pinpredict/k5s#161). The releases went 17m30s → 2m09s once the cache actually survived. So this is not theoretical pressure — it had a measurable price, and any repo using this reusable alongside a large Go dep graph is paying some version of it.
Two possible fixes
A. Delete the previous entry after saving. Keeps the rolling-freshness behaviour exactly as-is; just stops the garbage accumulating. Roughly:
- name: Reclaim the superseded Go hook cache
if: <same condition as the save>
continue-on-error: true
run: |
gh cache list --repo "$GITHUB_REPOSITORY" --limit 100 \
--json id,key,createdAt \
--jq '[.[] | select(.key | startswith("go-hook-caches-${{ runner.os }}-"))]
| sort_by(.createdAt) | reverse | .[1:] | .[].id' \
| while read -r id; do gh cache delete "$id" --repo "$GITHUB_REPOSITORY"; done
B. Key on content instead of the SHA, e.g. hashFiles('**/go.sum'). The entry is then rewritten only when dependencies change. Go's build cache and module cache are content-addressed, so a restore that is behind on source changes is partially useful, never wrong — the hooks recompile only what actually differs. This is what actions/setup-go's own caching does, and what the sibling go-pre-commit step in this same workflow already does (hashFiles('.pre-commit-config.yaml')).
I'd lean to B for its simplicity — it needs no gh call, no extra permissions, and no cleanup step that can fail — but A preserves the current always-freshest semantics exactly, which may be the reason the SHA key was chosen in the first place. Whoever owns the original decision is better placed to judge that than I am.
Happy to open a PR for either once there's a preference.
Note for anyone hitting this now
Reclaiming space in an affected repo is safe — caches regenerate:
gh cache list --repo <repo> --limit 100 --json id,key,sizeInBytes,ref,lastAccessedAt
gh cache delete <id> --repo <repo>
Look especially for entries under a refs/tags/* or a merged refs/pull/*/merge ref — a tag- or PR-scoped cache cannot serve main, so those are usually pure garbage. Clearing two such entries in k5s took it from 9.63 GB to 6.05 GB.
pre-commit-advisory.ymlsaves the Go module + build + golangci-lint caches under a key containinggithub.sha:This works as designed — every commit has a unique SHA, so the exact key never hits, the restore always comes from the prefix, and what it gets is the most recently saved entry. The workflow comments state that intent explicitly ("the previously saved cache is still exact — the restore-keys prefix serves it regardless of which sha it was saved under"). I am not proposing to change the behaviour.
The issue is the cost: each save writes a brand-new entry rather than updating one, and the previous entry is never deleted. Old entries linger until GitHub's LRU evicts them against the 10 GB per-repo cap.
Measured in pinpredict/k5s
The entry is ~2000 MB and it's written on every push to
mainthat touches Go files (the save is gated ongithub.event_name == 'push'+go-changes.changed == 'true', so it is not literally every commit — but it is frequent on an active Go repo).That repo was sitting at 9.63 GB of the 10 GB cap across 64 entries, evicting continuously. Concretely, within about one hour a 2 GB
go-hook-caches-Linux-<sha>entry was written and then evicted, and it was one of the pressures that had been silently killing a different cache: the release job's cross-compile cache, written every release and read by none, because it was always the least-recently-used thing in the repo.That cost that repo ~9 minutes on every release until it was found (pinpredict/k5s#161). The releases went 17m30s → 2m09s once the cache actually survived. So this is not theoretical pressure — it had a measurable price, and any repo using this reusable alongside a large Go dep graph is paying some version of it.
Two possible fixes
A. Delete the previous entry after saving. Keeps the rolling-freshness behaviour exactly as-is; just stops the garbage accumulating. Roughly:
B. Key on content instead of the SHA, e.g.
hashFiles('**/go.sum'). The entry is then rewritten only when dependencies change. Go's build cache and module cache are content-addressed, so a restore that is behind on source changes is partially useful, never wrong — the hooks recompile only what actually differs. This is whatactions/setup-go's own caching does, and what the siblinggo-pre-commitstep in this same workflow already does (hashFiles('.pre-commit-config.yaml')).I'd lean to B for its simplicity — it needs no
ghcall, no extra permissions, and no cleanup step that can fail — but A preserves the current always-freshest semantics exactly, which may be the reason the SHA key was chosen in the first place. Whoever owns the original decision is better placed to judge that than I am.Happy to open a PR for either once there's a preference.
Note for anyone hitting this now
Reclaiming space in an affected repo is safe — caches regenerate:
Look especially for entries under a
refs/tags/*or a mergedrefs/pull/*/mergeref — a tag- or PR-scoped cache cannot servemain, so those are usually pure garbage. Clearing two such entries in k5s took it from 9.63 GB to 6.05 GB.