Skip to content

ci: upgrade golangci-lint to 2.13.1, rework lint config - #59

Merged
min0625 merged 1 commit into
mainfrom
ci/golangci-lint-2.13.1
Aug 28, 2026
Merged

min0625 merged 1 commit into
mainfrom
ci/golangci-lint-2.13.1

Conversation

@min0625

@min0625 min0625 commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Bumps golangci-lint 2.12.2 → 2.13.1 and rewrites .golangci.yaml so every entry carries the reason it is there.

2.13 breaking / behavior changes

  • gofumpt.extra-rules is deprecated. extra is a split, not a rename, and the deprecation warning only names group-params — taking it at face value would have silently dropped clothe-returns, the only thing here that clothes naked returns (nonamedreturns is not enabled). Both are now named explicitly. balance-calls (new in 2.13) stays off: never previously enforced.
  • dupword now scans string literals. The stub LLM in main_test.go answers every chunk with OK, so a multi-chunk expectation is literally "OK\n\nOK\n" — 3 findings. Handled with dupword.ignore: [OK] rather than excluding dupword from _test.go wholesale, which would also stop it catching real duplicated words in test comments.
    This is a 2.13 behavior change, not fallout from the config rewrite — verified by running the new config under 2.12.2: 0 issues.

Removed as no-ops or duplicates

Removed Why
severity.default: error Strict no-op — text output ignores severity, and the formats that honour it (checkstyle, sarif) already emit error when nothing is configured.
run.timeout: 5m v2 has no default timeout. Editing this file busts the CI lint cache key, so a timeout only ever bites the slowest (cold) run. timeout-minutes on the job is the outer guard.
gofmt, gci gofumpt is a strict superset of gofmt and already splits std imports into their own group. goimports stays — it alone drops unused imports.
copyloopvar, intrange Owned by modernize (forvar, rangeint). The gofmt interface{} → any rewrite rule is likewise replaced by modernize's any.
whitespace wsl_v5 reports the same leading-/trailing-whitespace-in-a-block cases (verified against a probe file); gofumpt rewrites them too.

Tightened

  • max-same-issues: 0, max-issues-per-linter: 0, uniq-by-line: false — CI is a repo-wide gate, so a truncated report is a silently missed violation, and a dropped issue never gets auto-fixed either.
  • goheader gets the template the repo already uses, with the year as a regexp (20\d{2}) so files keep the year they were written with instead of churning every January.
  • nolintlint now requires a specific linter and an explanation — a bare //nolint disables everything on that line forever.
  • depguard / gomodguard_v2 split by what each can actually express (import paths vs. whole modules + version constraints + local replace directives), so no import is reported twice. gomodguard_v2.local-replace-directives catches a replace => ../local that would break the tagged goreleaser build.
  • mise.toml: version pin written bare (2.13.1) to match go / goreleaser / prek.

Verification

  • golangci-lint config verify — clean.
  • golangci-lint run ./... (full repo, not just new-from-rev) — 0 issues.
  • make check NEW_FROM_REV=origin/main — full prek suite passes.

🤖 Generated with Claude Code

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Migrate `gofumpt.extra-rules` to the 2.13 `extra` block -- it is a split,
not a rename, so each sub-rule is named explicitly. Drop the linters now
covered by others (gci/gofmt by gofumpt+goimports, copyloopvar/intrange by
modernize, whitespace by wsl_v5) plus dupword and goheader, whose value
does not carry for this repo. Lift both issue caps and turn off
uniq-by-line so a CI run reports every finding instead of one per line.

Add gomodguard_v2 with a deprecated-module blocklist and
local-replace-directives, which catches a `replace => ../local` that would
break the tagged goreleaser build. Azure track 1 goes in that list rather
than in depguard: depguard matches raw string prefixes, so a rule naming
the module root would also block the track 2 modules under sdk/, and its
allow list cannot carve them back out.

Extend govet with the non-default analyzers (deepequalerrors, nilness,
reflectvaluecompare, sortslice, unusedwrite); `inline` is left off the
list because it is enabled by default.

Sync the pinned version in AGENTS.md -- 2.12.2 rejects this config
outright -- and document the whole-files + --new-from-rev ratchet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@min0625
min0625 force-pushed the ci/golangci-lint-2.13.1 branch from 585831d to 5cfd14c Compare August 28, 2026 15:46
@min0625
min0625 merged commit fa33647 into main Aug 28, 2026
6 checks passed
@min0625
min0625 deleted the ci/golangci-lint-2.13.1 branch August 28, 2026 15:48
min0625 added a commit that referenced this pull request Sep 1, 2026
Re-add gci. #59 dropped it as covered by gofumpt+goimports, but those two
only sort within the groups a file already has: a third-party import
stranded in the stdlib block is moved out into a group of its own rather
than merged into the existing third-party block. gci is what collapses it
back to the canonical two groups.

Replace gomodguard_v2's local-replace-directives with the gomoddirectives
linter. gomodguard only sees directly imported modules, so an indirect
dep's replace slipped through; gomoddirectives forbids every replace by
default -- checked against a resolvable non-local one, not just a
`=> ../local`.

Retire depguard. Its one rule (github.com/pkg/errors) moves into the
gomodguard_v2 blocklist so the recommendation text sits next to the other
deprecated modules. mitchellh/mapstructure and gopkg.in/yaml (prefix
match, so both majors) join it.

Enable bidichk, makezero and reassign. reassign gets patterns: ".*", which
is upstream's own recommendation -- the default only guards EOF and Err*,
leaving os.Args and http.DefaultClient open. Tests are excluded from it:
they borrow os.Stdin/os.Args and restore them afterwards.

Turn on errcheck.check-type-assertions and prealloc.for-loops (modernize
rewrites 3-clause loops into the range form prealloc polices, so with the
default the finding only surfaces after --fix), and turn off
perfsprint.concat-loop, which modernize rewrites better by reusing the
variable instead of inventing one.

`make fix` now runs tidy -> --fix -> tidy -> lint. --fix is not a
fixpoint: its edits can trip a linter the fixing run never saw, and can
add or remove imports.

`golangci-lint run ./...` reports 0 issues repo-wide under this config, so
the whole-files ratchet takes on no new debt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
min0625 added a commit that referenced this pull request Sep 1, 2026
Re-add gci. #59 dropped it as covered by gofumpt+goimports, but those two
only sort within the groups a file already has: a third-party import
stranded in the stdlib block is moved out into a group of its own rather
than merged into the existing third-party block. gci is what collapses it
back to the canonical two groups.

Replace gomodguard_v2's local-replace-directives with the gomoddirectives
linter. gomodguard only sees directly imported modules, so an indirect
dep's replace slipped through; gomoddirectives forbids every replace by
default -- checked against a resolvable non-local one, not just a
`=> ../local`.

Retire depguard. Its one rule (github.com/pkg/errors) moves into the
gomodguard_v2 blocklist so the recommendation text sits next to the other
deprecated modules. mitchellh/mapstructure and gopkg.in/yaml (prefix
match, so both majors) join it.

Enable bidichk, makezero and reassign. reassign gets patterns: ".*", which
is upstream's own recommendation -- the default only guards EOF and Err*,
leaving os.Args and http.DefaultClient open. Tests are excluded from it:
they borrow os.Stdin/os.Args and restore them afterwards.

Turn on errcheck.check-type-assertions and prealloc.for-loops (modernize
rewrites 3-clause loops into the range form prealloc polices, so with the
default the finding only surfaces after --fix), and turn off
perfsprint.concat-loop, which modernize rewrites better by reusing the
variable instead of inventing one.

Exclude gosec's G104. It fires on bare call statements only, never on
defer/go, and honors a whitelist -- a strict subset of errcheck, which
also names the offending function; with uniq-by-line: false both would
print on the same line. Narrowing errcheck (exclude-functions,
std-error-handling) would reopen the gap.

Correct the check-rev rationale in the Makefile and AGENTS.md. #58
documented golangci-lint as only warning and exiting 0 on an unresolvable
--new-from-rev, which would let CI pass having linted nothing. It does
not: it warns, reports every issue in the repo, and exits 1. The guard
stays, but it buys a readable error message, not safety.

`make fix` now runs tidy -> --fix -> tidy -> lint. --fix is not a
fixpoint: its edits can trip a linter the fixing run never saw, and can
add or remove imports. The new ordering was exercised on a clean tree, so
it confirms the plumbing runs, not that the second pass caught a real
fixpoint miss.

Also document in AGENTS.md that .golangci.yaml is not self-contained --
whole-files: true is silently inert without one of the --new* modes, so
the file only works alongside the Makefile's --new-from-rev harness --
and that formatter findings are the exception to the ratchet: apply
`golangci-lint fmt ./...` repo-wide rather than letting a half-formatted
tree contradict its own config.

`golangci-lint run ./...` reports 0 issues repo-wide under this config, so
the whole-files ratchet takes on no new debt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
min0625 added a commit that referenced this pull request Sep 1, 2026
Re-add gci. #59 dropped it as covered by gofumpt+goimports, but those two
only sort within the groups a file already has: a third-party import
stranded in the stdlib block is moved out into a group of its own rather
than merged into the existing third-party block. gci is what collapses it
back to the canonical two groups.

Replace gomodguard_v2's local-replace-directives with the gomoddirectives
linter. gomodguard only sees directly imported modules, so an indirect
dep's replace slipped through; gomoddirectives forbids every replace by
default -- checked against a resolvable non-local one, not just a
`=> ../local`.

Retire depguard. Its one rule (github.com/pkg/errors) moves into the
gomodguard_v2 blocklist so the recommendation text sits next to the other
deprecated modules. mitchellh/mapstructure and gopkg.in/yaml (prefix
match, so both majors) join it.

Enable bidichk, makezero and reassign. reassign gets patterns: ".*", which
is upstream's own recommendation -- the default only guards EOF and Err*,
leaving os.Args and http.DefaultClient open. Tests are excluded from it:
they borrow os.Stdin/os.Args and restore them afterwards.

Turn on errcheck.check-type-assertions and prealloc.for-loops (modernize
rewrites 3-clause loops into the range form prealloc polices, so with the
default the finding only surfaces after --fix), and turn off
perfsprint.concat-loop, which modernize rewrites better by reusing the
variable instead of inventing one.

Exclude gosec's G104. It fires on bare call statements only, never on
defer/go, and honors a whitelist -- a strict subset of errcheck, which
also names the offending function; with uniq-by-line: false both would
print on the same line. Narrowing errcheck (exclude-functions,
std-error-handling) would reopen the gap.

Correct the check-rev rationale in the Makefile and AGENTS.md. #58
documented golangci-lint as only warning and exiting 0 on an unresolvable
--new-from-rev, which would let CI pass having linted nothing. It does
not: it warns, reports every issue in the repo, and exits 1. The guard
stays, but it buys a readable error message, not safety.

`make fix` now runs tidy -> --fix -> tidy -> lint. --fix is not a
fixpoint: its edits can trip a linter the fixing run never saw, and can
add or remove imports. The new ordering was exercised on a clean tree, so
it confirms the plumbing runs, not that the second pass caught a real
fixpoint miss.

Also document in AGENTS.md that .golangci.yaml is not self-contained --
whole-files: true is silently inert without one of the --new* modes, so
the file only works alongside the Makefile's --new-from-rev harness --
and that formatter findings are the exception to the ratchet: apply
`golangci-lint fmt ./...` repo-wide rather than letting a half-formatted
tree contradict its own config.

`golangci-lint run ./...` reports 0 issues repo-wide under this config, so
the whole-files ratchet takes on no new debt.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant