Skip to content

Add the shared Go lint policy and make kit follow it - #87

Open
mariusvniekerk wants to merge 5 commits into
mainfrom
t3code/shared-golangci-kit
Open

mariusvniekerk wants to merge 5 commits into
mainfrom
t3code/shared-golangci-kit

Conversation

@mariusvniekerk

@mariusvniekerk mariusvniekerk commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Kit now owns the Go lint policy for kenn-io repositories, and kit itself lints clean against it.

Nineteen repositories had drifted to nineteen golangci-lint configurations, with enabled linters ranging from two to sixty, five different pinned versions, and the testify helper analyzer copy-pasted into six repositories with divergent edits. The failures that reviewers and CI caught late were exactly the ones nobody enforced consistently: stdlib assertions in tests, wall-clock sleeps, error identity decided by matching err.Error() text, context-free subprocess and network calls, and raw net/http route registration.

golangci-lint has no configuration inheritance, so lint/config holds the canonical file and kennlint config renders a repository's .golangci.yml from it plus a small overlay of local additions; -check fails CI when the committed file is stale. The five analyzers (testifyhelper, sleeptest, errtext, nohttpmux, sqlenum) ship as the kennlint golangci-lint module plugin so //nolint and path exclusions behave like any other linter. kennlint run executes them directly for editors, and kennlint sql applies the enum CHECK constraint check to .sql migration files that golangci-lint cannot see. docs/adopting-kennlint.md covers adoption and staged rollout.

Review effort concentrates in a few places; the rest of the diff is mechanical migration of kit's tests and code to the policy.

Area What to look at
lint/config Merge semantics (lists append, scalars override, disable prunes) and the linter set. contextcheck and containedctx were left out because kit's cleanup contexts and ctx-carrying readers are deliberate; gocritic is limited to diagnostic checks.
lint/sqlenum Regex-based recognition of enum shapes anywhere inside a CHECK expression (col IN (...), col IS NULL OR col IN (...), nested NOT IN, per-branch state = 'a' ... OR state = 'b' ..., PostgreSQL = ANY (ARRAY[...])) plus CREATE TYPE ... AS ENUM. A survey of the downstream migration directories showed most hard-coded sets use the nullable or per-branch forms rather than a bare IN list. Range checks, single-literal invariants, and subqueries stay unflagged.
lint/testifyhelper Now recognises any variable bound to assert.New(t) or require.New(t), because shadowing the package cannot express a nested subtest that needs its own helper.
daemon/endpoint.go Endpoint.Listen takes a context so the listener is created through net.ListenConfig. Its only caller already had one.
//nolint sites Each remaining suppression carries a reason the policy now requires: sleeps around subprocesses and HTTP fixtures, fixed OS temp roots for short unix socket paths, the detached daemon start, git stderr matching, three fallback-on-error returns, and Proof's value-receiver Format that keeps redaction working.

One trap surfaced during migration and is documented for other repositories: t.Context() is already cancelled inside t.Cleanup, and helper subprocesses started with it are killed at cleanup, so those sites derive context.WithoutCancel(t.Context()).

🤖 Generated with Claude Code

Every kenn-io Go repository carried its own golangci-lint configuration.
Nineteen configs had drifted apart: enabled linters ranged from two to sixty,
pinned versions spanned five releases, and the testify helper analyzer had
been copied into six repositories with divergent edits. Agents moving between
repositories kept re-learning the same rules, and the failures that reviewers
and CI caught late were the ones no repository enforced consistently: stdlib
test assertions, wall-clock sleeps in tests, error identity decided by
matching err.Error() text, context-free calls, and raw net/http route
registration.

golangci-lint has no configuration inheritance, so the shared policy lives in
kit as a canonical file plus a renderer. A repository commits only an overlay
with its local additions, generates .golangci.yml from the two, and a drift
check keeps the committed file honest. The custom analyzers ship as a
golangci-lint module plugin so //nolint and path exclusions work like any other
linter, and kennlint also runs them directly for editors and repositories
without a custom build.

Migrations kept as .sql files are outside golangci-lint's reach, so the enum
CHECK constraint check (CHECK (status IN ('queued', 'done'))) also runs as a
standalone scanner. Those constraints turn every new value into a schema
migration that rewrites the constraint; the allowed set belongs in application
code or a lookup table.

Kit itself now lints clean against the full policy. Most of the roughly two
thousand pre-existing findings were converted mechanically; the few
suppressions that remain each carry a reason, which the policy now requires.
Two consequences for callers: Endpoint.Listen takes a context so the listener
is created through net.ListenConfig, and the testify helper analyzer accepts
any variable bound to assert.New(t) or require.New(t), because the convention
of shadowing the package cannot express a nested subtest that needs its own
helper.

Generated with Claude Code
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Sep 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (593b798)

Verdict: Changes require fixes for 4 findings.

Medium

  • daemon/endpoint.go:193: Endpoint.Listen was changed from a no-argument exported method to Listen(ctx), breaking source compatibility for downstream callers. Preserve the existing Listen() API and add a separately named context-aware method, or explicitly handle this as a versioned breaking change.

    Reported by: codex

  • packstore/s3store/backend_integration_test.go:47: Cleanup callbacks capture t.Context(), which testing cancels before cleanup runs, causing S3 cleanup failures and skipping git worktree metadata cleanup. Derive a cleanup context with context.WithoutCancel(t.Context()), optionally bounded by a cleanup timeout.

    Reported by: codex

  • lint/config/config.go:46: The config renderer applies disable entries before merging the overlay, allowing an overlay enable to re-enable an item that was disabled. Apply disable filtering after merging the base and overlay, or remove disabled names from both enable lists; add a conflict test.

    Reported by: codex

  • lint/sqlenum/sqlenum.go:54: The SQL enum analyzer accepts OR/AND chains containing different column identifiers and reports them as an enum constraint for the first column, producing false positives. Require every comparison in the complete chain to use the same normalized column identifier; add a mixed-column negative test.

    Reported by: codex


Reviewers: 2 done | Synthesis: codex, 11s | Total: 13m57s

@roborev-ci

roborev-ci Bot commented Sep 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (1a3c24c)

Verdict: Changes require fixes for 2 findings.

Medium

  • lint/sqlenum/sqlenum.go:60: Scan searches CHECK expressions across the entire source without skipping SQL comments or quoted text, so examples such as -- CHECK (status IN ('a')) can produce false diagnostics and block lint. Lex the SQL enough to ignore comments and string/quoted regions when locating CHECK keywords.

    Reported by: codex

  • lint/sqlenum/sqlenum.go:54: The orChain regex captures the first identifier but allows later terms to use different identifiers, so a predicate such as CHECK (status = 'queued' OR kind = 'x') is incorrectly reported as an enum for status. Parse each comparison and require every term to reference the same normalized identifier before reporting it.

    Reported by: codex


Reviewers: 2 total (1 done, 1 failed) | Synthesis: codex | Total: 9m13s

mariusvniekerk and others added 3 commits September 16, 2026 11:56
CI lints the merge with main on Linux, which exposed findings in files my
macOS run never compiled: the newly merged huma-check tool, the Linux-only
daemon and packstore tests, and the Windows-only sources. Bring all of them
under the policy and check the Windows build too, so the lint stays green on
every platform kit builds for.

The S3 conformance test cleaned its prefix from a t.Cleanup closure using the
test context, which is already cancelled by then; run cleanup under
context.WithoutCancel so the delete calls actually reach the service.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Surveying the downstream migration directories showed that most hard-coded
value sets are not bare `col IN (...)` expressions: they hide behind a
nullable prefix (`col IS NULL OR col IN (...)`), sit inside a larger AND/OR
expression as `NOT IN`, or spell the set out one state per branch
(`(state = 'a' AND ...) OR (state = 'b' AND ...)`). Every one of those still
forces a migration when a value is added, which is the thing the check exists
to prevent, so match the enum shapes anywhere inside the expression instead of
requiring the whole constraint to be one. Also recognize the PostgreSQL
`= ANY (ARRAY[...])` spelling and `CREATE TYPE ... AS ENUM`, which locks the
set in the same way.

Single-literal invariants, range and length checks, function-derived subjects
such as SUBSTR(...), and subqueries stay unflagged; the negative table test
pins that down.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d bubbles

Endpoint.Listen gained a context parameter to satisfy noctx, which broke
every downstream caller for a lint-policy change. Keep Listen() and add
ListenContext for callers that have a context.

The SQL scanner searched raw text, so CHECK constraints quoted in comments
or string literals were reported. Mask line comments, block comments, and
single-quoted strings before locating keywords, keeping offsets intact.

sleeptest only exempted function literals written inline in synctest.Test;
callbacks passed by name (declared functions or function-valued variables)
were reported even though they run inside the bubble. Resolve identifiers to
their bodies and exempt those too.

Fix the analyzer count in the adoption guide and state the Go version the
policy assumes, since some remediations need Go 1.26 APIs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Sep 16, 2026

Copy link
Copy Markdown

roborev: Combined Review (5095f3b)

Verdict: Changes require fixes for 4 findings.

Medium

  • lint/config/config.go:95: Overlay disables are applied before merging overlay enables, so a linter listed in both enable and disable is re-added and remains enabled. Apply disables to the fully merged configuration, or remove disabled names from both base and overlay enable lists.

    Reported by: codex

  • lint/sqlenum/sqlenum.go:210: matchParen scans raw SQL without skipping comments or escaped SQL string contents, so parentheses in comments or quoted values can terminate the scan early and miss valid enum-style constraints. Balance parentheses over the same comment/string-masked source, with proper SQL quote escaping support.

    Reported by: codex

  • cmd/kennlint/main.go:108: kennlint config writes the default .golangci.yml with os.WriteFile, which follows symlinks and can overwrite an unintended target in a malicious checkout. Inspect the destination with Lstat and reject symlinks or require explicit opt-in, then write through a no-follow, atomic replacement path.

    Reported by: codex (security)

  • tools/humacheck/jsonfix.go:118: rewriteJSONV1File uses os.Stat/os.ReadFile on a path derived from repository diagnostics and then replaces it, allowing an attacker-controlled .go symlink to disclose a private Go file. Reject symlink and non-regular targets with Lstat/no-follow opens before reading, and only atomically replace the verified regular file.

    Reported by: codex (security)


Reviewers: 2 done | Synthesis: codex, 12s | Total: 37m17s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant