Skip to content

chore(lint): enable errorlint and make error identity survive wrapping - #6985

Merged
otavio merged 1 commit into
masterfrom
chore/lint-errorlint
Aug 28, 2026
Merged

chore(lint): enable errorlint and make error identity survive wrapping#6985
otavio merged 1 commit into
masterfrom
chore/lint-errorlint

Conversation

@otavio

@otavio otavio commented Aug 28, 2026

Copy link
Copy Markdown
Member

What

Enables errorlint and fixes the 29 sites it reports. It catches the three ways an error check silently stops working once anything in the chain wraps the error: ==/!= comparison against a sentinel, a type assertion on error, and %v where fmt.Errorf should use %w.

Why

Task 5 of the linting ledger. This is the last batch in the series with real semantic weight rather than mechanical rewrites: %v versus %w decides what errors.Is can reach, and a bare == against a sentinel is a check that passes review and then quietly stops matching the first time a caller wraps.

Changes

Most of the 29 are one pattern: the err != nil && err != io.EOF guard after io.Copy in the SSH channel plumbing and the agent's session modes. Those are mechanical.

Three are not:

  • pkg/validator: StructWithFields did an unchecked err.(validator.ValidationErrors). Validate.Struct returns *InvalidValidationError when handed a non-struct, so that assertion panicked. It now reports the structure invalid with an empty field map.
  • pkg/errors: WithData matched its parent with a bare type assertion and returned nil for anything else, dropping the error entirely. errors.As finds a wrapped Error instead of swallowing it. All eight callers pass a bare sentinel, so nothing changes today — this only removes a way to lose an error later.
  • pkg/revdial: the listener-closed error formatted its cause with %v, so errors.Is could not reach it. It wraps now.

server/api/store/pg/utils.go and the bun query hook switched on the error value; both are errors.Is chains now.

Testing

fromSQLError has the table in server/api/store/pg/utils_test.go covering the wrapped and joined cases, including that context.Canceled still passes through unwrapped.

Verified locally: lint clean across all six modules plus the agent under both docker and native tags; builds clean; unit tests pass for every touched package. The enrollment_e2e tests fail in my container for want of a Docker socket and fail identically on master.

errorlint rejects the three ways an error check silently stops working once
something in the chain wraps it: == comparison, a type assertion, and a %v verb
in fmt.Errorf. 29 sites, most of them the io.Copy guard `err != io.EOF`.

Three are more than mechanical:

- pkg/validator: StructWithFields asserted err.(validator.ValidationErrors)
  unchecked. A non-struct argument makes Validate.Struct return
  InvalidValidationError instead, so the assertion panicked. It now reports the
  invalid structure with no fields.
- pkg/errors: WithData matched its parent with a bare type assertion and
  returned nil on anything else, dropping the error. errors.As finds a wrapped
  Error instead of swallowing it. All eight callers pass a bare sentinel, so
  none change behaviour today.
- pkg/revdial: the Listener-closed error formatted its cause with %v, so
  errors.Is could not reach it. It wraps now.
@otavio
otavio requested review from a team as code owners August 28, 2026 21:48
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @otavio's task in 1m 50s —— View job


Code Review Complete

Reviewed 16 files across code quality, security, testing, language patterns, and architecture — no new issues found. The code looks good as-is.

Notes (non-blocking)

I verified the parts of this diff that carry actual semantic weight, and they hold up:

  • Compilation / imports — every file using errors.Is/errors.As/%w has the import. agent/pkg/agentd/handlers.go resolves errors.Is through github.com/pkg/errors (v0.9.1), whose Is/As/Unwrap wrappers delegate to stdlib, so it's equivalent and compiles.
  • pkg/errors.WithDataerrors.As(parent, &err) with var err Error is valid: Error has a value receiver on Error(), so it satisfies error and errors.As won't panic. The behavior note in the PR (matching a wrapped Error returns the inner copy) is accurate and harmless for the eight bare-sentinel callers.
  • pkg/validator.StructWithFields — the fix is correct: Validate.Struct returns *InvalidValidationError for a non-struct, errors.As into validator.ValidationErrors returns false, and the function now reports invalid with an empty field map instead of panicking on the old unchecked assertion.
  • logrusbun.go — the switch event.Errswitch { case ... } conversion (plus the extracted isQuiet) is faithful; both nil and sql.ErrNoRows still land in the non-error branch.
  • revdial %v%w and the fromSQLError / redis / SSH-copy sites are all mechanical and correct; ErrListenerClosed is still returned separately so errors.Is reaches both paths.

To request another review round, comment /review.

@otavio
otavio merged commit 8426191 into master Aug 28, 2026
45 checks passed
@otavio
otavio deleted the chore/lint-errorlint branch August 28, 2026 22:26
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