fix(ci): cache Go modules once, not once per compile flavor - #446
Conversation
~/go/pkg/mod is a pure function of go.sum — byte-identical for every compile flavor — but it was cached together with ~/.cache/go-build under a key partitioned by go-cache-suffix. That stored the same 1.6 GB five times (-lint, -unit, -integration, -e2e-cov, -cov), ~5 GB per go.sum generation. Two live generations is the steady state (a bump mints a new set while the previous is still warm), so the repo sat at ~10 GB against GitHub's hard 10 GB cap; #438's 24-module bump tipped it to 10.53 GB and GitHub began LRU-evicting warm entries mid-run. Split into gomod-v1 (~/go/pkg/mod, unsuffixed, shared by every Go job) and gobuild-v3 (~/.cache/go-build only, still per flavor). ~5 GB -> ~2 GB per generation, so two generations fit with headroom. The v3 bump is required: saves fire only on an exact-key miss, so without it the old v2 entry — still carrying the module cache — would exact-hit forever and the smaller content would never be saved. gobuild-v3 also drops the bare-prefix restore-key, which existed only to borrow another flavor's copy of the module cache; that job is now gomod-v1's, and another flavor's build objects aren't reusable. Documents the sizing constraint in the workflows README so the next cache addition budgets for two generations against the 10 GB cap. Closes #443 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up. The split consolidated five copies of ~/go/pkg/mod but left a sixth outside setup-env: actions/setup-go caches by default, and publish-dev.yml runs on every push to main, so its go.sum-keyed entry was ~1 GB live — more than the gomod-v1 entry this PR consolidates to, and a direct violation of the sizing rule the PR adds. release.yml carries the same default. Both now pass cache: false, matching the call goreleaser-validate.yml already made. Also documents what the inventory was hiding: the CodeQL caches GHAS default setup mints outside this repo's workflows (~0.4 GB), so a maintainer doing the two-generations check from the table sees the whole budget rather than 85% of it. Adds the narrowing-rotation exception to the key-versioning policy. The policy said to keep old prefixes as transitional restore-keys; gobuild-v3 deliberately does not, because a v2 archive still carries ~/go/pkg/mod and restoring it would re-materialize exactly what the rotation removes. The doc now prescribes what the code does. CHANGELOG entry for the whole change per AGENTS.md Documentation Sync. Pre-existing docs drift the gate surfaced is tracked in #444, not folded in here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up. The sizing policy mixed on-disk size with stored-archive size, so the arithmetic it tells the next maintainer to perform didn't close: ~1.6 GB (a du of ~/go/pkg/mod) times five entries is 8 GB, not the ~5 GB quoted. What counts against the 10 GB cap is the compressed entry. Live API data: the five gobuild-v2 entries measure 0.97-1.27 GB each (5.57 GB total) and the setup-go entry holding the same module tree is 1.045 GB. So the tree is ~1.6 GB on disk and ~1 GB stored, and a generation is five entries of ~1.1 GB — ~5.5 GB. Budget statements now quote stored sizes, with the on-disk figure kept only where it explains what the directory actually contains. Same wording in the action header, the README table and policy, and the CHANGELOG. Also rewords "a seventh copy" of the setup-go cache, which contradicted the "sixth copy" in the CHANGELOG and the previous commit subject; the ordinal depended on whether you counted pre- or post-split, so it now just says it duplicates what gomod-v1 holds once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
No content change. 1f18ead inserted the stored-archive figures without re-wrapping, leaving a 39-char orphan line mid-paragraph in the setup-env comment block and a 20-char one in the README sizing policy, both inside paragraphs that otherwise wrap at ~72 cols. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe CI cache architecture separates shared Go module data from per-flavor build objects. Publish workflows disable ChangesGo cache architecture
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR reduces GitHub Actions cache pressure by separating the Go module cache (shared across all compile “flavors”) from the Go build object cache (still flavor-specific), preventing redundant storage that was pushing the repo past GitHub’s hard 10 GB cache cap and triggering LRU evictions.
Changes:
- Split
setup-envcaching into two keys: an unsuffixed~/go/pkg/modcache (gomod-v1) and a per-flavor~/.cache/go-buildcache (gobuild-v3), with key-version bumps to ensure new content is actually saved. - Disable
actions/setup-go’s default caching in release-oriented workflows (publish-dev.yml,release.yml) to avoid minting an additional redundant ~1 GB cache entry. - Update workflow documentation to reflect the new cache inventory and add an explicit sizing/keying policy to prevent recurrence.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
.github/actions/setup-env/action.yml |
Splits Go module vs build-object caches into separate keys and updates restore-key strategy accordingly. |
.github/workflows/publish-dev.yml |
Disables actions/setup-go cache to avoid an extra go.sum-keyed duplicate cache entry on every push to main. |
.github/workflows/release.yml |
Disables actions/setup-go cache for tag-triggered releases to avoid redundant cache budget usage. |
.github/workflows/README.md |
Updates cache inventory and documents key-versioning + sizing policies, including the “narrowing rotation” exception. |
CHANGELOG.md |
Records the cache-splitting fix and its motivation/impact under “Fixed”. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
📚 Docs preview is live → https://538bec56-wavehouse-docs.wave-rf.workers.dev
|
Code Coverage OverviewLanguages: Go GoThe overall coverage in commit 7aa2326 in the Show a code coverage summary of the most impacted files.
Updated |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 10fb7386-4b4c-411f-b570-e11f83210410
📒 Files selected for processing (5)
.github/actions/setup-env/action.yml.github/workflows/README.md.github/workflows/publish-dev.yml.github/workflows/release.ymlCHANGELOG.md
📜 Review details
⏰ Context from checks skipped due to timeout. (12)
- GitHub Check: E2E tests
- GitHub Check: Coverage
- GitHub Check: Docs build
- GitHub Check: Unit tests
- GitHub Check: Integration tests
- GitHub Check: copilot-pull-request-reviewer
- GitHub Check: Lint
- GitHub Check: Validate snapshot build
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-06-10T15:01:09.027Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: docs/src/content/docs/development.md:0-0
Timestamp: 2026-06-10T15:01:09.027Z
Learning: In this repo’s Markdown review (all .md files), do not flag capitalization/style issues for literal paths starting with ".github/" (or any substring that is a path beginning with ".github/"). Treat ".github" as the correct lowercase dotfile directory name, even when it appears inside prose or code spans; automated checks such as LanguageTool’s "(GITHUB)" rule commonly produce false positives for this literal filesystem path.
Applied to files:
CHANGELOG.md
🪛 LanguageTool
.github/workflows/README.md
[style] ~196-196: Since ownership is already implied, this phrasing may be redundant./go/pkg/mod
Context: ...ml (cache: falseon each). It stores its own go.sum-keyed copy of+...
(PRP_OWN)
🔇 Additional comments (4)
.github/workflows/README.md (2)
194-205: LGTM!
207-237: LGTM!.github/workflows/release.yml (1)
27-31: LGTM!.github/actions/setup-env/action.yml (1)
120-126: 🚀 Performance & ScalabilityNo suffix change is needed. All Go-enabled callers use distinct suffixes:
-lint,-unit,-integration,-e2e-cov, and-cov.> Likely an incorrect or invalid review comment.
CodeRabbit review. GOTOOLCHAIN=auto lands the toolchain in
~/go/pkg/mod/golang.org/toolchain (we run no setup-go in ci.yml), and
go.sum records no entry for it — this repo's go.sum has zero
golang.org/toolchain lines. So raising go.mod's `go` directive changes
which toolchain belongs in the cache while leaving go.sum byte-identical.
Verified: with go 1.26.5 -> 1.27.0, sha256 of go.sum alone is unchanged
(bcc16701da2c001f both before and after) while go.mod+go.sum rotates.
Under the old key that is a permanent regression, not a one-off — the
stale key exact-hits, and because saves fire only on an exact-key MISS
the freshly fetched toolchain is never saved, so every subsequent run
re-downloads it.
Both Go keys now hash ('**/go.mod', '**/go.sum'). Costs no extra rotation
in practice: dependency bumps already touch both files, so the only newly
rotating case is precisely the toolchain one this fixes.
Also corrects the setup-go rationale in publish-dev.yml and the CHANGELOG:
actions/cache saves on a cache MISS, so that entry is re-saved on each
dependency bump, not "minted afresh on every push to main". The budget
argument is unchanged — one live ~1 GB entry per generation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up on the go.mod key change. The gobuild-v3 block never stated its own reason for hashing go.mod — the header just said "same key inputs", pointing at a rationale (the toolchain rides in ~/go/pkg/mod) that doesn't apply to ~/.cache/go-build. The real reason is stronger and is what stops someone simplifying this key back to go.sum: the compiler's build ID feeds every action hash, so a toolchain bump invalidates every object in the build cache. Keyed on go.sum alone that bump exact-hits, nothing restored is reusable, and the new objects are never saved — every run recompiles cold and drags in ~1 GB of dead objects until an unrelated go.sum change rotates the key. Also updates the sizing policy's trigger list: after this change a go.mod-only bump mints a new generation too, and that paragraph is the one a maintainer reads before doing budget arithmetic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
The exception prescribes purging the stale entries but only shipped inspect commands, so a maintainer following it post-rotation had to improvise the delete. Adds the snippet, the after-not-before ordering (main still restores the old keys until the rotation lands, so an early purge just forces a cold repopulate), and the reminder to include every family the rotation orphans — turning on cache: false strands that job's setup-go-* entry as well as the renamed one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up. The split has now minted real entries, so the estimates
are replaced with readings from the live cache API:
gomod-v1 493 MB
gobuild-v3 unit/integration/lint/e2e-cov/cov
152 / 148 / 140 / 120 / 25 MB
generation total 1.05 GB (was 5.56 GB)
Two numbers were wrong. "~1 GB stored" for ~/go/pkg/mod was inferred from
the combined v2 entries rather than read; a cold save is 0.49 GB, drifting
up as superseded versions accumulate through the restore -> save chain, so
both halves of that are now stated. And "~1 GB of dead objects" in the
build-key comment overstated ~/.cache/go-build by 6-40x — that figure
belonged to the pre-split combined entry, and in the failure mode being
described the key exact-hits forever so the entry can't grow into it. The
win is larger than documented (1.05 GB, not ~2 GB), so the old figures
erred conservative, but they broke the PR's own arithmetic.
Also records the residual gap hashing go.mod does NOT close: GOTOOLCHAIN
=auto uses the local toolchain whenever it satisfies the directive, so a
runner image bumping its bundled Go swaps the compiler with neither
go.mod nor go.sum moving — same stale-exact-hit pathology, past both keys.
Left as a documented cause to check rather than plumbed into the key,
since resolving the toolchain before the cache restore would forfeit the
property that the restore captures the toolchain download.
And fixes the go-cache-suffix description, which still said "Empty = the
shared default key" — pre-split wording. Post-split an empty suffix yields
a restore-key that prefix-matches every flavor, the exact cross-flavor
restore this split removes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up, and a repeat of the defect the previous commit set out to fix. The measured figures were each read from the live API but converted three different ways: 0.49/0.59 were MiB/1000, 1.05 was bytes/2^30, 5.56 was bytes/10^9. So the decomposition disagreed with its own total (0.49 + 0.59 = 1.08, not 1.05) and the improvement read as 5.3x when it is 4.9x. Everything is now stored-archive bytes / 2^30 — the unit the README's own usage check prints, so a reader running the documented command sees the number the docs quote: gomod-v1 516,549,867 B = 0.48 GB gobuild-v3 x5 613,276,117 B = 0.57 GB generation 1,129,825,984 B = 1.05 GB gobuild-v2 x5 5,564,533,024 B = 5.18 GB (0.9-1.2 each) 0.48 + 0.57 = 1.05, and 5.18 / 1.05 = 4.9x. Also corrects what the setup-go entry actually holds. Four places called it a ~1 GB copy of the module tree; setup-go caches GOMODCACHE *and* GOCACHE, and .goreleaser.yaml cross-compiles 8 targets, so roughly half of it is release build objects nothing else caches. Only the other half duplicates gomod-v1. The budget conclusion is unchanged — dropping it frees the whole ~1 GB — but a change whose thesis is precise byte accounting should not misattribute the bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up. The biggest of these is a contradiction I introduced. "Adding a job" step 3 still read "use setup-env with a fresh go-cache-suffix IF it compiles Go with new flags", while the input description now says always pass one. A new Go job whose flags match an existing flavor reads the checklist as "no new flags, no suffix needed", omits it, and gets key gobuild-v3-<os>-go-<hash> with restore-key gobuild-v3-<os>-go- — which prefix-matches every flavor's entry and mints a sixth build cache. That is precisely the cross-flavor restore this PR removes and the sixth entry its sizing policy forbids, reachable by following the checklist 60 lines below the policy. AGENTS.md sends people to this README before editing ci.yml, so the rule has to live here too, not only in the input description. Also splits the "deliberately not cached" rationale, which applied the two release jobs' reasoning to goreleaser-validate.yml as well. That job runs build --single-target with no dockers_v2, and its own comment gives the real reason: the snapshot is fast enough that the post-step save costs more than a cold go mod download. A reader auditing whether cache: false still earns its place there was being handed the wrong justification. Re-flows the module-cache comment, which the units edit left ragged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up, and a correction to this PR's own change. Blanket cache: false on publish-dev.yml measurably regressed it. Across its last 20 runs, GoReleaser splits cleanly on whether setup-go's cache hit: warm (post-step 0-1s): 36 37 39 47 50 54 188 224 241 242 245 246 s cold (post-step 12-15s): 401 414 417 420 431 432 446 s No overlap. Run 29029026651 is the control — docs-only change, go.sum unchanged, entry evicted: 431s against 188-246s for its warm neighbours. So the opt-out cost 3-6.5 minutes on every push to main, and the value sits in ~/.cache/go-build (8 targets: 4 goos x 2 goarch), not the module tree. The rationale this PR shipped — "dominated by cross-compiling and multi-arch docker, not by go mod download" — argued for keeping it. setup-go's bundled cache is still wrong here because it stores the module tree alongside, re-duplicating what gomod-v1 holds once. So: keep cache: false, and cache ~/.cache/go-build alone under gobuild-v3-<os>-go-release- (~0.5 GB vs the bundled ~1 GB). The duplication #443 is about is gone; the cross-compile stays warm. The -release suffix is load-bearing: those objects are cross-compiled for 8 GOOS/GOARCH pairs and share nothing with ci.yml's native-only flavors, so neither side restores bytes it cannot use. It also satisfies the always-pass-a-suffix rule this branch added. release.yml keeps the plain opt-out — tag-triggered, so a warm entry is rarely there to hit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up. 4a48e03 added the repo's only bare actions/cache outside setup-env, contradicting invariant 6 ("Caches are owned end-to-end by setup-env ... No save steps") and the checklist item that enforces it, without updating either. Invariant 6 is now scoped to ci.yml and names publish-dev.yml's release build cache as the deliberate exception — hand-rolled because that workflow doesn't use setup-env at all. The inventory row says so too, since its "Saved by" cell was the only one naming a workflow rather than a ci.yml job, sending a reader to setup-env to find nothing. Three accuracy fixes alongside: - "~1 GB live" reintroduced the unit ambiguity 5bc361c removed. That figure is stored-archive; read as on-disk it contradicts the table 13 lines above, where the module half alone is 1.6 GB on disk. Now "stored". - release.yml's stated reason was wrong. Cache reads fall back to the default branch's scope, so a tag run CAN hit publish-dev's key — which this PR keeps hot on every push to main. The real reason is that a tagged release is rare and not latency-sensitive. Also notes it could restore that key at zero budget cost, since an exact hit never saves. - "mints a sixth build entry" became wrong the moment this PR added the release cache as the sixth. Now ordinal-free so it stays right. And attributes the timing measurements honestly: they were taken on setup-go's bundled entry, which carried the same ~/.cache/go-build tree. The gobuild-v3-<os>-go-release- key is new here, so someone auditing those runs for hits on it would find none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Review follow-up, and an error introduced while fixing the previous one. ab9cfe7 replaced release.yml's wrong reason ("a warm cache is almost never there to hit" — false, tag runs read the default branch's scope) with a second wrong reason: that a tag run "could hit it". It can't. "It" is setup-go's bundled archive, and this PR makes that key cease to exist — publish-dev.yml now opts out, goreleaser-validate.yml already did, and ci.yml runs no setup-go at all. The README even instructs purging the stranded entry. So flipping cache: true there would be a cold miss AND a fresh ~1 GB save, the opposite of what the comment implied. The parenthetical also contradicted itself: an "equivalent" key is a different key, and setup-go only looks up its own. Both the comment and the README paragraph now say what is actually true: nothing mints that key any more; what IS warm is publish-dev's gobuild-v3-<os>-go-release- entry, restorable from the default branch's scope; a tagged release is rare and not latency-sensitive, so it isn't worth a hand-rolled restore step — but that is the lever if it ever is. Also stops "Same call as publish-dev.yml" flattening a distinction the README is careful about: same cache: false, different follow-up, since publish-dev re-caches the useful half. And names the key in the README rather than saying "that key", whose antecedent resolved the wrong way in the release.yml comment — the ambiguity was not hypothetical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Two corrections from review, both checkable and both wrong in exactly the dimension this PR argues about. setup-go's bundled cache is NOT go.sum-keyed at the pinned v7 — it keys on the root go.mod. actions/setup-go#705 (Jan 2026) changed dependencyFilePattern from 'go.sum' to 'go.mod', and the live entry proves it: its key ends 9e56ecf5..., and sha256(sha256(go.mod)) = 9e56ecf5... exactly, while sha256(sha256(go.sum)) = 136f5059... — the hash in every gobuild-v2 key. Four places said go.sum-keyed. Worse, the CHANGELOG contrasted "both Go keys now hash go.mod alongside go.sum" against that "go.sum-keyed" entry, which reads as upstream having the toolchain staleness bug this PR fixes, when upstream fixed precisely that and errs the other way. And the release.yml argument stopped a step short. Cache writes are scoped to the ref that made them, so a save from refs/tags/v1.0.0 can never be read by refs/tags/v1.0.1 — or by anything else, ever. Re-enabling the bundled cache there isn't just a cold miss plus a save; it is a ~1 GB write-only entry burned on every tagged release, permanently unreadable. That is the strongest argument for the opt-out and it was missing. Same fact sharpens the escape hatch: "an exact hit never saves" holds only for an exact hit — a restore-keys prefix match would save, tag-scoped and unreadable. The README now prescribes actions/cache/restore (restore-only) for that lever, which has no save step at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Third pass at the same sentence, and the version boundary was wrong. Verified against the upstream tree rather than inferred this time: v6.2.0 dependencyFilePattern: 'go.sum' v6.3.0 dependencyFilePattern: 'go.mod' <- the switch v7.0.0 dependencyFilePattern: 'go.mod' actions/setup-go#705 merged 2026-01-26 and shipped in v6.3.0. Saying "go.sum through v6" is worse than vague: the floating v6 tag resolves to v6.5.0 today, which keys on go.mod, so it tells a reader that pinning @v6 gets go.sum keying. All four sites now say v6.2.0 / v6.3.0. Also drops an absolute that isn't true. "A save from refs/tags/v1.0.0 can never be read by anything else, permanently unreadable" overstates the scoping rule it cites: a re-run of that same tag's workflow runs at the same ref and does restore it — and a re-run is the realistic case here, since release.yml chains buildx, GHCR login, GoReleaser and attestation. Now: unreadable by another tag, by main, or by a PR — only by a retry. The conclusion is unchanged; the claim is now falsifiable-proof. And fixes a link that rendered as literal text: [actions/setup-go#705] was a shortcut reference with no definition in the file. markdownlint doesn't catch it (MD052's shortcut_syntax defaults false), and every other external reference in that README is inline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
.github/actions/setup-env/action.yml:56
go-cache-suffixis documented here as mandatory for Go jobs (to avoid thegobuild-v3-<os>-go-cross-flavor restore-key and an extra cache entry), but the input still defaults to an empty string. That leaves a footgun where a new Go job can accidentally reintroduce cross-flavor restore behavior and cache bloat simply by omitting the input. Consider making the default non-empty (e.g.-default) so an omitted suffix is safe by construction.
go-cache-suffix:
description: "Per-job Go build-cache partition, e.g. '-unit' (different jobs compile with different flags). Always pass one for a Go job: an empty suffix yields a restore-key that prefix-matches every other flavor's entry, which is the cross-flavor restore the gomod-v1/gobuild-v3 split exists to avoid. The shared part of the cache is gomod-v1, which needs no suffix."
default: ""
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a19a1faf-43ac-48c6-be75-458ef4906aae
📒 Files selected for processing (5)
.github/actions/setup-env/action.yml.github/workflows/README.md.github/workflows/publish-dev.yml.github/workflows/release.ymlCHANGELOG.md
📜 Review details
⏰ Context from checks skipped due to timeout. (11)
- GitHub Check: Coverage
- GitHub Check: E2E tests
- GitHub Check: Docs build
- GitHub Check: Integration tests
- GitHub Check: copilot-pull-request-reviewer
- GitHub Check: Lint
- GitHub Check: Validate snapshot build
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-06-10T15:01:09.027Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: docs/src/content/docs/development.md:0-0
Timestamp: 2026-06-10T15:01:09.027Z
Learning: In this repo’s Markdown review (all .md files), do not flag capitalization/style issues for literal paths starting with ".github/" (or any substring that is a path beginning with ".github/"). Treat ".github" as the correct lowercase dotfile directory name, even when it appears inside prose or code spans; automated checks such as LanguageTool’s "(GITHUB)" rule commonly produce false positives for this literal filesystem path.
Applied to files:
CHANGELOG.md
🪛 LanguageTool
.github/workflows/README.md
[uncategorized] ~123-~123: The official name of this software platform is spelled with a capital “H”.
Context: ...owned end-to-end by setup-env** ([.github/actions/setup-env](../actions/setup-env...
(GITHUB)
[style] ~191-~191: Since ownership is already implied, this phrasing may be redundant.
Context: ...erent flags. go.mod is in the key for its own reason — the compiler's build ID keys e...
(PRP_OWN)
[style] ~209-~209: Since ownership is already implied, this phrasing may be redundant.
Context: ...caches the half that pays for itself on its own key (gobuild-v3-<os>-go-release-, ~0....
(PRP_OWN)
[style] ~230-~230: Since ownership is already implied, this phrasing may be redundant.
Context: ... goreleaser-validate.yml opts out on its own grounds: its --single-target snapshot...
(PRP_OWN)
[style] ~341-~341: This word has been used in one of the immediately preceding sentences. Using a synonym could make your text more interesting to read, unless the repetition is intentional.
Context: ...nvariant 6); a workflow outside it that needs a cache hand-rolls one, as `publish-...
(EN_REPEATEDWORDS_NEED)
🪛 zizmor (1.29.0)
.github/workflows/publish-dev.yml
[error] 76-76: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default
(cache-poisoning)
🔇 Additional comments (4)
.github/actions/setup-env/action.yml (1)
17-28: LGTM!Also applies to: 82-93, 110-150
.github/workflows/publish-dev.yml (1)
53-60: LGTM!Also applies to: 62-81
.github/workflows/release.yml (1)
27-49: LGTM!.github/workflows/README.md (1)
122-132: LGTM!Also applies to: 192-197, 199-232, 234-246, 248-287, 335-342
CodeRabbit's re-review, and the second finding is a real bug this PR
introduced.
`make cov` never declared the go-mod-download prerequisite its siblings
have (lint-go, vulncheck, test-unit, test-integration all do; test-e2e
gets it via $(COVER_BINARIES)). That was harmless under gobuild-v2, where
every job had its own suffixed entry — a partial module tree only
affected the job that saved it. It is not harmless now: gomod-v1 is a
single unsuffixed entry every Go job races to save on a key rotation, and
the coverage job runs only `go run ./scripts/cov report`, which fetches
just the modules that one program imports. If it won that race it would
store a PARTIAL ~/go/pkg/mod under the shared key, which then exact-hits
for every other job — and because saves fire only on a miss, nothing would
ever repair it until the next rotation.
Worth noting an earlier review asserted the opposite ("every Go-toolchain
make target declares the go-mod-download prereq, so the shared entry is
complete no matter which job wins"). Only four targets do, and cov is not
one of them. The coverage job is also the likeliest winner: it starts at
run creation with no `needs` on the suites.
Also enforces the suffix contract instead of only documenting it.
setup-env now fails a go: true job that passes no go-cache-suffix, since
an empty one yields restore-key gobuild-v3-<os>-go-, prefix-matching every
flavor's entry and publish-dev's -release one.
And tightens ownership wording: gomod-v1 and the gobuild-v3 flavors belong
to ci.yml jobs going through setup-env; publish-dev owns only -release,
and release.yml caches nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
…advice Two reviews on dd0e7ee, and the load-bearing item is that the job would have shipped slower than its own documentation claims. The 36-246s "warm" figure justifying publish-dev's hand-rolled cache was measured on setup-go's bundled entry, which carried ~/go/pkg/mod as well. Caching only the build half leaves the module tree cold on every push — ~112 MB re-downloaded — so the job would land above the documented range and read as an unexplained regression to whoever checked next. It now also restores gomod-v1 from main's scope, which ci.yml keeps warm there. Restore-only, deliberately. A full actions/cache adds a post-step save, and this job must never write the entry every ci.yml Go job shares: it runs no full `go mod download`, so a save from here is exactly the partial tree the cov fix in the previous commit exists to prevent. Restore-only writes nothing and costs 0 GB. The guard's error message gave wrong advice to the person likeliest to hit it. `go` defaults to true, so the realistic trigger is a NEW NON-GO job that uses setup-env with no `with:` block — and the message told them to pass a suffix. It now names `go: "false"` as the fix for that case, and the input description states the contract ("REQUIRED when go is true") rather than advising it, since an empty value is now exit 1. Also: the ownership tightening in dd0e7ee missed two sentences written in that same commit (the checklist's "every Go job races to save" and the Makefile's "every other Go job") — both now ci.yml-scoped; CHANGELOG's file list gains Makefile, which that commit modified; the guard comment's "a sixth entry" is off by one now that -release exists; and checklist item 3, having grown to four rules in one paragraph, is now three sub-bullets so the go-mod-download requirement is findable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
The branch drifted against itself. ab9cfe7 amended invariant 6 specifically to name the ONE cache living outside setup-env — "It is the only actions/cache@ in the repo outside the composite — keep it that way" — and 3c14a9f added a second four commits later. The literal string actions/cache@ is still unique only because the new one is actions/cache/restore@, which is a pun rather than a contract: a maintainer grepping actions/cache finds two steps and no way to tell whether the second violates the rule they were just given. Invariant 6 is the normative list, so it now names both — the bare actions/cache owning the release build cache, and the actions/cache/restore that reads gomod-v1 and owns nothing — and states the principle behind the asymmetry: a workflow that needs the shared module tree reads it restore-only; writing it belongs to the ci.yml jobs that run a full go mod download. Verified there are exactly two `uses: actions/cache*` outside the composite and six inside. Also re-flows the two comment blocks the previous commit's rewording left one line wide (Makefile 82 cols in a 63-74 paragraph, action.yml 85 in a 71-82 one). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
The warm/cold clusters are 36-246s and 401-446s, so the envelope between them is 401-246 = 155s to 446-36 = 410s: roughly 2.5-7 minutes, mean delta ~4.8. "3-6.5" is a narrower claim than the data supports, and the sentence invites the subtraction by saying "so". Corrected in all four places it appears (README table row and prose, CHANGELOG, publish-dev.yml comment). Also unpicks a dangling "without which" in the README and CHANGELOG. The CHANGELOG version put twenty-odd words and a plausible-but-wrong antecedent between the pronoun and its referent, so it first parses as "without the key every ci.yml Go job shares" — the opposite of the point. Both now end the clause and start a sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFfYUaPp3Pv3gLjniiHCpm
Why
The repo hit 10.53 GB against GitHub's hard 10 GB Actions cache cap after #438's 24-module bump. Past the cap GitHub LRU-evicts, so warm entries disappear mid-run and builds get slower and less predictable — silently.
setup-envcached the Go module cache and build cache together under a per-flavor key:~/go/pkg/modmeasures 1.6 GB on disk for the currentgo.sumand is byte-identical across all five suffixes, so it was stored five times over — five entries of ~0.9–1.2 GB each, ~5.2 GB per generation. Two live generations is the normal steady state (a bump mints a new set while the previous is still warm), so ~10 GB was the expected footprint. #438 tipped it over:The action's header already noted the duplication — "Cross-suffix restore-keys still share the (identical) module cache on a cold start" — without drawing the sizing conclusion.
What
Split the one cache into two:
gomod-v1-<os>-<go.mod+go.sum hash>~/go/pkg/modgobuild-v3-<os>-go<suffix>-<go.mod+go.sum hash>~/.cache/go-buildMeasured on this branch's own runs: 5.18 GB → 1.05 GB per generation (a 4.9× reduction) —
gomod-v1at 0.48 GB plus fivegobuild-v3entries totalling 0.57 GB (25–152 MB each). Two generations now fit with room to spare.All sizes are stored-archive bytes ÷ 2³⁰, the unit the README's own usage check prints.
(The 0.48 GB module entry is a cold first save with no restore-key fallback; it drifts up as superseded module versions accumulate through the restore→save chain, so budget nearer ~1 GB for it in steady state.)
Two details worth review attention:
v3on the build key is required. Saves fire only on an exact-key miss, so without the bump the oldv2entry — which still contains the module cache — would exact-hit forever and the new, smaller content would never be saved. Same reasoning as the v2 bump documented at the top of the action.gobuild-v3drops the bare-prefix restore-key.gobuild-v2-<os>-go-existed to borrow another flavor's copy of the shared module cache on a cold start. That job now belongs togomod-v1, and another flavor's build objects aren't reusable here, so the fallback would only restore bytes that get thrown away.gomod-v1keeps a prefix restore-key: a bump moves a handful of modules, so a stale generation is still worth restoring andgo mod downloadfetches only the delta.Both keys hash
go.modas well asgo.sum(CodeRabbit review).GOTOOLCHAIN=autolands the toolchain in~/go/pkg/mod/golang.org/toolchainandgo.sumrecords no entry for it — this repo'sgo.sumhas zerogolang.org/toolchainlines. Ago-directive bump would therefore leave ago.sum-only key byte-identical (verified:bcc16701da2c001fbefore and aftergo 1.26.5→1.27.0), exact-hit a toolchain-less archive, and — saves firing only on an exact-key miss — never save the freshly fetched toolchain, re-downloading it every run until some unrelated dependency bump movedgo.sum. The same bump also invalidates every cached build object, since the compiler build ID feeds every action hash.The sixth copy (from review)
Consolidating five copies left a sixth outside
setup-enventirely:actions/setup-gocaches~/go/pkg/mod+~/.cache/go-buildby default, sopublish-dev.ymlcarries a live 0.97 GB entry (keyed on the rootgo.mod— setup-go hashedgo.sumthrough v6.2.0 andgo.modfrom v6.3.0, actions/setup-go#705) — larger than thegomod-v1entry this PR consolidates to, and a direct violation of the sizing rule the PR introduces. It is re-saved on each cache miss (every dependency bump), so the cost is ~1.95 GB across the two generations the repo holds in steady state.release.ymlcarried the same default.Both now pass
cache: false, matching the callgoreleaser-validate.ymlalready made.But
publish-dev.ymlre-caches the half that pays for itself. Reviewing this surfaced a measured regression: across its last 20 runs, GoReleaser takes 36–246 s with setup-go's cache warm and 401–446 s cold — zero overlap between the two groups. Dropping it outright would cost roughly 2.5–7 minutes (mean delta ≈4.8 min) on every push to main, and the value is entirely in the~/.cache/go-buildhalf (8-target cross-compile), not the module tree.So that job now caches
~/.cache/go-buildalone undergobuild-v3-<os>-go-release-(~0.5 GB instead of ~0.97 GB). The duplicate module tree — the actual #443 complaint — is gone; the cross-compile stays warm.And it keeps the module tree warm too. The 36–246 s timings were measured with setup-go's bundled entry, which held
~/go/pkg/modas well — so caching only the build half would have left the module tree cold on every push (~90 modules, ~112 MB re-downloaded) and landed the job above its own documented range.publish-devtherefore also restoresgomod-v1viaactions/cache/restore: read-only, readingci.yml's entry frommain's scope. It writes nothing, so it costs 0 GB of budget and — importantly — cannot write a partial module tree to the key everyci.ymlGo job shares. The-releasesuffix keeps those objects separate from CI's native-only flavors, which can't use them.release.ymlkeeps the plain opt-out — and re-enabling it there would be strictly negative, not merely unhelpful: cache writes are scoped to the ref that made them, so a save fromrefs/tags/v1.0.0can never be read byrefs/tags/v1.0.1, bymain, or by a PR — only by a re-run of that same tag. It would be a ~1 GB entry per release that nothing but a retry can ever read. If release wall-clock ever matters, the lever isactions/cache/restore(restore-only) onpublish-dev's key.Docs
.github/workflows/README.mdgets the updated inventory rows plus a sizing policy section — the 10 GB cap, the two-generations rule, thegh apione-liners to check the current footprint, and the rule that content which is a pure function of a lockfile gets keyed once, unsuffixed. That's the part that stops this recurring.Two additions from review:
gobuild-v3deliberately doesn't, because a v2 archive still contains~/go/pkg/modand restoring it would re-materialize exactly what the rotation removes. The doc now prescribes what the code does.codeql-dependencies-*/codeql-overlay-base-database-*(~0.4 GB) are minted by GHAS default setup, outside this repo's workflows. They were invisible in the inventory, so a maintainer doing the two-generations check was seeing ~85% of the real budget.Verification
make cigreen on this tree (full pipeline incl. integration + e2e)make verifyclean — markdownlint over the README + CHANGELOG, and actionlint over.github/workflows/**.Note:
make lint-ghaglobs workflows only, so.github/actions/setup-env/action.ymlgets no actionlint and no shellcheck — pointed at it directly, actionlint rejects a composite action as a malformed workflow. The new guard step's inlinerun:is therefore hand-verified, not tool-verified: theif:predicate is byte-identical to the twoactions/cachesteps' own conditions, and all 8 call sites were audited (5 suffixed, 3go: "false").Expect the first run on
mainafter merge to be a cold miss on both new keys (one-time repopulation), then warm. Don't read that run's timing as a regression.Post-merge
Purge the orphaned entries — the five dead
gobuild-v2-*(5.18 GB) and thesetup-go-*entry (0.97 GB) thatcache: falseorphans. Nothing restores or refreshes either family after this lands. The repo is at 9.27 GB / 10 GB across 24 entries with them still resident. This branch's own runs add a ~1.05 GB generation on the PR ref (pre-merge peak ~9.3 GB), and merging adds another ~1.05 GB generation plus the ~0.5 GB release cache onmain— so if the PR-scope entries haven't been reclaimed yet the transient peak is ~10.3 GB, over the cap this PR exists to defend. Purge immediately at merge, not later — this is load-bearing, not housekeeping. The orphans would clear on the 7-day idle sweep, but not necessarily before the next dependency bump.Do this after merge, not before —
mainstill uses the v2 keys until then, so an early purge just forces a cold repopulate of caches we're about to abandon.Notes
docs-reviewergate (unrelated to this branch) is tracked in docs: development.md drifted from Makefile; make verify needs pnpm #444 rather than folded in here.Closes #443
🤖 Generated with Claude Code