Isolate golangci-lint cache per worktree to stop cross-worktree contamination#5925
Open
janniklasrose wants to merge 2 commits into
Open
Isolate golangci-lint cache per worktree to stop cross-worktree contamination#5925janniklasrose wants to merge 2 commits into
janniklasrose wants to merge 2 commits into
Conversation
…mination golangci-lint keys its results cache on file *contents* but stores each cached issue's *absolute* path. Two worktrees with byte-identical packages (the near-static tools/ module is the usual culprit) get a cross-worktree cache hit, so the second inherits the first's absolute paths. The //nolint processor then re-opens those paths to apply suppressions; when the other worktree has been deleted the open fails, so suppressed issues (e.g. dupword) leak through as phantom failures pointing at a gone path, plus a wall of "no such file or directory" warnings. The content-addressable cache keys added in v2.12.0 only fixed the concurrent case (both worktrees live). The deleted-worktree case still reproduces on the pinned v2.12.2. Rooting GOLANGCI_LINT_CACHE under the gitignored per-worktree .task/ makes contamination structurally impossible. CI is unaffected: it runs a single checkout and never persists this directory (setup-go caches only the Go build cache). Co-authored-by: Isaac
Contributor
Waiting for approvalBased on git history, these people are best suited to review:
Eligible reviewers: Suggestions based on git history. See OWNERS for ownership rules. |
Collaborator
Integration test reportCommit: 4085d52
8 interesting tests: 4 SKIP, 3 RECOVERED, 1 KNOWN
Top 10 slowest tests (at least 2 minutes):
|
Per-worktree GOLANGCI_LINT_CACHE isolation removes cross-worktree cache reuse, so a fresh worktree's first full `task lint` runs cold (~2min on the root module vs ~20s warm; the incremental `task lint-q` in the default dev loop is unaffected, <1s either way). Two mitigations for developers with many worktrees: - Opt-out: Task's `env:` yields to an inherited value, so exporting GOLANGCI_LINT_CACHE (e.g. to the shared ~/Library/Caches/golangci-lint) restores cross-worktree reuse without editing the Taskfile. - `task seed-lint-cache`: copies the main worktree's cache into the current one so the first full lint runs warm (~4s in testing). Safe because the main worktree is permanent, so its cached issue paths always resolve; the task no-ops when GOLANGCI_LINT_CACHE has been overridden. Also link golangci/golangci-lint#6656, where the maintainer confirmed the shared-cache path contamination is an accepted trade-off of #6445. Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Running lint from a worktree under
~/pub/cli.worktrees/*intermittently fails with phantom lint issues pointing at file paths in other worktrees — including deleted ones (failed to parse file … no such file or directory).golangci-lint keys its results cache on file contents but stores each cached issue's absolute path. Two worktrees with byte-identical packages (the near-static
tools/module is the usual culprit) get a cross-worktree cache hit in the machine-global~/Library/Caches/golangci-lint, so the second worktree inherits the first's absolute paths. The//nolintpost-processor then re-opens those paths to apply suppressions; when the other worktree has been deleted the open fails, so suppressed issues (e.g.dupword) leak through as phantom failures.Upstream: this is golangci/golangci-lint#6656, closed as an accepted trade-off (won't-fix, no opt-out flag shipped) of the shared-cache work in #6445 ("decrease cache entropy", which dropped the path from the cache key in v2.12.0). The maintainer's position: "the core of linters is required absolute paths." A commenter reproduced the exact
//nolint-leak variant. #6445 also notes concurrent cross-worktree runs on the shared cache are unsafe (file races).Fix
Set
GOLANGCI_LINT_CACHEto a per-worktree path ({{.ROOT_DIR}}/.task/golangci-lint) in a top-levelenv:block, so the cache lives under each worktree's already-gitignored.task/. No two worktrees can share it → contamination is structurally impossible.Verified with disposable worktrees (create A → lint → delete A → lint identical B): 3 phantom
dupwordfailures before,0 issuesafter. CI is unaffected: thelintjob runs a single clean checkout and never persists this directory (setup-gocaches only the Go build cache).Performance trade-off (measured)
Isolation removes cross-worktree cache reuse, so a fresh worktree's first full lint runs cold. Measured on the root module with the shared
go-buildcache kept warm (only golangci-lint's own issues cache is affected):task lintwarm (steady state)task lintcold golangci, warm go-build (fresh worktree, our change)task lintfresh worktree reusing shared cache (today's default)task lint-q(incremental, the default./taskdev loop) cold or warmSo the penalty only hits full
task lint/full/all. The everyday./taskloop (lint-q, changed files only) is unaffected.Two mitigations for developers with many worktrees:
env:yields to an inherited value, soexport GOLANGCI_LINT_CACHE=~/Library/Caches/golangci-lintrestores cross-worktree reuse with no Taskfile edit.task seed-lint-cache: copies the main worktree's cache into the current one so the first full lint runs warm (~4s in testing). Safe because the main worktree is permanent, so its cached issue paths always resolve; no-ops whenGOLANGCI_LINT_CACHEis overridden.Testing
Reproduced the failure and confirmed the fix, the opt-out precedence, and the seed task end-to-end using disposable detached worktrees.
yamlfmt -lintclean.This pull request and its description were written by Isaac.