Repository navigation
star lint go reports 'go.mod not tidy' for any uncommitted go.mod edit #686
Description
Epic: #452
Related: #670 (CI must be a strict superset of the local checks)
star lint go reports "go.mod not tidy" for any uncommitted go.mod or go.sum change, whether or not
go mod tidy produced it.
The defect
cmd/star/provider/lint/provider.go:396:
func checkModTidy(ctx context.Context) (tidy bool, detail string) {
tidyCmd := exec.CommandContext(ctx, "go", "mod", "tidy")
if output, err := tidyCmd.CombinedOutput(); err != nil {
return false, fmt.Sprintf("go mod tidy failed: %s\n%s", err, string(output))
}
diffCmd := exec.CommandContext(ctx, "git", "diff", "--exit-code", "go.mod", "go.sum")
if output, err := diffCmd.CombinedOutput(); err != nil {
return false, fmt.Sprintf("go.mod or go.sum not tidy:\n%s", string(output))
}
return true, ""
}It runs go mod tidy, then compares the result to HEAD rather than to the pre-tidy state. The two are the
same thing only in a clean tree.
Reproduction, observed 2026-08-25
Bumping the go directive from 1.26.0 to 1.27.0 (uncommitted), then make lint:
[star] [✖] go.mod is not tidy
[star] [+] -go 1.26.0
[star] [+] +go 1.27.0
[star] [✔] No golangci-lint issues found
[star] [✖] Go lint failed: go.mod not tidy
The diff shown is the developer's own edit. go mod tidy produced zero changes — verified by snapshotting
go.mod and go.sum before the run and diffing after: no go.mod change, no go.sum churn.
Why CI never sees it
CI checks out a clean tree, so go.mod matches HEAD and the only diff git diff can report is one go mod tidy
actually made. The check is correct there and wrong everywhere else — which is precisely the workflow a developer
has: edit go.mod, run the gate, get a false failure until the edit is committed.
It also means the check cannot be satisfied while the edit is uncommitted, so the gate blocks the commit that
would satisfy it.
The fix
Compare against the pre-tidy state, not HEAD: copy go.mod and go.sum, run go mod tidy, diff the copies
against the results, restore on mismatch. That works in a dirty tree, in CI, and outside a git repository —
the current implementation requires all three of a git repo, a clean tree, and a committed baseline.
Severity
It is a false failure in the gate developers are now required to run: make lint routes through
star lint go as of 2026-08-25, so this blocks any workflow that touches go.mod before committing.
Activity
David-Noble-at-work commentedon Aug 26, 2026
Fixed in
cmd/star/provider/lint/provider.go.The change
checkModTidynow compares the module files against their own pre-tidy content, never against HEAD:snapshotModuleFiles()records the bytes, mode, and existence ofgo.modandgo.sumgo mod tidyrunsThat last step is new behaviour. The old implementation ran tidy for real and left whatever it wrote in place — a
linter that silently rewrote your source. It now reports without mutating.
Two other properties fall out: the check no longer needs a git repository, and no longer needs a committed
baseline.
Verified both directions
The false positive is gone. With the uncommitted
go 1.26.0→go 1.27.0bump still in the tree — the exactcondition that produced this issue:
Genuine untidiness is still caught. Injecting an unused
github.com/google/uuid v1.6.0require:And the restore holds. After that failing run, the injected require was still present in
go.mod— the checkreported the problem rather than quietly fixing it. Confirmed by grep, then reverted.
Context
Found while routing
make lintthroughstar lint goso the local gate matches CI (#670). Routing exposed thatstar lint gois golangci-lint plus a tidy gate that localmake lintnever ran — a third divergencebetween the two gates, alongside the golangci-lint version skew and the GOOS coverage gap.
chore: sandbox vocabulary, lint-gate parity, and the access-directive…
Providers claim what they guarantee, and a type-checked pass holds th…